music rewritten
This commit is contained in:
		| @@ -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<Song> _playlist = new List<Song>(); | ||||
|         public IReadOnlyCollection<Song> 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() { | ||||
|   | ||||
| @@ -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; } | ||||
|     } | ||||
|     /// <summary> | ||||
|     /// 💩 | ||||
|     /// </summary> | ||||
|     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<Song> 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, | ||||
|                     }); | ||||
|  | ||||
|                 } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user