small refactor

This commit is contained in:
Master Kwoth 2016-04-01 22:50:41 +02:00
parent 52923e2680
commit 450ca709b4
3 changed files with 260 additions and 171 deletions

View File

@ -0,0 +1,120 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Classes.Music
{
/// <summary>
/// 💩
/// </summary>
public class PoopyBuffer
{
private readonly byte[] ringBuffer;
public int WritePosition { get; private set; } = 0;
public int ReadPosition { get; private set; } = 0;
public int ContentLength => (WritePosition >= ReadPosition ?
WritePosition - ReadPosition :
(BufferSize - ReadPosition) + WritePosition);
public int BufferSize { get; }
private readonly object readWriteLock = new object();
public PoopyBuffer(int size)
{
if (size <= 0)
throw new ArgumentException();
BufferSize = size;
ringBuffer = new byte[size];
}
public int Read(byte[] buffer, int count)
{
if (buffer.Length < count)
throw new ArgumentException();
//Console.WriteLine($"***\nRead: {ReadPosition}\nWrite: {WritePosition}\nContentLength:{ContentLength}\n***");
lock (readWriteLock)
{
//read as much as you can if you're reading too much
if (count > ContentLength)
count = ContentLength;
//if nothing to read, return 0
if (WritePosition == ReadPosition)
return 0;
// if buffer is in the "normal" state, just read
if (WritePosition > ReadPosition)
{
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, count);
ReadPosition += count;
//Console.WriteLine($"Read only normally1 {count}[{ReadPosition - count} to {ReadPosition}]");
return count;
}
//else ReadPos <Writepos
// buffer is in its inverted state
// A: if i can read as much as possible without hitting the buffer.length, read that
if (count + ReadPosition <= BufferSize)
{
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, count);
ReadPosition += count;
//Console.WriteLine($"Read only normally2 {count}[{ReadPosition - count} to {ReadPosition}]");
return count;
}
// B: if i can't read as much, read to the end,
var readNormaly = BufferSize - ReadPosition;
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, readNormaly);
//Console.WriteLine($"Read normaly {count}[{ReadPosition} to {ReadPosition + readNormaly}]");
//then read the remaining amount from the start
var readFromStart = count - readNormaly;
Buffer.BlockCopy(ringBuffer, 0, buffer, readNormaly, readFromStart);
//Console.WriteLine($"Read From start {readFromStart}[{0} to {readFromStart}]");
ReadPosition = readFromStart;
return count;
}
}
public async Task WriteAsync(byte[] buffer, int count, CancellationToken cancelToken)
{
if (count > buffer.Length)
throw new ArgumentException();
while (ContentLength + count > BufferSize)
{
await Task.Delay(20, cancelToken);
if (cancelToken.IsCancellationRequested)
return;
}
//the while above assures that i cannot write past readposition with my write, so i don't have to check
// *unless its multithreaded or task is not awaited
lock (readWriteLock)
{
// if i can just write without hitting buffer.length, do it
if (WritePosition + count < BufferSize)
{
Buffer.BlockCopy(buffer, 0, ringBuffer, WritePosition, count);
WritePosition += count;
//Console.WriteLine($"Wrote only normally {count}[{WritePosition - count} to {WritePosition}]");
return;
}
// otherwise, i have to write to the end, then write the rest from the start
var wroteNormaly = BufferSize - WritePosition;
Buffer.BlockCopy(buffer, 0, ringBuffer, WritePosition, wroteNormaly);
//Console.WriteLine($"Wrote normally {wroteNormaly}[{WritePosition} to {BufferSize}]");
var wroteFromStart = count - wroteNormaly;
Buffer.BlockCopy(buffer, wroteNormaly, ringBuffer, 0, wroteFromStart);
//Console.WriteLine($"and from start {wroteFromStart} [0 to {wroteFromStart}");
WritePosition = wroteFromStart;
}
}
}
}

View File

