Merge branch 'music-rewrite' into dev

This commit is contained in:
Kwoth 2016-07-24 18:02:01 +02:00
commit fce0da50f1
5 changed files with 149 additions and 79 deletions

View File

@ -303,6 +303,15 @@ namespace NadekoBot.Extensions
public static int GiB(this int value) => value.MiB() * 1024; public static int GiB(this int value) => value.MiB() * 1024;
public static int GB(this int value) => value.MB() * 1000; public static int GB(this int value) => value.MB() * 1000;
public static ulong KiB(this ulong value) => value * 1024;
public static ulong KB(this ulong value) => value * 1000;
public static ulong MiB(this ulong value) => value.KiB() * 1024;
public static ulong MB(this ulong value) => value.KB() * 1000;
public static ulong GiB(this ulong value) => value.MiB() * 1024;
public static ulong GB(this ulong value) => value.MB() * 1000;
public static Stream ToStream(this Image img, System.Drawing.Imaging.ImageFormat format = null) public static Stream ToStream(this Image img, System.Drawing.Imaging.ImageFormat format = null)
{ {
if (format == null) if (format == null)
@ -362,5 +371,7 @@ namespace NadekoBot.Extensions
return sw.BaseStream; return sw.BaseStream;
} }
public static double UnixTimestamp(this DateTime dt) => dt.ToUniversalTime().Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
} }
} }

View File

@ -113,17 +113,10 @@ namespace NadekoBot.Modules.Music.Classes
if (CurrentSong == null) if (CurrentSong == null)
continue; continue;
try
{
OnStarted(this, CurrentSong); OnStarted(this, CurrentSong);
await CurrentSong.Play(audioClient, cancelToken); await CurrentSong.Play(audioClient, cancelToken);
}
catch (OperationCanceledException)
{
Console.WriteLine("Song canceled");
SongCancelSource = new CancellationTokenSource();
cancelToken = SongCancelSource.Token;
}
OnCompleted(this, CurrentSong); OnCompleted(this, CurrentSong);
if (RepeatPlaylist) if (RepeatPlaylist)
@ -135,6 +128,12 @@ namespace NadekoBot.Modules.Music.Classes
} }
finally finally
{ {
if (!cancelToken.IsCancellationRequested)
{
SongCancelSource.Cancel();
}
SongCancelSource = new CancellationTokenSource();
cancelToken = SongCancelSource.Token;
CurrentSong = null; CurrentSong = null;
await Task.Delay(300).ConfigureAwait(false); await Task.Delay(300).ConfigureAwait(false);
} }

View File

