using Discord; using Discord.Audio; using System; using System.Threading; using System.Threading.Tasks; using NLog; using System.Linq; using NadekoBot.Extensions; using System.Diagnostics; using NadekoBot.Common.Collections; using NadekoBot.Modules.Music.Services; using NadekoBot.Core.Services; using NadekoBot.Core.Services.Database.Models; using Discord.WebSocket; namespace NadekoBot.Modules.Music.Common { public enum StreamState { Resolving, Queued, Playing, Completed } public class MusicPlayer { private readonly Thread _player; public IVoiceChannel VoiceChannel { get; private set; } public ITextChannel OriginalTextChannel { get; set; } private readonly Logger _log; private MusicQueue Queue { get; } = new MusicQueue(); public bool Exited { get; set; } = false; public bool Stopped { get; private set; } = false; public float Volume { get; private set; } = 1.0f; public bool Paused => pauseTaskSource != null; private TaskCompletionSource pauseTaskSource { get; set; } = null; public string PrettyVolume => $"🔉 {(int)(Volume * 100)}%"; public string PrettyCurrentTime { get { var time = CurrentTime.ToString(@"mm\:ss"); var hrs = (int)CurrentTime.TotalHours; if (hrs > 0) return hrs + ":" + time; else return time; } } public string PrettyFullTime => PrettyCurrentTime + " / " + (Queue.Current.Song?.PrettyTotalTime ?? "?"); private CancellationTokenSource SongCancelSource { get; set; } public ITextChannel OutputTextChannel { get; set; } public (int Index, SongInfo Current) Current { get { if (Stopped) return (0, null); return Queue.Current; } } public bool RepeatCurrentSong { get; private set; } public bool Shuffle { get; private set; } public bool Autoplay { get; private set; } public bool RepeatPlaylist { get; private set; } = false; public uint MaxQueueSize { get => Queue.MaxQueueSize; set { lock (locker) Queue.MaxQueueSize = value; } } private bool _fairPlay; public bool FairPlay { get => _fairPlay; set { if (value) { var cur = Queue.Current; if (cur.Song != null) RecentlyPlayedUsers.Add(cur.Song.QueuerName); } else { RecentlyPlayedUsers.Clear(); } _fairPlay = value; } } public bool AutoDelete { get; set; } public uint MaxPlaytimeSeconds { get; set; } const int _frameBytes = 3840; const float _miliseconds = 20.0f; public TimeSpan CurrentTime => TimeSpan.FromSeconds(_bytesSent / (float)_frameBytes / (1000 / _miliseconds)); private int _bytesSent = 0; private IAudioClient _audioClient; private readonly object locker = new object(); private MusicService _musicService; #region events public event Action OnStarted; public event Action OnCompleted; public event Action OnPauseChanged; #endregion private bool manualSkip = false; private bool manualIndex = false; private bool newVoiceChannel = false; private readonly IGoogleApiService _google; private bool cancel = false; private ConcurrentHashSet RecentlyPlayedUsers { get; } = new ConcurrentHashSet(); public TimeSpan TotalPlaytime { get { var songs = Queue.ToArray().Songs; return songs.Any(s => s.TotalTime == TimeSpan.MaxValue) ? TimeSpan.MaxValue : new TimeSpan(songs.Sum(s => s.TotalTime.Ticks)); } } public MusicPlayer(MusicService musicService, MusicSettings ms, IGoogleApiService google, IVoiceChannel vch, ITextChannel original, float volume) { _log = LogManager.GetCurrentClassLogger(); this.Volume = volume; this.VoiceChannel = vch; this.OriginalTextChannel = original; this.SongCancelSource = new CancellationTokenSource(); if(ms.MusicChannelId is ulong cid) { this.OutputTextChannel = ((SocketGuild)original.Guild).GetTextChannel(cid) ?? original; } else { this.OutputTextChannel = original; } this._musicService = musicService; this._google = google; _player = new Thread(new ThreadStart(PlayerLoop)); _player.Start(); } private async void PlayerLoop() { while (!Exited) { _bytesSent = 0; cancel = false; CancellationToken cancelToken; (int Index, SongInfo Song) data; lock (locker) { data = Queue.Current; cancelToken = SongCancelSource.Token; manualSkip = false; manualIndex = false; } if (data.Song != null) { _log.Info("Starting"); AudioOutStream pcm = null; SongBuffer b = null; try { b = new SongBuffer(await data.Song.Uri(), "", data.Song.ProviderType == MusicType.Local); //_log.Info("Created buffer, buffering..."); //var bufferTask = b.StartBuffering(cancelToken); //var timeout = Task.Delay(10000); //if (Task.WhenAny(bufferTask, timeout) == timeout) //{ // _log.Info("Buffering failed due to a timeout."); // continue; //} //else if (!bufferTask.Result) //{ // _log.Info("Buffering failed due to a cancel or error."); // continue; //} //_log.Info("Buffered. Getting audio client..."); var ac = await GetAudioClient(); _log.Info("Got Audio client"); if (ac == null) { _log.Info("Can't join"); await Task.Delay(900, cancelToken); // just wait some time, maybe bot doesn't even have perms to join that voice channel, // i don't want to spam connection attempts continue; } pcm = ac.CreatePCMStream(AudioApplication.Music, bufferMillis: 500); _log.Info("Created pcm stream"); OnStarted?.Invoke(this, data); byte[] buffer = new byte[3840]; int bytesRead = 0; while ((bytesRead = b.Read(buffer, 0, buffer.Length)) > 0 && (MaxPlaytimeSeconds <= 0 || MaxPlaytimeSeconds >= CurrentTime.TotalSeconds)) { AdjustVolume(buffer, Volume); await pcm.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false); unchecked { _bytesSent += bytesRead; } await (pauseTaskSource?.Task ?? Task.CompletedTask); } } catch (OperationCanceledException) { _log.Info("Song Canceled"); cancel = true; } catch (Exception ex) { _log.Warn(ex); } finally { if (pcm != null) { // flush is known to get stuck from time to time, // just skip flushing if it takes more than 1 second var flushCancel = new CancellationTokenSource(); var flushToken = flushCancel.Token; var flushDelay = Task.Delay(1000, flushToken); await Task.WhenAny(flushDelay, pcm.FlushAsync(flushToken)); flushCancel.Cancel(); pcm.Dispose(); } if (b != null) b.Dispose(); OnCompleted?.Invoke(this, data.Song); if (_bytesSent == 0 && !cancel) { lock (locker) Queue.RemoveSong(data.Song); _log.Info("Song removed because it can't play"); } } try { //if repeating current song, just ignore other settings, // and play this song again (don't change the index) // ignore rcs if song is manually skipped int queueCount; bool stopped; int currentIndex; lock (locker) { queueCount = Queue.Count; stopped = Stopped; currentIndex = Queue.CurrentIndex; } if (AutoDelete && !RepeatCurrentSong && !RepeatPlaylist && data.Song != null) { Queue.RemoveSong(data.Song); } if (!manualIndex && (!RepeatCurrentSong || manualSkip)) { if (Shuffle) { _log.Info("Random song"); Queue.Random(); //if shuffle is set, set current song index to a random number } else { //if last song, and autoplay is enabled, and if it's a youtube song // do autplay magix if (queueCount - 1 == data.Index && Autoplay && data.Song?.ProviderType == MusicType.YouTube) { try { _log.Info("Loading related song"); await _musicService.TryQueueRelatedSongAsync(data.Song, OutputTextChannel, VoiceChannel); if(!AutoDelete) Queue.Next(); } catch { _log.Info("Loading related song failed."); } } else if (FairPlay) { lock (locker) { _log.Info("Next fair song"); var q = Queue.ToArray().Songs.Shuffle().ToArray(); bool found = false; for (var i = 0; i < q.Length; i++) //first try to find a queuer who didn't have their song played recently { var item = q[i]; if (RecentlyPlayedUsers.Add(item.QueuerName)) // if it's found, set current song to that index { Queue.CurrentIndex = i; found = true; break; } } if (!found) //if it's not { RecentlyPlayedUsers.Clear(); //clear all recently played users (that means everyone from the playlist has had their song played) Queue.Random(); //go to a random song (to prevent looping on the first few songs) var cur = Current; if (cur.Current != null) // add newely scheduled song's queuer to the recently played list RecentlyPlayedUsers.Add(cur.Current.QueuerName); } } } else if (queueCount - 1 == data.Index && !RepeatPlaylist && !manualSkip) { _log.Info("Stopping because repeatplaylist is disabled"); lock (locker) { Stop(); } } else { _log.Info("Next song"); lock (locker) { if (!Stopped) if(!AutoDelete) Queue.Next(); } } } } } catch (Exception ex) { _log.Error(ex); } } do { await Task.Delay(500); } while ((Queue.Count == 0 || Stopped) && !Exited); } } private async Task GetAudioClient(bool reconnect = false) { if (_audioClient == null || _audioClient.ConnectionState != ConnectionState.Connected || reconnect || newVoiceChannel) try { try { var t = _audioClient?.StopAsync(); if (t != null) { _log.Info("Stopping audio client"); await t; _log.Info("Disposing audio client"); _audioClient.Dispose(); } } catch { } newVoiceChannel = false; var curUser = await VoiceChannel.Guild.GetCurrentUserAsync(); if (curUser.VoiceChannel != null) { _log.Info("Connecting"); var ac = await VoiceChannel.ConnectAsync(); _log.Info("Connected, stopping"); await ac.StopAsync(); _log.Info("Disconnected"); await Task.Delay(1000); } _log.Info("Connecting"); _audioClient = await VoiceChannel.ConnectAsync(); } catch { return null; } return _audioClient; } public int Enqueue(SongInfo song) { lock (locker) { if (Exited) return -1; Queue.Add(song); return Queue.Count - 1; } } public int EnqueueNext(SongInfo song) { lock (locker) { if (Exited) return -1; return Queue.AddNext(song); } } public void SetIndex(int index) { if (index < 0) throw new ArgumentOutOfRangeException(nameof(index)); lock (locker) { if (Exited) return; if (AutoDelete && index >= Queue.CurrentIndex && index > 0) index--; Queue.CurrentIndex = index; manualIndex = true; Stopped = false; CancelCurrentSong(); } } public void Next(int skipCount = 1) { lock (locker) { if (Exited) return; manualSkip = true; // if player is stopped, and user uses .n, it should play current song. // It's a bit weird, but that's the least annoying solution if (!Stopped) if (!RepeatPlaylist && Queue.IsLast()) // if it's the last song in the queue, and repeat playlist is disabled { //stop the queue Stop(); return; } else Queue.Next(skipCount - 1); else Queue.CurrentIndex = 0; Stopped = false; CancelCurrentSong(); Unpause(); } } public void Stop(bool clearQueue = false) { lock (locker) { Stopped = true; //Queue.ResetCurrent(); if (clearQueue) Queue.Clear(); Unpause(); CancelCurrentSong(); } } private void Unpause() { lock (locker) { if (pauseTaskSource != null) { pauseTaskSource.TrySetResult(true); pauseTaskSource = null; } } } public void TogglePause() { lock (locker) { if (pauseTaskSource == null) pauseTaskSource = new TaskCompletionSource(); else { Unpause(); } } OnPauseChanged?.Invoke(this, pauseTaskSource != null); } public void SetVolume(int volume) { if (volume < 0 || volume > 100) throw new ArgumentOutOfRangeException(nameof(volume)); lock (locker) { Volume = ((float)volume) / 100; } } public SongInfo RemoveAt(int index) { lock (locker) { var cur = Queue.Current; var toReturn = Queue.RemoveAt(index); if (cur.Index == index) Next(); return toReturn; } } private void CancelCurrentSong() { lock (locker) { var cs = SongCancelSource; SongCancelSource = new CancellationTokenSource(); cs.Cancel(); } } public void ClearQueue() { lock (locker) { Queue.Clear(); } } public (int CurrentIndex, SongInfo[] Songs) QueueArray() { lock (locker) return Queue.ToArray(); } //aidiakapi ftw public static unsafe byte[] AdjustVolume(byte[] audioSamples, float volume) { if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples; // 16-bit precision for the multiplication var volumeFixed = (int)Math.Round(volume * 65536d); var count = audioSamples.Length / 2; fixed (byte* srcBytes = audioSamples) { var src = (short*)srcBytes; for (var i = count; i != 0; i--, src++) *src = (short)(((*src) * volumeFixed) >> 16); } return audioSamples; } public bool ToggleRepeatSong() { lock (locker) { return RepeatCurrentSong = !RepeatCurrentSong; } } public async Task Destroy() { _log.Info("Destroying"); lock (locker) { Stop(); Exited = true; Unpause(); OnCompleted = null; OnPauseChanged = null; OnStarted = null; } var ac = _audioClient; if (ac != null) await ac.StopAsync(); } public bool ToggleShuffle() { lock (locker) { return Shuffle = !Shuffle; } } public bool ToggleAutoplay() { lock (locker) { return Autoplay = !Autoplay; } } public bool ToggleRepeatPlaylist() { lock (locker) { return RepeatPlaylist = !RepeatPlaylist; } } public async Task SetVoiceChannel(IVoiceChannel vch) { lock (locker) { if (Exited) return; VoiceChannel = vch; } _audioClient = await vch.ConnectAsync(); } public async Task UpdateSongDurationsAsync() { var sw = Stopwatch.StartNew(); var (_, songs) = Queue.ToArray(); var toUpdate = songs .Where(x => x.ProviderType == MusicType.YouTube && x.TotalTime == TimeSpan.Zero); var vIds = toUpdate.Select(x => x.VideoId); sw.Stop(); _log.Info(sw.Elapsed.TotalSeconds); if (!vIds.Any()) return; var durations = await _google.GetVideoDurationsAsync(vIds); foreach (var x in toUpdate) { if (durations.TryGetValue(x.VideoId, out var dur)) x.TotalTime = dur; } } public SongInfo MoveSong(int n1, int n2) => Queue.MoveSong(n1, n2); public void SetMusicChannelToOriginal() { this.OutputTextChannel = OriginalTextChannel; } //// this should be written better //public TimeSpan TotalPlaytime => // _playlist.Any(s => s.TotalTime == TimeSpan.MaxValue) ? // TimeSpan.MaxValue : // new TimeSpan(_playlist.Sum(s => s.TotalTime.Ticks)); } }