From cb5079a3843d20711358e4592a0fc3e5788ed4ea Mon Sep 17 00:00:00 2001 From: Kwoth Date: Sun, 24 Jul 2016 01:26:43 +0200 Subject: [PATCH 1/5] mostly rewritten music, doesn't work on large number of concurrent streams properly >.> --- NadekoBot/Modules/Music/Classes/Song.cs | 136 +++++++++++++----------- NadekoBot/Modules/Music/MusicModule.cs | 7 +- 2 files changed, 78 insertions(+), 65 deletions(-) diff --git a/NadekoBot/Modules/Music/Classes/Song.cs b/NadekoBot/Modules/Music/Classes/Song.cs index 9f2fb382..91c866e8 100644 --- a/NadekoBot/Modules/Music/Classes/Song.cs +++ b/NadekoBot/Modules/Music/Classes/Song.cs @@ -3,6 +3,7 @@ using NadekoBot.Classes; using NadekoBot.Extensions; using System; using System.Diagnostics; +using System.Diagnostics.Contracts; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -74,7 +75,7 @@ namespace NadekoBot.Modules.Music.Classes return this; } - private Task BufferSong(CancellationToken cancelToken) => + private Task BufferSong(string filename, CancellationToken cancelToken) => Task.Factory.StartNew(async () => { Process p = null; @@ -89,32 +90,9 @@ namespace NadekoBot.Modules.Music.Classes RedirectStandardError = false, CreateNoWindow = true, }); - const int blockSize = 3840; - var buffer = new byte[blockSize]; - var attempt = 0; - while (!cancelToken.IsCancellationRequested) - { - var read = 0; - try - { - read = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, blockSize, cancelToken) - .ConfigureAwait(false); - } - catch - { - return; - } - if (read == 0) - if (attempt++ == 50) - break; - else - await Task.Delay(100, cancelToken).ConfigureAwait(false); - else - attempt = 0; - await songBuffer.WriteAsync(buffer, read, cancelToken).ConfigureAwait(false); - if (songBuffer.ContentLength > 2.MB()) - prebufferingComplete = true; - } + using (var outStream = new FileStream(filename, FileMode.Append, FileAccess.Write, FileShare.Read)) + await p.StandardOutput.BaseStream.CopyToAsync(outStream, 81920, cancelToken); + prebufferingComplete = true; } catch (Exception ex) { @@ -137,53 +115,56 @@ namespace NadekoBot.Modules.Music.Classes internal async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) { - // initialize the buffer here because if this song was playing before (requeued), we must delete old buffer data - songBuffer = new PoopyBuffer(NadekoBot.Config.BufferSize); + var filename = Path.Combine(MusicModule.MusicDataPath, DateTime.Now.UnixTimestamp().ToString()); - var bufferTask = BufferSong(cancelToken).ConfigureAwait(false); - var bufferAttempts = 0; - const int waitPerAttempt = 500; - var toAttemptTimes = SongInfo.ProviderType != MusicType.Normal ? 5 : 9; - while (!prebufferingComplete && bufferAttempts++ < toAttemptTimes) + var bufferTask = BufferSong(filename, cancelToken).ConfigureAwait(false); + + var inStream = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); + try { - await Task.Delay(waitPerAttempt, cancelToken).ConfigureAwait(false); - } - Console.WriteLine($"Prebuffering done? in {waitPerAttempt * bufferAttempts}"); - const int blockSize = 3840; - var attempt = 0; - while (!cancelToken.IsCancellationRequested) - { - //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); - byte[] buffer = new byte[blockSize]; - var read = await songBuffer.ReadAsync(buffer, blockSize).ConfigureAwait(false); - unchecked + await Task.Delay(1000); + + const int blockSize = 3840; + var attempt = 0; + while (!cancelToken.IsCancellationRequested) { - bytesSent += (ulong)read; - } - if (read == 0) - if (attempt++ == 20) + //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); + byte[] buffer = new byte[blockSize]; + var read = inStream.Read(buffer, 0, buffer.Length); + //await inStream.CopyToAsync(voiceClient.OutputStream); + unchecked { - voiceClient.Wait(); - Console.WriteLine($"Song finished. [{songBuffer.ContentLength}]"); - break; + bytesSent += (ulong)read; } + if (read == 0) + if (attempt++ == 20) + { + Console.WriteLine("blocking"); + voiceClient.Wait(); + Console.WriteLine("unblocking"); + break; + } + else + await Task.Delay(100, cancelToken).ConfigureAwait(false); else - await Task.Delay(100, cancelToken).ConfigureAwait(false); - else - attempt = 0; + attempt = 0; - while (this.MusicPlayer.Paused) - await Task.Delay(200, cancelToken).ConfigureAwait(false); - buffer = AdjustVolume(buffer, MusicPlayer.Volume); - voiceClient.Send(buffer, 0, read); + while (this.MusicPlayer.Paused) + await Task.Delay(200, cancelToken).ConfigureAwait(false); + buffer = AdjustVolume(buffer, MusicPlayer.Volume); + voiceClient.Send(buffer, 0, read); + } + await bufferTask; + voiceClient.Clear(); + cancelToken.ThrowIfCancellationRequested(); + } + finally { + inStream.Dispose(); + try { File.Delete(filename); } catch { } } - Console.WriteLine("Awiting buffer task"); - await bufferTask; - Console.WriteLine("Buffer task done."); - voiceClient.Clear(); - cancelToken.ThrowIfCancellationRequested(); } + /* //stackoverflow ftw private static byte[] AdjustVolume(byte[] audioSamples, float volume) { @@ -210,6 +191,33 @@ namespace NadekoBot.Modules.Music.Classes } return array; } + */ + + //aidiakapi ftw + public unsafe static byte[] AdjustVolume(byte[] audioSamples, float volume) + { + Contract.Requires(audioSamples != null); + Contract.Requires(audioSamples.Length % 2 == 0); + Contract.Requires(volume >= 0f && volume <= 1f); + Contract.Assert(BitConverter.IsLittleEndian); + + if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples; + + // 16-bit precision for the multiplication + int volumeFixed = (int)Math.Round(volume * 65536d); + + int count = audioSamples.Length / 2; + + fixed (byte* srcBytes = audioSamples) + { + short* src = (short*)srcBytes; + + for (int i = count; i != 0; i--, src++) + *src = (short)(((*src) * volumeFixed) >> 16); + } + + return audioSamples; + } public static async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) { diff --git a/NadekoBot/Modules/Music/MusicModule.cs b/NadekoBot/Modules/Music/MusicModule.cs index 1780dcf0..e9620d2c 100644 --- a/NadekoBot/Modules/Music/MusicModule.cs +++ b/NadekoBot/Modules/Music/MusicModule.cs @@ -18,11 +18,16 @@ namespace NadekoBot.Modules.Music { internal class MusicModule : DiscordModule { - public static ConcurrentDictionary MusicPlayers = new ConcurrentDictionary(); + public const string MusicDataPath = "data/musicdata"; + public MusicModule() { + //it can fail if its currenctly opened or doesn't exist. Either way i don't care + try { Directory.Delete(MusicDataPath, true); } catch { } + + Directory.CreateDirectory(MusicDataPath); } public override string Prefix { get; } = NadekoBot.Config.CommandPrefixes.Music; From 21b1778a1fc429f223a45eed77048d80fa7d451e Mon Sep 17 00:00:00 2001 From: Kwoth Date: Sun, 24 Jul 2016 01:28:23 +0200 Subject: [PATCH 2/5] unix timestamp extension --- NadekoBot/Classes/Extensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NadekoBot/Classes/Extensions.cs b/NadekoBot/Classes/Extensions.cs index a4071ded..c66b1b30 100644 --- a/NadekoBot/Classes/Extensions.cs +++ b/NadekoBot/Classes/Extensions.cs @@ -362,5 +362,7 @@ namespace NadekoBot.Extensions return sw.BaseStream; } + public static double UnixTimestamp(this DateTime dt) => dt.ToUniversalTime().Subtract(new DateTime(1970, 1, 1)).TotalSeconds; + } } From a01ed97c6de21e6a17285dfbf1e56428ae1494bf Mon Sep 17 00:00:00 2001 From: Kwoth Date: Sun, 24 Jul 2016 01:28:43 +0200 Subject: [PATCH 3/5] allow unsafe --- NadekoBot/NadekoBot.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NadekoBot/NadekoBot.csproj b/NadekoBot/NadekoBot.csproj index 4e16fee4..442296dc 100644 --- a/NadekoBot/NadekoBot.csproj +++ b/NadekoBot/NadekoBot.csproj @@ -46,6 +46,7 @@ true + true AnyCPU @@ -116,6 +117,7 @@ prompt MinimumRecommendedRules.ruleset true + true From 305a9a4a980ae4436b2249dd2a6edde44d411e61 Mon Sep 17 00:00:00 2001 From: Kwoth Date: Sun, 24 Jul 2016 13:00:34 +0200 Subject: [PATCH 4/5] better error when ffmpeg is not found --- NadekoBot/Modules/Music/Classes/Song.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/NadekoBot/Modules/Music/Classes/Song.cs b/NadekoBot/Modules/Music/Classes/Song.cs index 91c866e8..fc831b52 100644 --- a/NadekoBot/Modules/Music/Classes/Song.cs +++ b/NadekoBot/Modules/Music/Classes/Song.cs @@ -94,6 +94,16 @@ namespace NadekoBot.Modules.Music.Classes await p.StandardOutput.BaseStream.CopyToAsync(outStream, 81920, cancelToken); prebufferingComplete = true; } + catch (System.ComponentModel.Win32Exception) { + var oldclr = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(@"You have not properly installed or configured FFMPEG. +Please install and configure FFMPEG to play music. +Check the guides for your platform on how to setup ffmpeg correctly: + Windows Guide: https://goo.gl/SCv72y + Linux Guide: https://goo.gl/rRhjCp"); + Console.ForegroundColor = oldclr; + } catch (Exception ex) { Console.WriteLine($"Buffering errored: {ex.Message}"); From 922739be63d1b52963c3c7f5b1ce9a38c7aa3e0f Mon Sep 17 00:00:00 2001 From: Kwoth Date: Sun, 24 Jul 2016 18:00:11 +0200 Subject: [PATCH 5/5] Now not prebuffering more than 100 megs of PCM data --- NadekoBot/Classes/Extensions.cs | 9 +++ .../Modules/Music/Classes/MusicControls.cs | 21 +++--- NadekoBot/Modules/Music/Classes/Song.cs | 67 ++++++++++++++----- 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/NadekoBot/Classes/Extensions.cs b/NadekoBot/Classes/Extensions.cs index c66b1b30..36ba4dec 100644 --- a/NadekoBot/Classes/Extensions.cs +++ b/NadekoBot/Classes/Extensions.cs @@ -303,6 +303,15 @@ namespace NadekoBot.Extensions public static int GiB(this int value) => value.MiB() * 1024; public static int GB(this int value) => value.MB() * 1000; + public static ulong KiB(this ulong value) => value * 1024; + public static ulong KB(this ulong value) => value * 1000; + + public static ulong MiB(this ulong value) => value.KiB() * 1024; + public static ulong MB(this ulong value) => value.KB() * 1000; + + public static ulong GiB(this ulong value) => value.MiB() * 1024; + public static ulong GB(this ulong value) => value.MB() * 1000; + public static Stream ToStream(this Image img, System.Drawing.Imaging.ImageFormat format = null) { if (format == null) diff --git a/NadekoBot/Modules/Music/Classes/MusicControls.cs b/NadekoBot/Modules/Music/Classes/MusicControls.cs index b024ca47..847070eb 100644 --- a/NadekoBot/Modules/Music/Classes/MusicControls.cs +++ b/NadekoBot/Modules/Music/Classes/MusicControls.cs @@ -113,17 +113,10 @@ namespace NadekoBot.Modules.Music.Classes if (CurrentSong == null) continue; - try - { - OnStarted(this, CurrentSong); - await CurrentSong.Play(audioClient, cancelToken); - } - catch (OperationCanceledException) - { - Console.WriteLine("Song canceled"); - SongCancelSource = new CancellationTokenSource(); - cancelToken = SongCancelSource.Token; - } + + OnStarted(this, CurrentSong); + await CurrentSong.Play(audioClient, cancelToken); + OnCompleted(this, CurrentSong); if (RepeatPlaylist) @@ -135,6 +128,12 @@ namespace NadekoBot.Modules.Music.Classes } finally { + if (!cancelToken.IsCancellationRequested) + { + SongCancelSource.Cancel(); + } + SongCancelSource = new CancellationTokenSource(); + cancelToken = SongCancelSource.Token; CurrentSong = null; await Task.Delay(300).ConfigureAwait(false); } diff --git a/NadekoBot/Modules/Music/Classes/Song.cs b/NadekoBot/Modules/Music/Classes/Song.cs index fc831b52..08be13b8 100644 --- a/NadekoBot/Modules/Music/Classes/Song.cs +++ b/NadekoBot/Modules/Music/Classes/Song.cs @@ -32,9 +32,7 @@ namespace NadekoBot.Modules.Music.Classes public SongInfo SongInfo { get; } public string QueuerName { get; set; } - private PoopyBuffer songBuffer { get; set; } - - private bool prebufferingComplete { get; set; } = false; + private bool bufferingCompleted { get; set; } = false; public MusicPlayer MusicPlayer { get; set; } public string PrettyCurrentTime() @@ -90,9 +88,20 @@ namespace NadekoBot.Modules.Music.Classes RedirectStandardError = false, CreateNoWindow = true, }); + var prebufferSize = 100ul.MiB(); using (var outStream = new FileStream(filename, FileMode.Append, FileAccess.Write, FileShare.Read)) - await p.StandardOutput.BaseStream.CopyToAsync(outStream, 81920, cancelToken); - prebufferingComplete = true; + { + byte[] buffer = new byte[81920]; + int bytesRead; + while ((bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) != 0) + { + await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); + while ((ulong)outStream.Length - bytesSent > prebufferSize) + await Task.Delay(100, cancelToken); + } + } + + bufferingCompleted = true; } catch (System.ComponentModel.Win32Exception) { var oldclr = Console.ForegroundColor; @@ -106,11 +115,11 @@ Check the guides for your platform on how to setup ffmpeg correctly: } catch (Exception ex) { - Console.WriteLine($"Buffering errored: {ex.Message}"); + Console.WriteLine($"Buffering stopped: {ex.Message}"); } finally { - Console.WriteLine($"Buffering done." + $" [{songBuffer.ContentLength}]"); + Console.WriteLine($"Buffering done."); if (p != null) { try @@ -128,18 +137,36 @@ Check the guides for your platform on how to setup ffmpeg correctly: var filename = Path.Combine(MusicModule.MusicDataPath, DateTime.Now.UnixTimestamp().ToString()); var bufferTask = BufferSong(filename, cancelToken).ConfigureAwait(false); - + var inStream = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write); + + bytesSent = 0; + try { - await Task.Delay(1000); + var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken); + var sw = new Stopwatch(); + sw.Start(); + var t = await Task.WhenAny(prebufferingTask, Task.Delay(5000, cancelToken)); + if (t != prebufferingTask) + { + Console.WriteLine("Prebuffering timed out or canceled. Cannot get any data from the stream."); + return; + } + else if(prebufferingTask.IsCanceled) + { + Console.WriteLine("Prebuffering timed out. Cannot get any data from the stream."); + return; + } + sw.Stop(); + Console.WriteLine("Prebuffering successfully completed in "+ sw.Elapsed); const int blockSize = 3840; var attempt = 0; + byte[] buffer = new byte[blockSize]; while (!cancelToken.IsCancellationRequested) { //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); - byte[] buffer = new byte[blockSize]; var read = inStream.Read(buffer, 0, buffer.Length); //await inStream.CopyToAsync(voiceClient.OutputStream); unchecked @@ -149,9 +176,7 @@ Check the guides for your platform on how to setup ffmpeg correctly: if (read == 0) if (attempt++ == 20) { - Console.WriteLine("blocking"); voiceClient.Wait(); - Console.WriteLine("unblocking"); break; } else @@ -161,19 +186,29 @@ Check the guides for your platform on how to setup ffmpeg correctly: while (this.MusicPlayer.Paused) await Task.Delay(200, cancelToken).ConfigureAwait(false); + buffer = AdjustVolume(buffer, MusicPlayer.Volume); voiceClient.Send(buffer, 0, read); } - await bufferTask; - voiceClient.Clear(); - cancelToken.ThrowIfCancellationRequested(); } - finally { + finally + { + await bufferTask; + await Task.Run(() => voiceClient.Clear()); inStream.Dispose(); try { File.Delete(filename); } catch { } } } + private async Task CheckPrebufferingAsync(Stream inStream, CancellationToken cancelToken) + { + while (!bufferingCompleted && inStream.Length < 2.MiB()) + { + await Task.Delay(100, cancelToken); + } + Console.WriteLine("Buffering successfull"); + } + /* //stackoverflow ftw private static byte[] AdjustVolume(byte[] audioSamples, float volume)