| @@ -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); | |||
| } | |||
| } | |||