music rewritten

This commit is contained in:
Master Kwoth 2016-02-28 04:30:16 +01:00
parent 29a0dbdfce
commit 99656095b7
9 changed files with 202 additions and 82 deletions

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace NadekoBot.Classes.Music { namespace NadekoBot.Classes.Music {
public enum MusicType { public enum MusicType {
@ -29,7 +28,7 @@ namespace NadekoBot.Classes.Music {
private List<Song> _playlist = new List<Song>(); private List<Song> _playlist = new List<Song>();
public IReadOnlyCollection<Song> Playlist => _playlist; public IReadOnlyCollection<Song> Playlist => _playlist;
private object playlistLock = new object(); private readonly object playlistLock = new object();
public Song CurrentSong { get; set; } = default(Song); public Song CurrentSong { get; set; } = default(Song);
private CancellationTokenSource SongCancelSource { get; set; } private CancellationTokenSource SongCancelSource { get; set; }
@ -44,26 +43,31 @@ namespace NadekoBot.Classes.Music {
public Channel PlaybackVoiceChannel { get; private set; } public Channel PlaybackVoiceChannel { get; private set; }
public MusicPlayer(Channel startingVoiceChannel, float defaultVolume) { public MusicPlayer(Channel startingVoiceChannel, float? defaultVolume) {
if (startingVoiceChannel == null) if (startingVoiceChannel == null)
throw new ArgumentNullException(nameof(startingVoiceChannel)); throw new ArgumentNullException(nameof(startingVoiceChannel));
if (startingVoiceChannel.Type != ChannelType.Voice) if (startingVoiceChannel.Type != ChannelType.Voice)
throw new ArgumentException("Channel must be of type voice"); throw new ArgumentException("Channel must be of type voice");
Volume = defaultVolume ?? 1.0f;
PlaybackVoiceChannel = startingVoiceChannel; PlaybackVoiceChannel = startingVoiceChannel;
SongCancelSource = new CancellationTokenSource(); SongCancelSource = new CancellationTokenSource();
cancelToken = SongCancelSource.Token; cancelToken = SongCancelSource.Token;
Task.Run(async () => { Task.Run(async () => {
while (_client?.State != ConnectionState.Disconnected && while (true) {
_client?.State != ConnectionState.Disconnecting) { try {
_client = await PlaybackVoiceChannel.JoinAudio();
}
catch {
await Task.Delay(1000);
continue;
}
CurrentSong = GetNextSong(); CurrentSong = GetNextSong();
if (CurrentSong != null) { if (CurrentSong != null) {
try { try {
_client = await PlaybackVoiceChannel.JoinAudio();
OnStarted(CurrentSong); OnStarted(CurrentSong);
CurrentSong.Play(_client, cancelToken); await CurrentSong.Play(_client, cancelToken);
} }
catch (OperationCanceledException) { catch (OperationCanceledException) {
Console.WriteLine("Song canceled"); Console.WriteLine("Song canceled");
@ -71,7 +75,10 @@ namespace NadekoBot.Classes.Music {
catch (Exception ex) { catch (Exception ex) {
Console.WriteLine($"Exception in PlaySong: {ex}"); Console.WriteLine($"Exception in PlaySong: {ex}");
} }
OnCompleted(CurrentSong); try {
OnCompleted(CurrentSong);
}
catch { }
SongCancelSource = new CancellationTokenSource(); SongCancelSource = new CancellationTokenSource();
cancelToken = SongCancelSource.Token; cancelToken = SongCancelSource.Token;
} }
@ -81,30 +88,31 @@ namespace NadekoBot.Classes.Music {
} }
public void Next() { public void Next() {
if (!SongCancelSource.IsCancellationRequested) lock (playlistLock) {
SongCancelSource.Cancel(); if (!SongCancelSource.IsCancellationRequested) {
Paused = false;
SongCancelSource.Cancel();
}
}
} }
public async Task Stop() { public void Stop() {
lock (playlistLock) {
lock (_playlist) {
_playlist.Clear(); _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 TogglePause() => Paused = !Paused;
public void Shuffle() { public void Shuffle() {
lock (_playlist) { lock (playlistLock) {
_playlist.Shuffle(); _playlist.Shuffle();
} }
} }
@ -115,7 +123,8 @@ namespace NadekoBot.Classes.Music {
if (volume > 150) if (volume > 150)
volume = 150; volume = 150;
return (int)(Volume = volume / 100.0f); Volume = volume / 100.0f;
return volume;
} }
private Song GetNextSong() { private Song GetNextSong() {

View File

@ -10,42 +10,157 @@ using System.Threading.Tasks;
using VideoLibrary; using VideoLibrary;
namespace NadekoBot.Classes.Music { namespace NadekoBot.Classes.Music {
public class SongInfo { public class SongInfo {
public string Provider { get; internal set; } public string Provider { get; internal set; }
public MusicType ProviderType { get; internal set; }
public string Title { get; internal set; } public string Title { get; internal set; }
public string Uri { 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 class Song {
public StreamState State { get; internal set; } public StreamState State { get; internal set; }
public object PrettyName => public string PrettyName =>
$"**【 {SongInfo.Title.TrimTo(55)} 】**`{(SongInfo.Provider ?? "-")}`"; $"**【 {SongInfo.Title.TrimTo(55)} 】**`{(SongInfo.Provider ?? "-")}`";
public SongInfo SongInfo { get; } 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) { private Song(SongInfo songInfo) {
this.SongInfo = songInfo; this.SongInfo = songInfo;
} }
internal void Play(IAudioClient voiceClient, CancellationToken cancelToken) { private Task BufferSong(CancellationToken cancelToken) =>
var p = Process.Start(new ProcessStartInfo { Task.Run(async () => {
FileName = "ffmpeg", var p = Process.Start(new ProcessStartInfo {
Arguments = $"-i {SongInfo.Uri} -f s16le -ar 48000 -ac 2 pipe:1 -loglevel quiet", FileName = "ffmpeg",
UseShellExecute = false, Arguments = $"-i {SongInfo.Uri} -f s16le -ar 48000 -ac 2 pipe:1 -loglevel quiet",
RedirectStandardOutput = true, 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]; byte[] buffer = new byte[blockSize];
int read; int attempt = 0;
while (!cancelToken.IsCancellationRequested) { while (!cancelToken.IsCancellationRequested) {
read = p.StandardOutput.BaseStream.Read(buffer, 0, blockSize); int read = songBuffer.Read(buffer, blockSize);
if (read == 0) 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.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) { public static async Task<Song> ResolveSong(string query, MusicType musicType = MusicType.Normal) {
@ -63,6 +178,7 @@ namespace NadekoBot.Classes.Music {
Uri = "\"" + Path.GetFullPath(query) + "\"", Uri = "\"" + Path.GetFullPath(query) + "\"",
Title = Path.GetFileNameWithoutExtension(query), Title = Path.GetFileNameWithoutExtension(query),
Provider = "Local File", Provider = "Local File",
ProviderType = musicType,
}); });
} }
else if (musicType == MusicType.Radio) { else if (musicType == MusicType.Radio) {
@ -70,6 +186,7 @@ namespace NadekoBot.Classes.Music {
Uri = query, Uri = query,
Title = $"{query}", Title = $"{query}",
Provider = "Radio Stream", Provider = "Radio Stream",
ProviderType = musicType,
}); });
} }
else if (SoundCloud.Default.IsSoundCloudLink(query)) { else if (SoundCloud.Default.IsSoundCloudLink(query)) {
@ -78,6 +195,7 @@ namespace NadekoBot.Classes.Music {
Title = svideo.FullName, Title = svideo.FullName,
Provider = "SoundCloud", Provider = "SoundCloud",
Uri = svideo.StreamLink, Uri = svideo.StreamLink,
ProviderType = musicType,
}); });
} }
else { else {
@ -97,6 +215,7 @@ namespace NadekoBot.Classes.Music {
Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube" Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube"
Provider = "YouTube", Provider = "YouTube",
Uri = video.Uri, Uri = video.Uri,
ProviderType = musicType,
}); });
} }

View File

@ -293,30 +293,6 @@ namespace NadekoBot.Classes.Music {
if (oldState == StreamState.Playing) if (oldState == StreamState.Playing)
OnCompleted(); 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 { public class DualStream : MemoryStream {

View File

@ -58,8 +58,14 @@ namespace NadekoBot.Modules {
.Description("Completely stops the music, unbinds the bot from the channel, and cleans up files.") .Description("Completely stops the music, unbinds the bot from the channel, and cleans up files.")
.Do(async e => { .Do(async e => {
MusicPlayer musicPlayer; MusicPlayer musicPlayer;
if (!musicPlayers.TryRemove(e.Server, out musicPlayer)) return; if (!musicPlayers.TryGetValue(e.Server, out musicPlayer)) return;
await musicPlayer.Stop(); 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") cgb.CreateCommand("p")
@ -95,6 +101,8 @@ namespace NadekoBot.Modules {
string toSend = "🎵 **" + musicPlayer.Playlist.Count + "** `videos currently queued.` "; string toSend = "🎵 **" + musicPlayer.Playlist.Count + "** `videos currently queued.` ";
if (musicPlayer.Playlist.Count >= MusicPlayer.MaximumPlaylistSize) if (musicPlayer.Playlist.Count >= MusicPlayer.MaximumPlaylistSize)
toSend += "**Song queue is full!**\n"; toSend += "**Song queue is full!**\n";
else
toSend += "\n";
int number = 1; int number = 1;
await e.Channel.SendMessage(toSend + string.Join("\n", musicPlayer.Playlist.Take(15).Select(v => $"`{number++}.` {v.PrettyName}"))); 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; float throwAway;
if (defaultMusicVolumes.TryGetValue(TextCh.Server.Id, out throwAway)) if (defaultMusicVolumes.TryGetValue(TextCh.Server.Id, out throwAway))
vol = throwAway; vol = throwAway;
musicPlayer = new MusicPlayer(VoiceCh) { musicPlayer = new MusicPlayer(VoiceCh, vol) {
OnCompleted = async (song) => { OnCompleted = async (song) => {
try { try {
await TextCh.SendMessage($"🎵`Finished`{song.PrettyName}"); await TextCh.SendMessage($"🎵`Finished`{song.PrettyName}");
@ -331,7 +339,7 @@ namespace NadekoBot.Modules {
musicPlayers.TryAdd(TextCh.Server, musicPlayer); musicPlayers.TryAdd(TextCh.Server, musicPlayer);
} }
var resolvedSong = await Song.ResolveSong(query, musicType); var resolvedSong = await Song.ResolveSong(query, musicType);
resolvedSong.MusicPlayer = musicPlayer;
if(!silent) if(!silent)
await TextCh.Send($"🎵`Queued`{resolvedSong.PrettyName}"); await TextCh.Send($"🎵`Queued`{resolvedSong.PrettyName}");
musicPlayer.AddSong(resolvedSong); musicPlayer.AddSong(resolvedSong);

View File

@ -66,6 +66,13 @@ namespace NadekoBot {
//create new discord client //create new discord client
client = new DiscordClient(new DiscordConfigBuilder() { client = new DiscordClient(new DiscordConfigBuilder() {
MessageCacheSize = 20, 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 //create a command service
@ -141,15 +148,16 @@ namespace NadekoBot {
Classes.Permissions.PermissionsHandler.Initialize(); Classes.Permissions.PermissionsHandler.Initialize();
client.ClientAPI.SendingRequest += (s, e) => { client.ClientAPI.SendingRequest += (s, e) => {
try { try {
var request = e.Request as Discord.API.Client.Rest.SendMessageRequest; var request = e.Request as Discord.API.Client.Rest.SendMessageRequest;
if (request != null) { 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)) if (string.IsNullOrWhiteSpace(request.Content))
e.Cancel = true; e.Cancel = true;
else //else
Console.WriteLine("Sending request."); // Console.WriteLine("Sending request");
var content = request.Content;
} }
} }
catch { catch {
@ -157,15 +165,15 @@ namespace NadekoBot {
} }
}; };
client.ClientAPI.SentRequest += (s, e) => { //client.ClientAPI.SentRequest += (s, e) => {
try { // try {
var request = e.Request as Discord.API.Client.Rest.SendMessageRequest; // var request = e.Request as Discord.API.Client.Rest.SendMessageRequest;
if (request != null) { // if (request != null) {
Console.WriteLine("Sent."); // Console.WriteLine("Sent.");
} // }
} // }
catch { Console.WriteLine("SENT REQUEST ERRORED!!!"); } // catch { Console.WriteLine("SENT REQUEST ERRORED!!!"); }
}; //};
}); });
Console.WriteLine("Exiting..."); Console.WriteLine("Exiting...");
Console.ReadKey(); Console.ReadKey();

Binary file not shown.