@ -10,116 +10,17 @@ using System.Threading;
using System.Threading.Tasks;
using VideoLibrary;
namespace NadekoBot.Classes.Music {
public class SongInfo {
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 readonly byte[] ringBuffer;
public int WritePosition { get; private set; } = 0;
public int ReadPosition { get; private set; } = 0;
public int ContentLength => (WritePosition >= ReadPosition ?
WritePosition - ReadPosition :
(BufferSize - ReadPosition) + WritePosition);
public int BufferSize { get; }
private readonly object readWriteLock = new object();
public PoopyBuffer(int size) {
if (size <= 0)
throw new ArgumentException();
BufferSize = size;
ringBuffer = new byte[size];
}
public int Read(byte[] buffer, int count) {
if (buffer.Length < count)
throw new ArgumentException();
//Console.WriteLine($"***\nRead: {ReadPosition}\nWrite: {WritePosition}\nContentLength:{ContentLength}\n***");
lock (readWriteLock) {
//read as much as you can if you're reading too much
if (count > ContentLength)
count = ContentLength;
//if nothing to read, return 0
if (WritePosition == ReadPosition)
return 0;
// if buffer is in the "normal" state, just read
if (WritePosition > ReadPosition) {
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, count);
ReadPosition += count;
//Console.WriteLine($"Read only normally1 {count}[{ReadPosition - count} to {ReadPosition}]");
return count;
}
//else ReadPos <Writepos
// buffer is in its inverted state
// A: if i can read as much as possible without hitting the buffer.length, read that
if (count + ReadPosition <= BufferSize) {
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, count);
ReadPosition += count;
//Console.WriteLine($"Read only normally2 {count}[{ReadPosition - count} to {ReadPosition}]");
return count;
}
// B: if i can't read as much, read to the end,
var readNormaly = BufferSize - ReadPosition;
Buffer.BlockCopy(ringBuffer, ReadPosition, buffer, 0, readNormaly);
//Console.WriteLine($"Read normaly {count}[{ReadPosition} to {ReadPosition + readNormaly}]");
//then read the remaining amount from the start
var readFromStart = count - readNormaly;
Buffer.BlockCopy(ringBuffer, 0, buffer, readNormaly, readFromStart);
//Console.WriteLine($"Read From start {readFromStart}[{0} to {readFromStart}]");
ReadPosition = readFromStart;
return count;
}
}
public async Task WriteAsync(byte[] buffer, int count, CancellationToken cancelToken) {
if (count > buffer.Length)
throw new ArgumentException();
while (ContentLength + count > BufferSize) {
await Task.Delay(20, cancelToken);
if (cancelToken.IsCancellationRequested)
return;
}
//the while above assures that i cannot write past readposition with my write, so i don't have to check
// *unless its multithreaded or task is not awaited
lock (readWriteLock) {
// if i can just write without hitting buffer.length, do it
if (WritePosition + count < BufferSize) {
Buffer.BlockCopy(buffer, 0, ringBuffer, WritePosition, count);
WritePosition += count;
//Console.WriteLine($"Wrote only normally {count}[{WritePosition - count} to {WritePosition}]");
return;
}
// otherwise, i have to write to the end, then write the rest from the start
var wroteNormaly = BufferSize - WritePosition;
Buffer.BlockCopy(buffer, 0, ringBuffer, WritePosition, wroteNormaly);
//Console.WriteLine($"Wrote normally {wroteNormaly}[{WritePosition} to {BufferSize}]");
var wroteFromStart = count - wroteNormaly;
Buffer.BlockCopy(buffer, wroteNormaly, ringBuffer, 0, wroteFromStart);
//Console.WriteLine($"and from start {wroteFromStart} [0 to {wroteFromStart}");
WritePosition = wroteFromStart;
}
}
}
public class Song {
public class Song
{
public StreamState State { get; internal set; }
public string PrettyName =>
$"**【 {SongInfo.Title.TrimTo(55)} 】**`{(SongInfo.Provider ?? "-")}`";
@ -130,22 +31,27 @@ namespace NadekoBot.Classes.Music {
private bool prebufferingComplete { get; set; } = false;
public MusicPlayer MusicPlayer { get; set; }
public string PrettyCurrentTime() {
public string PrettyCurrentTime()
{
var time = TimeSpan.FromSeconds(bytesSent / 3840 / 50);
return $"【{(int)time.TotalMinutes}m {time.Seconds}s】";
}
private ulong bytesSent { get; set; } = 0;
private Song(SongInfo songInfo) {
private Song(SongInfo songInfo)
{
this.SongInfo = songInfo;
}
private Task BufferSong(CancellationToken cancelToken) =>
Task.Run(async () => {
Task.Factory.StartNew(async () =>
{
Process p = null;
try {
p = Process.Start(new ProcessStartInfo {
try
{
p = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $"-i {SongInfo.Uri} -f s16le -ar 48000 -ac 2 pipe:1 -loglevel quiet",
UseShellExecute = false,
@ -156,11 +62,15 @@ namespace NadekoBot.Classes.Music {
const int blockSize = 3840;
var buffer = new byte[blockSize];
var attempt = 0;
while (!cancelToken.IsCancellationRequested) {
while (!cancelToken.IsCancellationRequested)
{
var read = 0;
try {
try
{
read = await p.StandardOutput.BaseStream.ReadAsync(buffer, 0, blockSize, cancelToken);
} catch {
}
catch
{
return;
}
if (read == 0)
@ -168,7 +78,8 @@ namespace NadekoBot.Classes.Music {
break;
else
await Task.Delay(100, cancelToken);
else {
else
{
attempt = 0;
await Task.Delay(5, cancelToken);
}
@ -176,24 +87,35 @@ namespace NadekoBot.Classes.Music {
if (songBuffer.ContentLength > 2.MB())
prebufferingComplete = true;
}
} catch (Exception ex) {
}
catch (Exception ex)
{
Console.WriteLine($"Buffering errored: {ex.Message}");
} finally {
}
finally
{
Console.WriteLine($"Buffering done." + $" [{songBuffer.ContentLength}]");
if (p != null) {
try {
if (p != null)
{
try
{
p.Kill();
} catch { }
}
catch { }
p.Dispose();
}
}
});
}, TaskCreationOptions.LongRunning);
internal async Task Play(IAudioClient voiceClient, CancellationToken cancelToken) {
internal async Task Play(IAudioClient voiceClient, CancellationToken cancelToken)
{
var bufferTask = new ConfiguredTaskAwaitable();
try {
try
{
bufferTask = BufferSong(cancelToken).ConfigureAwait(false);
} catch (Exception ex) {
}
catch (Exception ex)
{
var clr = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"ERR BUFFER START : {ex.Message}\n{ex}");
@ -202,26 +124,31 @@ namespace NadekoBot.Classes.Music {
var bufferAttempts = 0;
const int waitPerAttempt = 500;
var toAttemptTimes = SongInfo.ProviderType != MusicType.Normal ? 5 : 9;
while (!prebufferingComplete && bufferAttempts++ < toAttemptTimes) {
while (!prebufferingComplete && bufferAttempts++ < toAttemptTimes)
{
await Task.Delay(waitPerAttempt, cancelToken);
}
cancelToken.ThrowIfCancellationRequested();
Console.WriteLine($"Prebuffering done? in {waitPerAttempt * bufferAttempts}");
const int blockSize = 3840;
var attempt = 0;
while (!cancelToken.IsCancellationRequested) {
while (!cancelToken.IsCancellationRequested)
{
//Console.WriteLine($"Read: {songBuffer.ReadPosition}\nWrite: {songBuffer.WritePosition}\nContentLength:{songBuffer.ContentLength}\n---------");
byte[] buffer = new byte[blockSize];
var read = songBuffer.Read(buffer, blockSize);
unchecked {
unchecked
{
bytesSent += (ulong)read;
}
if (read == 0)
if (attempt++ == 20) {
if (attempt++ == 20)
{
voiceClient.Wait();
Console.WriteLine($"Song finished. [{songBuffer.ContentLength}]");
break;
} else
}
else
await Task.Delay(100, cancelToken);
else
attempt = 0;
@ -239,11 +166,13 @@ namespace NadekoBot.Classes.Music {
}
//stackoverflow ftw
private static byte[] AdjustVolume(byte[] audioSamples, float volume) {
private static byte[] AdjustVolume(byte[] audioSamples, float volume)
{
if (Math.Abs(volume - 1.0f) < 0.01f)
return audioSamples;
var array = new byte[audioSamples.Length];
for (var i = 0; i < array.Length; i += 2) {
for (var i = 0; i < array.Length; i += 2)
{
// convert byte pair to int
short buf1 = audioSamples[i + 1];
@ -263,35 +192,43 @@ namespace NadekoBot.Classes.Music {
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)
{
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentNullException(nameof(query));
if (musicType != MusicType.Local && IsRadioLink(query)) {
if (musicType != MusicType.Local && IsRadioLink(query))
{
musicType = MusicType.Radio;
query = await HandleStreamContainers(query) ?? query;
}
try {
switch (musicType) {
try
{
switch (musicType)
{
case MusicType.Local:
return new Song(new SongInfo {
return new Song(new SongInfo
{
Uri = "\"" + Path.GetFullPath(query) + "\"",
Title = Path.GetFileNameWithoutExtension(query),
Provider = "Local File",
ProviderType = musicType,
});
case MusicType.Radio:
return new Song(new SongInfo {
return new Song(new SongInfo
{
Uri = query,
Title = $"{query}",
Provider = "Radio Stream",
ProviderType = musicType,
});
}
if (SoundCloud.Default.IsSoundCloudLink(query)) {
if (SoundCloud.Default.IsSoundCloudLink(query))
{
var svideo = await SoundCloud.Default.GetVideoAsync(query);
return new Song(new SongInfo {
return new Song(new SongInfo
{
Title = svideo.FullName,
Provider = "SoundCloud",
Uri = svideo.StreamLink,
@ -310,76 +247,99 @@ namespace NadekoBot.Classes.Music {
if (video == null) // do something with this error
throw new Exception("Could not load any video elements based on the query.");
return new Song(new SongInfo {
return new Song(new SongInfo
{
Title = video.Title.Substring(0, video.Title.Length - 10), // removing trailing "- You Tube"
Provider = "YouTube",
Uri = video.Uri,
ProviderType = musicType,
});
} catch (Exception ex) {
}
catch (Exception ex)
{
Console.WriteLine($"Failed resolving the link.{ex.Message}");
return null;
}
}
private static async Task<string> HandleStreamContainers(string query) {
private static async Task<string> HandleStreamContainers(string query)
{
string file = null;
try {
try
{
file = await SearchHelper.GetResponseStringAsync(query);
} catch {
}
catch
{
return query;
}
if (query.Contains(".pls")) {
if (query.Contains(".pls"))
{
//File1=http://armitunes.com:8000/
//Regex.Match(query)
try {
try
{
var m = Regex.Match(file, "File1=(?<url>.*?)\\n");
var res = m.Groups["url"]?.ToString();
return res?.Trim();
} catch {
}
catch
{
Console.WriteLine($"Failed reading .pls:\n{file}");
return null;
}
}
if (query.Contains(".m3u")) {
if (query.Contains(".m3u"))
{
/*
# This is a comment
C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3
C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3
*/
try {
try
{
var m = Regex.Match(file, "(?<url>^[^#].*)", RegexOptions.Multiline);
var res = m.Groups["url"]?.ToString();
return res?.Trim();
} catch {
}
catch
{
Console.WriteLine($"Failed reading .m3u:\n{file}");
return null;
}
}
if (query.Contains(".asx")) {
if (query.Contains(".asx"))
{
//<ref href="http://armitunes.com:8000"/>
try {
try
{
var m = Regex.Match(file, "<ref href=\"(?<url>.*?)\"");
var res = m.Groups["url"]?.ToString();
return res?.Trim();
} catch {
}
catch
{
Console.WriteLine($"Failed reading .asx:\n{file}");
return null;
}
}
if (query.Contains(".xspf")) {
if (query.Contains(".xspf"))
{
/*
<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<trackList>
<track><location>file:///mp3s/song_1.mp3</location></track>
*/
try {
try
{
var m = Regex.Match(file, "<location>(?<url>.*?)</location>");
var res = m.Groups["url"]?.ToString();
return res?.Trim();
} catch {
}
catch
{
Console.WriteLine($"Failed reading .xspf:\n{file}");
return null;
}

View File

@ -1,35 +1,41 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
using System.Collections.Concurrent;
using Discord;
using Discord.Commands;
using NadekoBot.Modules;
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using TriviaGame = NadekoBot.Classes.Trivia.TriviaGame;
namespace NadekoBot.Commands {
internal class Trivia : DiscordCommand {
namespace NadekoBot.Commands
{
internal class Trivia : DiscordCommand
{
public static ConcurrentDictionary<ulong, TriviaGame> RunningTrivias = new ConcurrentDictionary<ulong, TriviaGame>();
public Func<CommandEventArgs, Task> DoFunc() => async e => {
public Func<CommandEventArgs, Task> DoFunc() => async e =>
{
TriviaGame trivia;
if (!RunningTrivias.TryGetValue(e.Server.Id, out trivia)) {
if (!RunningTrivias.TryGetValue(e.Server.Id, out trivia))
{
var triviaGame = new TriviaGame(e);
if (RunningTrivias.TryAdd(e.Server.Id, triviaGame))
await e.Channel.SendMessage("**Trivia game started!**\nFirst player to get to 10 points wins! You have 30 seconds per question.\nUse command `tq` if game was started by accident.**");
else
await triviaGame.StopGame();
} else
}
else
await e.Channel.SendMessage("Trivia game is already running on this server.\n" + trivia.CurrentQuestion);
};
internal override void Init(CommandGroupBuilder cgb) {
internal override void Init(CommandGroupBuilder cgb)
{
cgb.CreateCommand(Module.Prefix + "t")
.Description("Starts a game of trivia.")
.Do(DoFunc());
cgb.CreateCommand(Module.Prefix + "tl")
.Description("Shows a current trivia leaderboard.")
.Do(async e=> {
.Do(async e =>
{
TriviaGame trivia;
if (RunningTrivias.TryGetValue(e.Server.Id, out trivia))
await e.Channel.SendMessage(trivia.GetLeaderboard());
@ -39,15 +45,18 @@ namespace NadekoBot.Commands {
cgb.CreateCommand(Module.Prefix + "tq")
.Description("Quits current trivia after current question.")
.Do(async e=> {
.Do(async e =>
{
TriviaGame trivia;
if (RunningTrivias.TryGetValue(e.Server.Id, out trivia)) {
if (RunningTrivias.TryGetValue(e.Server.Id, out trivia))
{
await trivia.StopGame();
} else
}
else
await e.Channel.SendMessage("No trivia is running on this server.");
});
}
public Trivia(DiscordModule module) : base(module) {}
public Trivia(DiscordModule module) : base(module) { }
}
}