| @@ -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> | /// </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="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> | /// <returns></returns> | ||||
| Stream CreateOpusStream(int samplesPerFrame, int bufferMillis = 1000); | |||||
| AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis = 1000); | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. | /// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. | ||||
| /// </summary> | /// </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="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> | /// <returns></returns> | ||||
| Stream CreateDirectOpusStream(int samplesPerFrame); | |||||
| AudioOutStream CreateDirectOpusStream(int samplesPerFrame); | |||||
| /// <summary> | /// <summary> | ||||
| /// Creates a new outgoing stream accepting PCM (raw) data. | /// Creates a new outgoing stream accepting PCM (raw) data. | ||||
| /// </summary> | /// </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="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> | /// <param name="bitrate"></param> | ||||
| /// <returns></returns> | /// <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> | /// <summary> | ||||
| /// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. | /// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. | ||||
| /// </summary> | /// </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="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> | /// <param name="bitrate"></param> | ||||
| /// <returns></returns> | /// <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); | 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); | CheckSamplesPerFrame(samplesPerFrame); | ||||
| var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token); | var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token); | ||||
| return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc); | return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc); | ||||
| } | } | ||||
| public Stream CreateDirectOpusStream(int samplesPerFrame) | |||||
| public AudioOutStream CreateDirectOpusStream(int samplesPerFrame) | |||||
| { | { | ||||
| CheckSamplesPerFrame(samplesPerFrame); | CheckSamplesPerFrame(samplesPerFrame); | ||||
| var target = new DirectAudioTarget(ApiClient); | var target = new DirectAudioTarget(ApiClient); | ||||
| return new RTPWriteStream(target, _secretKey, samplesPerFrame, _ssrc); | 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); | CheckSamplesPerFrame(samplesPerFrame); | ||||
| var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token); | var target = new BufferedAudioTarget(ApiClient, samplesPerFrame, bufferMillis, _cancelTokenSource.Token); | ||||
| return new OpusEncodeStream(target, _secretKey, channels, samplesPerFrame, _ssrc, bitrate); | 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); | CheckSamplesPerFrame(samplesPerFrame); | ||||
| var target = new DirectAudioTarget(ApiClient); | var target = new DirectAudioTarget(ApiClient); | ||||
| @@ -5,7 +5,7 @@ using System.Threading.Tasks; | |||||
| namespace Discord.Audio | namespace Discord.Audio | ||||
| { | { | ||||
| internal class RTPWriteStream : Stream | |||||
| internal class RTPWriteStream : AudioOutStream | |||||
| { | { | ||||
| private readonly IAudioTarget _target; | private readonly IAudioTarget _target; | ||||
| private readonly byte[] _nonce, _secretKey; | private readonly byte[] _nonce, _secretKey; | ||||
| @@ -14,10 +14,6 @@ namespace Discord.Audio | |||||
| protected readonly byte[] _buffer; | 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) | internal RTPWriteStream(IAudioTarget target, byte[] secretKey, int samplesPerFrame, uint ssrc) | ||||
| { | { | ||||
| _target = target; | _target = target; | ||||
| @@ -40,6 +36,7 @@ namespace Discord.Audio | |||||
| } | } | ||||
| public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | ||||
| { | { | ||||
| cancellationToken.ThrowIfCancellationRequested(); | |||||
| unchecked | unchecked | ||||
| { | { | ||||
| if (_nonce[3]++ == byte.MaxValue) | if (_nonce[3]++ == byte.MaxValue) | ||||
| @@ -63,7 +60,16 @@ namespace Discord.Audio | |||||
| } | } | ||||
| public override async Task FlushAsync(CancellationToken cancellationToken) | 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(); } } | public override long Length { get { throw new NotSupportedException(); } } | ||||
| @@ -87,17 +87,25 @@ namespace Discord.Audio | |||||
| Buffer.BlockCopy(data, 0, buffer, 0, count); | Buffer.BlockCopy(data, 0, buffer, 0, count); | ||||
| _queuedFrames.Enqueue(new Frame(buffer, count)); | _queuedFrames.Enqueue(new Frame(buffer, count)); | ||||
| } | } | ||||
| public async Task FlushAsync() | |||||
| public async Task FlushAsync(CancellationToken cancelToken) | |||||
| { | { | ||||
| while (true) | while (true) | ||||
| { | { | ||||
| cancelToken.ThrowIfCancellationRequested(); | |||||
| if (_queuedFrames.Count == 0) | if (_queuedFrames.Count == 0) | ||||
| return; | 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) | protected void Dispose(bool disposing) | ||||
| { | { | ||||
| if (disposing) | if (disposing) | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System.Threading.Tasks; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Audio | namespace Discord.Audio | ||||
| { | { | ||||
| @@ -13,7 +14,9 @@ namespace Discord.Audio | |||||
| public Task SendAsync(byte[] buffer, int count) | public Task SendAsync(byte[] buffer, int count) | ||||
| => _client.SendAsync(buffer, count); | => _client.SendAsync(buffer, count); | ||||
| public Task FlushAsync() | |||||
| public Task FlushAsync(CancellationToken cancelToken) | |||||
| => Task.Delay(0); | |||||
| public Task ClearAsync(CancellationToken cancelToken) | |||||
| => Task.Delay(0); | => Task.Delay(0); | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,10 +1,12 @@ | |||||
| using System.Threading.Tasks; | |||||
| using System.Threading; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Audio | namespace Discord.Audio | ||||
| { | { | ||||
| internal interface IAudioTarget | internal interface IAudioTarget | ||||
| { | { | ||||
| Task SendAsync(byte[] buffer, int count); | Task SendAsync(byte[] buffer, int count); | ||||
| Task FlushAsync(); | |||||
| Task FlushAsync(CancellationToken cancelToken); | |||||
| Task ClearAsync(CancellationToken cancelToken); | |||||
| } | } | ||||
| } | } | ||||