diff --git a/NadekoBot/Classes/Music/MusicControls.cs b/NadekoBot/Classes/Music/MusicControls.cs index 6e997619..fe0bffdb 100644 --- a/NadekoBot/Classes/Music/MusicControls.cs +++ b/NadekoBot/Classes/Music/MusicControls.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; - namespace NadekoBot.Classes.Music { public enum MusicType { @@ -29,7 +28,7 @@ namespace NadekoBot.Classes.Music { private List _playlist = new List(); public IReadOnlyCollection Playlist => _playlist; - private object playlistLock = new object(); + private readonly object playlistLock = new object(); public Song CurrentSong { get; set; } = default(Song); private CancellationTokenSource SongCancelSource { get; set; } @@ -44,26 +43,31 @@ namespace NadekoBot.Classes.Music { public Channel PlaybackVoiceChannel { get; private set; } - public MusicPlayer(Channel startingVoiceChannel, float defaultVolume) { + public MusicPlayer(Channel startingVoiceChannel, float? defaultVolume) { if (startingVoiceChannel == null) throw new ArgumentNullException(nameof(startingVoiceChannel)); if (startingVoiceChannel.Type != ChannelType.Voice) throw new ArgumentException("Channel must be of type voice"); + Volume = defaultVolume ?? 1.0f; PlaybackVoiceChannel = startingVoiceChannel; SongCancelSource = new CancellationTokenSource(); cancelToken = SongCancelSource.Token; Task.Run(async () => { - while (_client?.State != ConnectionState.Disconnected && - _client?.State != ConnectionState.Disconnecting) { - + while (true) { + try { + _client = await PlaybackVoiceChannel.JoinAudio(); + } + catch { + await Task.Delay(1000); + continue; + } CurrentSong = GetNextSong(); if (CurrentSong != null) { try { - _client = await PlaybackVoiceChannel.JoinAudio(); OnStarted(CurrentSong); - CurrentSong.Play(_client, cancelToken); + await CurrentSong.Play(_client, cancelToken); } catch (OperationCanceledException) { Console.WriteLine("Song canceled"); @@ -71,7 +75,10 @@ namespace NadekoBot.Classes.Music { catch (Exception ex) { Console.WriteLine($"Exception in PlaySong: {ex}"); } - OnCompleted(CurrentSong); + try { + OnCompleted(CurrentSong); + } + catch { } SongCancelSource = new CancellationTokenSource(); cancelToken = SongCancelSource.Token; } @@ -81,30 +88,31 @@ namespace NadekoBot.Classes.Music { } public void Next() { - if (!SongCancelSource.IsCancellationRequested) - SongCancelSource.Cancel(); + lock (playlistLock) { + if (!SongCancelSource.IsCancellationRequested) { + Paused = false; + SongCancelSource.Cancel(); + } + } } - public async Task Stop() { - - lock (_playlist) { + public void Stop() { + lock (playlistLock) { _playlist.Clear(); + try { + if (!SongCancelSource.IsCancellationRequested) + SongCancelSource.Cancel(); + } + catch { + Console.WriteLine("STOP"); + } } - try { - if (!SongCancelSource.IsCancellationRequested) - SongCancelSource.Cancel(); - } - catch { - Console.WriteLine("This shouldn't happen"); - } - Console.WriteLine("Disconnecting"); - await _client?.Disconnect(); } public void TogglePause() => Paused = !Paused; public void Shuffle() { - lock (_playlist) { + lock (playlistLock) { _playlist.Shuffle(); } } @@ -115,7 +123,8 @@ namespace NadekoBot.Classes.Music { if (volume > 150) volume = 150; - return (int)(Volume = volume / 100.0f); + Volume = volume / 100.0f; + return volume; } private Song GetNextSong() { diff --git a/NadekoBot/Classes/Music/Song.cs b/NadekoBot/Classes/Music/Song.cs index 72e99d91..c71d0778 100644 --- a/NadekoBot/Classes/Music/Song.cs +++ b/NadekoBot/Classes/Music/Song.cs @@ -10,42 +10,157 @@ using System.Threading.Tasks; using VideoLibrary; namespace NadekoBot.Classes.Music { - - public class SongInfo { public string Provider { get; internal set; } + public MusicType ProviderType { get; internal set; } public string Title { get; internal set; } public string Uri { get; internal set; } } + /// + /// 💩 + /// + public class PoopyBuffer { + private byte[] ringBuffer; + + public int WritePosition { get; private set; } = 0; + + public PoopyBuffer(int size) { + ringBuffer = new byte[size]; + } + + public int Read(byte[] buffer, int count) { + lock (this) { + if (count > WritePosition) + count = WritePosition; + if (count == 0) + return 0; + + Buffer.BlockCopy(ringBuffer, 0, buffer, 0, count); + Buffer.BlockCopy(ringBuffer, count, ringBuffer, 0, WritePosition -= count); + + return count; + } + } + + public async Task WriteAsync(byte[] buffer, int count, CancellationToken cancelToken) { + if (count > buffer.Length) + throw new ArgumentException(); + while (count + WritePosition > ringBuffer.Length) { + await Task.Delay(20); + if (cancelToken.IsCancellationRequested) + return; + } + lock (this) { + Buffer.BlockCopy(buffer, 0, ringBuffer, WritePosition, count); + WritePosition += count; + } + } + } public class Song { public StreamState State { get; internal set; } - public object PrettyName => + public string PrettyName => $"**【 {SongInfo.Title.TrimTo(55)} 】**`{(SongInfo.Provider ?? "-")}`"; public SongInfo SongInfo { get; } + + private PoopyBuffer songBuffer { get; } = new PoopyBuffer(10.MB()); + + private bool prebufferingComplete { get; set; } = false; + public MusicPlayer MusicPlayer { get; set; } private Song(SongInfo songInfo) { this.SongInfo = songInfo; } - internal void Play(IAudioClient voiceClient, CancellationToken cancelToken) { - var p = Process.Start(new ProcessStartInfo { - FileName = "ffmpeg", - Arguments = $"-i {SongInfo.Uri} -f s16le -ar 48000 -ac 2 pipe:1 -loglevel quiet", - UseShellExecute = false, - RedirectStandardOutput = true, + private Task BufferSong(CancellationToken cancelToken) => + Task.Run(async () => { + var p = Process.Start(new ProcessStartInfo { + FileName = "ffmpeg", + Arguments = $"-i {SongInfo.Uri} -f s16le -ar 48000 -ac 2 pipe:1 -loglevel quiet", + UseShellExecute = false, + RedirectStandardOutput = true, + }); + + int blockSize = 512; + byte[] buffer = new byte[blockSize]; + int attempt = 0; + while (!cancelToken.IsCancellationRequested) { + int read = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, blockSize); + if (read == 0) + if (attempt++ == 10) + break; + else + await Task.Delay(50); + else + attempt = 0; + await songBuffer.WriteAsync(buffer, read, cancelToken); + if (songBuffer.WritePosition > 5.MB()) + prebufferingComplete = true; + } + Console.WriteLine("Buffering done."); }); - Task.Delay(2000); //give it 2 seconds to get some dataz - int blockSize = 3840; // 1920 for mono + + internal async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) { + var t = BufferSong(cancelToken).ConfigureAwait(false); + int bufferAttempts = 0; + int waitPerAttempt = 500; + int toAttemptTimes = SongInfo.ProviderType != MusicType.Normal ? 4 : 8; + while (!prebufferingComplete && bufferAttempts++ < toAttemptTimes) { + await Task.Delay(waitPerAttempt); + } + int blockSize = 3840; byte[] buffer = new byte[blockSize]; - int read; + int attempt = 0; while (!cancelToken.IsCancellationRequested) { - read = p.StandardOutput.BaseStream.Read(buffer, 0, blockSize); + int read = songBuffer.Read(buffer, blockSize); if (read == 0) - break; //nothing to read + if (attempt++ == 10) { + voiceClient.Wait(); + Console.WriteLine("Playing done."); + return; + } + else + await Task.Delay(50); + else + attempt = 0; + + while (this.MusicPlayer.Paused) + await Task.Delay(200); + buffer = adjustVolume(buffer, MusicPlayer.Volume); voiceClient.Send(buffer, 0, read); } - voiceClient.Wait(); + //try { + // voiceClient.Clear(); + // Console.WriteLine("CLEARED"); + //} + //catch { + // Console.WriteLine("CLEAR FAILED!!!"); + //} + } + + //stackoverflow ftw + private byte[] adjustVolume(byte[] audioSamples, float volume) { + if (volume == 1.0f) + return audioSamples; + byte[] array = new byte[audioSamples.Length]; + for (int i = 0; i < array.Length; i += 2) { + + // convert byte pair to int + short buf1 = audioSamples[i + 1]; + short buf2 = audioSamples[i]; + + buf1 = (short)((buf1 & 0xff) << 8); + buf2 = (short)(buf2 & 0xff); + + short res = (short)(buf1 | buf2); + res = (short)(res * volume); + + // convert back + array[i] = (byte)res; + array[i + 1] = (byte)(res >> 8); + + } + return array; } public static async Task ResolveSong(string query, MusicType musicType = MusicType.Normal) { @@ -63,6 +178,7 @@ namespace NadekoBot.Classes.Music { Uri = "\"" + Path.GetFullPath(query) + "\"", Title = Path.GetFileNameWithoutExtension(query), Provider = "Local File", + ProviderType = musicType, }); } else if (musicType == MusicType.Radio) { @@ -70,6 +186,7 @@ namespace NadekoBot.Classes.Music { Uri = query, Title = $"{query}", Provider = "Radio Stream", + ProviderType = musicType, }); } else if (SoundCloud.Default.IsSoundCloudLink(query)) { @@ -78,6 +195,7 @@ namespace NadekoBot.Classes.Music { Title = svideo.FullName, Provider = "SoundCloud", Uri = svideo.StreamLink, + ProviderType = musicType, }); } else { @@ -97,6 +215,7 @@ namespace NadekoBot.Classes.Music { Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" Provider = "YouTube", Uri = video.Uri, + ProviderType = musicType, }); } diff --git a/NadekoBot/Classes/Music/StreamRequest.cs b/NadekoBot/Classes/Music/StreamRequest.cs index 3481772e..8aa8e664 100644 --- a/NadekoBot/Classes/Music/StreamRequest.cs +++ b/NadekoBot/Classes/Music/StreamRequest.cs @@ -293,30 +293,6 @@ namespace NadekoBot.Classes.Music { if (oldState == StreamState.Playing) OnCompleted(); } - //stackoverflow ftw - private byte[] adjustVolume(byte[] audioSamples, float volume) { - if (volume == 1.0f) - return audioSamples; - byte[] array = new byte[audioSamples.Length]; - for (int i = 0; i < array.Length; i += 2) { - - // convert byte pair to int - short buf1 = audioSamples[i + 1]; - short buf2 = audioSamples[i]; - - buf1 = (short)((buf1 & 0xff) << 8); - buf2 = (short)(buf2 & 0xff); - - short res = (short)(buf1 | buf2); - res = (short)(res * volume); - - // convert back - array[i] = (byte)res; - array[i + 1] = (byte)(res >> 8); - - } - return array; - } } public class DualStream : MemoryStream { diff --git a/NadekoBot/Modules/Music.cs b/NadekoBot/Modules/Music.cs index 266e7fc7..43324097 100644 --- a/NadekoBot/Modules/Music.cs +++ b/NadekoBot/Modules/Music.cs @@ -58,8 +58,14 @@ namespace NadekoBot.Modules { .Description("Completely stops the music, unbinds the bot from the channel, and cleans up files.") .Do(async e => { MusicPlayer musicPlayer; - if (!musicPlayers.TryRemove(e.Server, out musicPlayer)) return; - await musicPlayer.Stop(); + if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) return; + musicPlayer.Stop(); + var msg = await e.Channel.SendMessage("⚠Due to music issues, NadekoBot is unable to leave voice channels at this moment.\nIf this presents inconvenience, you can use `!m mv` command to make her join your current voice channel."); + await Task.Delay(5000); + try { + await msg.Delete(); + } + catch { } }); cgb.CreateCommand("p") @@ -95,6 +101,8 @@ namespace NadekoBot.Modules { string toSend = "🎵 **" + musicPlayer.Playlist.Count + "** `videos currently queued.` "; if (musicPlayer.Playlist.Count >= MusicPlayer.MaximumPlaylistSize) toSend += "**Song queue is full!**\n"; + else + toSend += "\n"; int number = 1; await e.Channel.SendMessage(toSend + string.Join("\n", musicPlayer.Playlist.Take(15).Select(v => $"`{number++}.` {v.PrettyName}"))); }); @@ -313,7 +321,7 @@ namespace NadekoBot.Modules { float throwAway; if (defaultMusicVolumes.TryGetValue(TextCh.Server.Id, out throwAway)) vol = throwAway; - musicPlayer = new MusicPlayer(VoiceCh) { + musicPlayer = new MusicPlayer(VoiceCh, vol) { OnCompleted = async (song) => { try { await TextCh.SendMessage($"🎵`Finished`{song.PrettyName}"); @@ -331,7 +339,7 @@ namespace NadekoBot.Modules { musicPlayers.TryAdd(TextCh.Server, musicPlayer); } var resolvedSong = await Song.ResolveSong(query, musicType); - + resolvedSong.MusicPlayer = musicPlayer; if(!silent) await TextCh.Send($"🎵`Queued`{resolvedSong.PrettyName}"); musicPlayer.AddSong(resolvedSong); diff --git a/NadekoBot/NadekoBot.cs b/NadekoBot/NadekoBot.cs index 4089767d..25411069 100644 --- a/NadekoBot/NadekoBot.cs +++ b/NadekoBot/NadekoBot.cs @@ -66,6 +66,13 @@ namespace NadekoBot { //create new discord client client = new DiscordClient(new DiscordConfigBuilder() { MessageCacheSize = 20, + LogLevel = LogSeverity.Warning, + LogHandler = (s, e) => { + try { + Console.WriteLine($"Severity: {e.Severity}\nMessage: {e.Message}\nExceptionMessage: {e.Exception?.Message ?? "-"}\nException: {(e.Exception?.ToString() ?? "-")}"); + } + catch { } + } }); //create a command service @@ -139,17 +146,18 @@ namespace NadekoBot { } Classes.Permissions.PermissionsHandler.Initialize(); - + client.ClientAPI.SendingRequest += (s, e) => { + try { var request = e.Request as Discord.API.Client.Rest.SendMessageRequest; if (request != null) { - request.Content = request.Content?.Replace("@everyone", "@everyοne") ?? "_error_"; + //@everyοne + request.Content = request.Content?.Replace("@everyone", "@everryone") ?? "_error_"; if (string.IsNullOrWhiteSpace(request.Content)) e.Cancel = true; - else - Console.WriteLine("Sending request."); - var content = request.Content; + //else + // Console.WriteLine("Sending request"); } } catch { @@ -157,15 +165,15 @@ namespace NadekoBot { } }; - client.ClientAPI.SentRequest += (s, e) => { - try { - var request = e.Request as Discord.API.Client.Rest.SendMessageRequest; - if (request != null) { - Console.WriteLine("Sent."); - } - } - catch { Console.WriteLine("SENT REQUEST ERRORED!!!"); } - }; + //client.ClientAPI.SentRequest += (s, e) => { + // try { + // var request = e.Request as Discord.API.Client.Rest.SendMessageRequest; + // if (request != null) { + // Console.WriteLine("Sent."); + // } + // } + // catch { Console.WriteLine("SENT REQUEST ERRORED!!!"); } + //}; }); Console.WriteLine("Exiting..."); Console.ReadKey(); diff --git a/NadekoBot/bin/Debug/Discord.Net.Audio.dll b/NadekoBot/bin/Debug/Discord.Net.Audio.dll index e888650c..d78758e6 100644 Binary files a/NadekoBot/bin/Debug/Discord.Net.Audio.dll and b/NadekoBot/bin/Debug/Discord.Net.Audio.dll differ diff --git a/NadekoBot/bin/Debug/Discord.Net.Commands.dll b/NadekoBot/bin/Debug/Discord.Net.Commands.dll index 6c7059c5..4096fa6c 100644 Binary files a/NadekoBot/bin/Debug/Discord.Net.Commands.dll and b/NadekoBot/bin/Debug/Discord.Net.Commands.dll differ diff --git a/NadekoBot/bin/Debug/Discord.Net.Modules.dll b/NadekoBot/bin/Debug/Discord.Net.Modules.dll index 4f746178..9d8214f2 100644 Binary files a/NadekoBot/bin/Debug/Discord.Net.Modules.dll and b/NadekoBot/bin/Debug/Discord.Net.Modules.dll differ diff --git a/NadekoBot/bin/Debug/Discord.Net.dll b/NadekoBot/bin/Debug/Discord.Net.dll index 1476fed7..bb4e2311 100644 Binary files a/NadekoBot/bin/Debug/Discord.Net.dll and b/NadekoBot/bin/Debug/Discord.Net.dll differ