music rewritten
This commit is contained in:
parent
29a0dbdfce
commit
99656095b7
@ -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}");
|
||||
}
|
||||
try {
|
||||
OnCompleted(CurrentSong);
|
||||
}
|
||||
catch { }
|
||||
SongCancelSource = new CancellationTokenSource();
|
||||
cancelToken = SongCancelSource.Token;
|
||||
}
|
||||
@ -81,30 +88,31 @@ namespace NadekoBot.Classes.Music {
|
||||
}
|
||||
|
||||
public void Next() {
|
||||
if (!SongCancelSource.IsCancellationRequested)
|
||||
lock (playlistLock) {
|
||||
if (!SongCancelSource.IsCancellationRequested) {
|
||||
Paused = false;
|
||||
SongCancelSource.Cancel();
|
||||
}
|
||||
|
||||
public async Task Stop() {
|
||||
|
||||
lock (_playlist) {
|
||||
_playlist.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop() {
|
||||
lock (playlistLock) {
|
||||
_playlist.Clear();
|
||||
try {
|
||||
if (!SongCancelSource.IsCancellationRequested)
|
||||
SongCancelSource.Cancel();
|
||||
}
|
||||
catch {
|
||||
Console.WriteLine("This shouldn't happen");
|
||||
Console.WriteLine("STOP");
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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,
|
||||
});
|
||||
Task.Delay(2000); //give it 2 seconds to get some dataz
|
||||
int blockSize = 3840; // 1920 for mono
|
||||
|
||||
int blockSize = 512;
|
||||
byte[] buffer = new byte[blockSize];
|
||||
int read;
|
||||
int attempt = 0;
|
||||
while (!cancelToken.IsCancellationRequested) {
|
||||
read = p.StandardOutput.BaseStream.Read(buffer, 0, blockSize);
|
||||
int read = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, blockSize);
|
||||
if (read == 0)
|
||||
break; //nothing to read
|
||||
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.");
|
||||
});
|
||||
|
||||
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 attempt = 0;
|
||||
while (!cancelToken.IsCancellationRequested) {
|
||||
int read = songBuffer.Read(buffer, blockSize);
|
||||
if (read == 0)
|
||||
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
|
||||
@ -141,15 +148,16 @@ 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.
Loading…
Reference in New Issue
Block a user