@ -3,6 +3,7 @@ using NadekoBot.Classes;
using NadekoBot.Extensions; using NadekoBot.Extensions;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -31,9 +32,7 @@ namespace NadekoBot.Modules.Music.Classes
public SongInfo SongInfo { get; } public SongInfo SongInfo { get; }
public string QueuerName { get; set; } public string QueuerName { get; set; }
private PoopyBuffer songBuffer { get; set; } private bool bufferingCompleted { get; set; } = false;
private bool prebufferingComplete { get; set; } = false;
public MusicPlayer MusicPlayer { get; set; } public MusicPlayer MusicPlayer { get; set; }
public string PrettyCurrentTime() public string PrettyCurrentTime()
@ -74,7 +73,7 @@ namespace NadekoBot.Modules.Music.Classes
return this; return this;
} }
private Task BufferSong(CancellationToken cancelToken) => private Task BufferSong(string filename, CancellationToken cancelToken) =>
Task.Factory.StartNew(async () => Task.Factory.StartNew(async () =>
{ {
Process p = null; Process p = null;
@ -89,40 +88,38 @@ namespace NadekoBot.Modules.Music.Classes
RedirectStandardError = false, RedirectStandardError = false,
CreateNoWindow = true, CreateNoWindow = true,
}); });
const int blockSize = 3840; var prebufferSize = 100ul.MiB();
var buffer = new byte[blockSize]; using (var outStream = new FileStream(filename, FileMode.Append, FileAccess.Write, FileShare.Read))
var attempt = 0;
while (!cancelToken.IsCancellationRequested)
{ {
var read = 0; byte[] buffer = new byte[81920];
try int bytesRead;
while ((bytesRead = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancelToken).ConfigureAwait(false)) != 0)
{ {
read = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, blockSize, cancelToken) await outStream.WriteAsync(buffer, 0, bytesRead, cancelToken).ConfigureAwait(false);
.ConfigureAwait(false); while ((ulong)outStream.Length - bytesSent > prebufferSize)
await Task.Delay(100, cancelToken);
} }
catch
{
return;
} }
if (read == 0)
if (attempt++ == 50) bufferingCompleted = true;
break;
else
await Task.Delay(100, cancelToken).ConfigureAwait(false);
else
attempt = 0;
await songBuffer.WriteAsync(buffer, read, cancelToken).ConfigureAwait(false);
if (songBuffer.ContentLength > 2.MB())
prebufferingComplete = true;
} }
catch (System.ComponentModel.Win32Exception) {
var oldclr = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(@"You have not properly installed or configured FFMPEG.
Please install and configure FFMPEG to play music.
Check the guides for your platform on how to setup ffmpeg correctly:
Windows Guide: https://goo.gl/SCv72y
Linux Guide: https://goo.gl/rRhjCp");
Console.ForegroundColor = oldclr;
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Buffering errored: {ex.Message}"); Console.WriteLine($"Buffering stopped: {ex.Message}");
} }
finally finally
{ {
Console.WriteLine($"Buffering done." + $" [{songBuffer.ContentLength}]"); Console.WriteLine($"Buffering done.");
if (p != null) if (p != null)
{ {
try try
@ -137,25 +134,41 @@ namespace NadekoBot.Modules.Music.Classes
internal async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) internal async Task Play(IAudioClient voiceClient, CancellationToken cancelToken)
{ {
// initialize the buffer here because if this song was playing before (requeued), we must delete old buffer data var filename = Path.Combine(MusicModule.MusicDataPath, DateTime.Now.UnixTimestamp().ToString());
songBuffer = new PoopyBuffer(NadekoBot.Config.BufferSize);
var bufferTask = BufferSong(cancelToken).ConfigureAwait(false); var bufferTask = BufferSong(filename, cancelToken).ConfigureAwait(false);
var bufferAttempts = 0;
const int waitPerAttempt = 500; var inStream = new FileStream(filename, FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write);
var toAttemptTimes = SongInfo.ProviderType != MusicType.Normal ? 5 : 9;
while (!prebufferingComplete && bufferAttempts++ < toAttemptTimes) bytesSent = 0;
try
{ {
await Task.Delay(waitPerAttempt, cancelToken).ConfigureAwait(false); var prebufferingTask = CheckPrebufferingAsync(inStream, cancelToken);
var sw = new Stopwatch();
sw.Start();
var t = await Task.WhenAny(prebufferingTask, Task.Delay(5000, cancelToken));
if (t != prebufferingTask)
{
Console.WriteLine("Prebuffering timed out or canceled. Cannot get any data from the stream.");
return;
} }
Console.WriteLine($"Prebuffering done? in {waitPerAttempt * bufferAttempts}"); else if(prebufferingTask.IsCanceled)
{
Console.WriteLine("Prebuffering timed out. Cannot get any data from the stream.");
return;
}
sw.Stop();
Console.WriteLine("Prebuffering successfully completed in "+ sw.Elapsed);
const int blockSize = 3840; const int blockSize = 3840;
var attempt = 0; var attempt = 0;
byte[] buffer = new byte[blockSize];
while (!cancelToken.IsCancellationRequested) while (!cancelToken.IsCancellationRequested)
{ {
//Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------"); //Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------");
byte[] buffer = new byte[blockSize]; var read = inStream.Read(buffer, 0, buffer.Length);
var read = await songBuffer.ReadAsync(buffer, blockSize).ConfigureAwait(false); //await inStream.CopyToAsync(voiceClient.OutputStream);
unchecked unchecked
{ {
bytesSent += (ulong)read; bytesSent += (ulong)read;
@ -164,7 +177,6 @@ namespace NadekoBot.Modules.Music.Classes
if (attempt++ == 20) if (attempt++ == 20)
{ {
voiceClient.Wait(); voiceClient.Wait();
Console.WriteLine($"Song finished. [{songBuffer.ContentLength}]");
break; break;
} }
else else
@ -174,16 +186,30 @@ namespace NadekoBot.Modules.Music.Classes
while (this.MusicPlayer.Paused) while (this.MusicPlayer.Paused)
await Task.Delay(200, cancelToken).ConfigureAwait(false); await Task.Delay(200, cancelToken).ConfigureAwait(false);
buffer = AdjustVolume(buffer, MusicPlayer.Volume); buffer = AdjustVolume(buffer, MusicPlayer.Volume);
voiceClient.Send(buffer, 0, read); voiceClient.Send(buffer, 0, read);
} }
Console.WriteLine("Awiting buffer task"); }
finally
{
await bufferTask; await bufferTask;
Console.WriteLine("Buffer task done."); await Task.Run(() => voiceClient.Clear());
voiceClient.Clear(); inStream.Dispose();
cancelToken.ThrowIfCancellationRequested(); try { File.Delete(filename); } catch { }
}
} }
private async Task CheckPrebufferingAsync(Stream inStream, CancellationToken cancelToken)
{
while (!bufferingCompleted && inStream.Length < 2.MiB())
{
await Task.Delay(100, cancelToken);
}
Console.WriteLine("Buffering successfull");
}
/*
//stackoverflow ftw //stackoverflow ftw
private static byte[] AdjustVolume(byte[] audioSamples, float volume) private static byte[] AdjustVolume(byte[] audioSamples, float volume)
{ {
@ -210,6 +236,33 @@ namespace NadekoBot.Modules.Music.Classes
} }
return array; return array;
} }
*/
//aidiakapi ftw
public unsafe static byte[] AdjustVolume(byte[] audioSamples, float volume)
{
Contract.Requires(audioSamples != null);
Contract.Requires(audioSamples.Length % 2 == 0);
Contract.Requires(volume >= 0f && volume <= 1f);
Contract.Assert(BitConverter.IsLittleEndian);
if (Math.Abs(volume - 1f) < 0.0001f) return audioSamples;
// 16-bit precision for the multiplication
int volumeFixed = (int)Math.Round(volume * 65536d);
int count = audioSamples.Length / 2;
fixed (byte* srcBytes = audioSamples)
{
short* src = (short*)srcBytes;
for (int i = count; i != 0; i--, src++)
*src = (short)(((*src) * volumeFixed) >> 16);
}
return audioSamples;
}
public static async Task<Song> ResolveSong(string query, MusicType musicType = MusicType.Normal) public static async Task<Song> ResolveSong(string query, MusicType musicType = MusicType.Normal)
{ {

View File

@ -18,11 +18,16 @@ namespace NadekoBot.Modules.Music
{ {
internal class MusicModule : DiscordModule internal class MusicModule : DiscordModule
{ {
public static ConcurrentDictionary<Server, MusicPlayer> MusicPlayers = new ConcurrentDictionary<Server, MusicPlayer>(); public static ConcurrentDictionary<Server, MusicPlayer> MusicPlayers = new ConcurrentDictionary<Server, MusicPlayer>();
public const string MusicDataPath = "data/musicdata";
public MusicModule() public MusicModule()
{ {
//it can fail if its currenctly opened or doesn't exist. Either way i don't care
try { Directory.Delete(MusicDataPath, true); } catch { }
Directory.CreateDirectory(MusicDataPath);
} }
public override string Prefix { get; } = NadekoBot.Config.CommandPrefixes.Music; public override string Prefix { get; } = NadekoBot.Config.CommandPrefixes.Music;

View File

@ -46,6 +46,7 @@
<Prefer32Bit>true</Prefer32Bit> <Prefer32Bit>true</Prefer32Bit>
<NoWarn> <NoWarn>
</NoWarn> </NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>AnyCPU</PlatformTarget>
@ -116,6 +117,7 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
<Prefer32Bit>true</Prefer32Bit> <Prefer32Bit>true</Prefer32Bit>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="libvideo, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="libvideo, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">