@@ -0,0 +1,16 @@ | |||
using System.IO; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
public abstract class AudioOutStream : Stream | |||
{ | |||
public override bool CanRead => false; | |||
public override bool CanSeek => false; | |||
public override bool CanWrite => true; | |||
public virtual void Clear() { } | |||
public virtual Task ClearAsync(CancellationToken cancelToken) { return Task.Delay(0); } | |||
} | |||
} |
@@ -22,26 +22,26 @@ namespace Discord.Audio | |||
/// </summary> | |||
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param> | |||
/// <returns></returns> | |||
Stream CreateOpusStream(int samplesPerFrame, int bufferMillis = 1000); | |||
AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis = 1000); | |||
/// <summary> | |||
/// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. | |||
/// </summary> | |||
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param> | |||
/// <returns></returns> | |||
Stream CreateDirectOpusStream(int samplesPerFrame); | |||
AudioOutStream CreateDirectOpusStream(int samplesPerFrame); | |||
/// <summary> | |||
/// Creates a new outgoing stream accepting PCM (raw) data. | |||
/// </summary> | |||
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param> | |||
/// <param name="bitrate"></param> | |||
/// <returns></returns> | |||
Stream CreatePCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null, int bufferMillis = 1000); | |||
AudioOutStream CreatePCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null, int bufferMillis = 1000); | |||
/// <summary> | |||
/// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. | |||
/// </summary> | |||
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param> | |||
/// <param name="bitrate"></param> | |||
/// <returns></returns> | |||
Stream CreateDirectPCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null); | |||
AudioOutStream CreateDirectPCMStream(int samplesPerFrame, int channels = 2, int? bitrate = null); | |||
} | |||
} |
@@ -170,25 +170,25 @@ namespace Discord.Audio | |||
await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false); | |||
} | |||
public Stream CreateOpusStream(int samplesPerFrame, int bufferMillis) | |||
public AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis) | |||
{ | |||
CheckSamplesPerFrame(samplesPerFrame); | |||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token); | |||
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc); | |||
} | |||
public Stream CreateDirectOpusStream(int samplesPerFrame) | |||
public AudioOutStream CreateDirectOpusStream(int samplesPerFrame) | |||
{ | |||
CheckSamplesPerFrame(samplesPerFrame); | |||
var target = new DirectAudioTarget(ApiClient); | |||
return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc); | |||
} | |||
public Stream CreatePCMStream(int samplesPerFrame, int channels, int? bitrate, int bufferMillis) | |||
public AudioOutStream CreatePCMStream(int samplesPerFrame, int channels, int? bitrate, int bufferMillis) | |||
{ | |||
CheckSamplesPerFrame(samplesPerFrame); | |||
var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token); | |||
return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate); | |||
} | |||
public Stream CreateDirectPCMStream(int samplesPerFrame, int channels, int? bitrate) | |||
public AudioOutStream CreateDirectPCMStream(int samplesPerFrame, int channels, int? bitrate) | |||
{ | |||
CheckSamplesPerFrame(samplesPerFrame); | |||
var target = new DirectAudioTarget(ApiClient); | |||
@@ -5,7 +5,7 @@ using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
internal class RTPWriteStream : Stream | |||
internal class RTPWriteStream : AudioOutStream | |||
{ | |||
private readonly IAudioTarget _target; | |||
private readonly byte[] _nonce, _secretKey; | |||
@@ -14,10 +14,6 @@ namespace Discord.Audio | |||
protected readonly byte[] _buffer; | |||
public override bool CanRead => false; | |||
public override bool CanSeek => false; | |||
public override bool CanWrite => true; | |||
internal RTPWriteStream(IAudioTarget target, byte[] secretKey, int samplesPerFrame, uint ssrc) | |||
{ | |||
_target = target; | |||
@@ -40,6 +36,7 @@ namespace Discord.Audio | |||
} | |||
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | |||
{ | |||
cancellationToken.ThrowIfCancellationRequested(); | |||
unchecked | |||
{ | |||
if (_nonce[3]++ == byte.MaxValue) | |||
@@ -63,7 +60,16 @@ namespace Discord.Audio | |||
} | |||
public override async Task FlushAsync(CancellationToken cancellationToken) | |||
{ | |||
await _target.FlushAsync().ConfigureAwait(false); | |||
await _target.FlushAsync(cancellationToken).ConfigureAwait(false); | |||
} | |||
public override void Clear() | |||
{ | |||
ClearAsync(CancellationToken.None).GetAwaiter().GetResult(); | |||
} | |||
public override async Task ClearAsync(CancellationToken cancelToken) | |||
{ | |||
await _target.ClearAsync(cancelToken).ConfigureAwait(false); | |||
} | |||
public override long Length { get { throw new NotSupportedException(); } } | |||
@@ -87,17 +87,25 @@ namespace Discord.Audio | |||
Buffer.BlockCopy(data, 0, buffer, 0, count); | |||
_queuedFrames.Enqueue(new Frame(buffer, count)); | |||
} | |||
public async Task FlushAsync() | |||
public async Task FlushAsync(CancellationToken cancelToken) | |||
{ | |||
while (true) | |||
{ | |||
cancelToken.ThrowIfCancellationRequested(); | |||
if (_queuedFrames.Count == 0) | |||
return; | |||
await Task.Delay(250).ConfigureAwait(false); | |||
await Task.Delay(250, cancelToken).ConfigureAwait(false); | |||
} | |||
} | |||
public Task ClearAsync(CancellationToken cancelToken) | |||
{ | |||
Frame ignored; | |||
do | |||
cancelToken.ThrowIfCancellationRequested(); | |||
while (_queuedFrames.TryDequeue(out ignored)); | |||
return Task.Delay(0); | |||
} | |||
protected void Dispose(bool disposing) | |||
{ | |||
if (disposing) | |||
@@ -1,4 +1,5 @@ | |||
using System.Threading.Tasks; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
@@ -13,7 +14,9 @@ namespace Discord.Audio | |||
public Task SendAsync(byte[] buffer, int count) | |||
=> _client.SendAsync(buffer, count); | |||
public Task FlushAsync() | |||
public Task FlushAsync(CancellationToken cancelToken) | |||
=> Task.Delay(0); | |||
public Task ClearAsync(CancellationToken cancelToken) | |||
=> Task.Delay(0); | |||
} | |||
} |
@@ -1,10 +1,12 @@ | |||
using System.Threading.Tasks; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord.Audio | |||
{ | |||
internal interface IAudioTarget | |||
{ | |||
Task SendAsync(byte[] buffer, int count); | |||
Task FlushAsync(); | |||
Task FlushAsync(CancellationToken cancelToken); | |||
Task ClearAsync(CancellationToken cancelToken); | |||
} | |||
} |