Skip to content

Commit

Permalink
Portaudio (#10)
Browse files Browse the repository at this point in the history
* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* asddasd

* WIP

* WIp

* WIP

---------

Co-authored-by: xBaank <[email protected]>
  • Loading branch information
xBaank and xBaank authored Aug 12, 2024
1 parent e63c5ca commit 33a37d8
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 235 deletions.
174 changes: 78 additions & 96 deletions Console/Audio/AudioSender.cs
Original file line number Diff line number Diff line change
@@ -1,134 +1,116 @@
using System.Threading.Channels;
using OpenTK.Audio.OpenAL;
using PortAudioSharp;

namespace Console.Audio;

internal class AudioSender(int sourceId, ALFormat targetFormat) : IAsyncDisposable
internal class AudioSender(float volume, PlayState initialState) : IAsyncDisposable
{
private readonly Channel<PcmPacket> _queue = Channel.CreateBounded<PcmPacket>(150);
private readonly Channel<PcmPacket<short>> _queue = Channel.CreateBounded<PcmPacket<short>>(
500
);
public readonly int SampleRate = 48000;
public readonly int Channels = 2;
private readonly int[] _buffers = AL.GenBuffers(50);
private bool _clearBuffer = false;
public PlayState State { get; set; } = initialState;
public float Volume { get; set; } = volume;
public TimeSpan CurrentTime { get; private set; } = default;
public TaskCompletionSource WaitForEmptyBuffer { get; set; } = new();
private PortAudioSharp.Stream? _stream;

public void ClearBuffer()
{
_clearBuffer = true;
while (_queue.Reader.TryRead(out var next))
{
next.Dispose();
}
}

public async ValueTask Add(PcmPacket data) => await _queue.Writer.WriteAsync(data);
public async ValueTask Add(PcmPacket<short> data) => await _queue.Writer.WriteAsync(data);

private async ValueTask ClearBufferAL(CancellationToken token)
public unsafe void StartSending(CancellationToken token = default)
{
AL.GetSource(sourceId, ALGetSourcei.SourceState, out int initialState);
PortAudio.Initialize();

// Define a callback delegate for audio processing
StreamCallbackResult callback(
IntPtr input,
IntPtr output,
UInt32 frameCount,
ref StreamCallbackTimeInfo timeInfo,
StreamCallbackFlags statusFlags,
IntPtr userData
)
{
var sizeInBytes = (int)frameCount * 2;

AL.SourceStop(sourceId);
AL.GetSource(sourceId, ALGetSourcei.BuffersQueued, out int queuedCount);
if (token.IsCancellationRequested)
return StreamCallbackResult.Abort;

if (queuedCount > 0)
{
int[] bufferIds = new int[queuedCount];
AL.SourceUnqueueBuffers(sourceId, queuedCount, bufferIds);
foreach (var buffer in bufferIds)
if (State == PlayState.Paused)
{
using var next = await _queue.Reader.ReadAsync(token);
AL.BufferData(buffer, targetFormat, next.Data, SampleRate);
AL.SourceQueueBuffer(sourceId, buffer);
var spanUnmanagedBuffer = new Span<short>(output.ToPointer(), sizeInBytes);
spanUnmanagedBuffer.Clear();
return StreamCallbackResult.Continue;
}
}

_clearBuffer = false;

if ((ALSourceState)initialState == ALSourceState.Playing)
{
AL.SourcePlay(sourceId);
}
if (State == PlayState.Stopped)
return StreamCallbackResult.Abort;

if ((ALSourceState)initialState == ALSourceState.Paused)
{
AL.SourcePlay(sourceId);
AL.SourcePause(sourceId);
}
}
if (_queue.Reader.TryRead(out var nextBuffer))
{
using var buffer = nextBuffer;
var spanUnmanagedBuffer = new Span<short>(output.ToPointer(), sizeInBytes);
var source = buffer.Data[..sizeInBytes];

public async Task StartSending(CancellationToken token = default)
{
var fillBuffers = await _queue
.Reader.ReadAllAsync(token)
.Take(10)
.ToListAsync(cancellationToken: token);
for (var i = 0; i < source.Length; i++)
{
source[i] = (short)(source[i] * Volume);
}

for (int i = 0; i < fillBuffers.Count; i++)
{
using var item = fillBuffers[i];
AL.BufferData(_buffers[i], targetFormat, item.Data, SampleRate);
AL.SourceQueueBuffer(sourceId, _buffers[i]);
source.CopyTo(spanUnmanagedBuffer);
CurrentTime = buffer.Time;
return StreamCallbackResult.Continue;
}
else
{
WaitForEmptyBuffer.TrySetResult();
var spanUnmanagedBuffer = new Span<short>(output.ToPointer(), sizeInBytes);
spanUnmanagedBuffer.Clear();
return StreamCallbackResult.Continue;
}
}

var _ = Task.Run(
async () =>
{
try
{
while (!token.IsCancellationRequested)
{
if (_clearBuffer)
{
await ClearBufferAL(token);
continue;
}

AL.GetSource(
sourceId,
ALGetSourcei.BuffersProcessed,
out int releasedCount
);

if (releasedCount > 0)
{
int[] bufferIds = new int[releasedCount];
AL.SourceUnqueueBuffers(sourceId, releasedCount, bufferIds);
foreach (var buffer in bufferIds)
{
using var next = await _queue.Reader.ReadAsync(token);
AL.BufferData(buffer, targetFormat, next.Data, SampleRate);
AL.SourceQueueBuffer(sourceId, buffer);
}
}

AL.GetSource(sourceId, ALGetSourcei.SourceState, out int stateInt);

if ((ALSourceState)stateInt == ALSourceState.Stopped)
{
AL.SourcePlay(sourceId);
}

await Task.Delay(100);
}
}
finally
{
await ClearBufferAL(token);
}
},
token
StreamParameters param = new();
var deviceIndex = PortAudio.DefaultOutputDevice;
var info = PortAudio.GetDeviceInfo(deviceIndex);
param.device = PortAudio.DefaultOutputDevice;
param.channelCount = Channels;
param.sampleFormat = SampleFormat.Int16;
param.suggestedLatency = info.defaultLowOutputLatency;
param.hostApiSpecificStreamInfo = IntPtr.Zero;

_stream = new PortAudioSharp.Stream(
inParams: null,
outParams: param,
streamFlags: StreamFlags.ClipOff,
sampleRate: SampleRate,
framesPerBuffer: 960, //TODO This should not be hardcoded maybe?
callback: callback,
userData: IntPtr.Zero
);

AL.SourcePlay(sourceId);
_stream.Start();
}

public ValueTask DisposeAsync()
{
ClearBuffer();
AL.SourceStop(sourceId);
AL.GetSource(sourceId, ALGetSourcei.BuffersProcessed, out int releasedCount);
int[] bufferIds = new int[releasedCount];
AL.SourceUnqueueBuffers(sourceId, releasedCount, bufferIds);
AL.DeleteBuffers(bufferIds);
State = PlayState.Stopped;
try
{
_stream?.Stop();
_stream?.Dispose();
}
catch { }
return ValueTask.CompletedTask;
}
}
39 changes: 6 additions & 33 deletions Console/Audio/Containers/Matroska/Matroska.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ public void Dispose()
_ebmlReader.Dispose();
_inputStream.Dispose();
_memoryOwner.Dispose();
CurrentTime = default;
TotalTime = default;
}

Expand All @@ -52,11 +51,9 @@ public async ValueTask DisposeAsync()
await _ebmlReader.DisposeAsync();
await _inputStream.DisposeAsync();
_memoryOwner.Dispose();
CurrentTime = default;
TotalTime = default;
}

public TimeSpan CurrentTime { get; private set; } = TimeSpan.Zero;
public TimeSpan TotalTime { get; private set; }

public async Task AddFrames(CancellationToken cancellationToken)
Expand Down Expand Up @@ -154,40 +151,16 @@ await WriteBlock(
}
}

private static void ShortsToBytes(ReadOnlySpan<short> input, Span<byte> output)
{
for (int i = 0; i < input.Length; i++)
{
output[i * 2] = (byte)input[i];
output[i * 2 + 1] = (byte)(input[i] >> 8);
}
}

private async ValueTask AddOpusPacket(ReadOnlyMemory<byte> data)
private async ValueTask AddOpusPacket(ReadOnlyMemory<byte> data, TimeSpan time)
{
var frames = OpusPacketInfo.GetNumFrames(data.Span);
var samplePerFrame = OpusPacketInfo.GetNumSamplesPerFrame(data.Span, _sender.SampleRate);
var frameSize = frames * samplePerFrame;
var pcmSize = frameSize * _sender.Channels;

var pcm = ArrayPool<short>.Shared.Rent(pcmSize);
var pcmBytes = ArrayPool<byte>.Shared.Rent(pcmSize * 2);

try
{
_decoder.Decode(data.Span, pcm.AsSpan()[..pcmSize], frameSize);
ShortsToBytes(pcm.AsSpan()[..pcmSize], pcmBytes.AsSpan()[..(pcmSize * 2)]);
await _sender.Add(new PcmPacket(pcmBytes, pcmSize * 2));
}
catch
{
ArrayPool<byte>.Shared.Return(pcmBytes);
throw;
}
finally
{
ArrayPool<short>.Shared.Return(pcm);
}
_decoder.Decode(data.Span, pcm.AsSpan()[..pcmSize], frameSize);
await _sender.Add(new PcmPacket<short>(pcm, pcmSize, time));
}

private async ValueTask WriteBlock(
Expand All @@ -209,13 +182,13 @@ CancellationToken cancellationToken
var memory = _memoryOwner.Memory[..size];

var block = await _ebmlReader.GetSimpleBlock(memory, cancellationToken);
CurrentTime = time + TimeSpan.FromMilliseconds(block.Timestamp);
var currentTime = time + TimeSpan.FromMilliseconds(block.Timestamp);

if (CurrentTime.TotalMilliseconds < _seekTime)
if (currentTime.TotalMilliseconds < _seekTime)
return;

foreach (var frame in block.GetFrames())
await AddOpusPacket(frame);
await AddOpusPacket(frame, currentTime);

return;
}
Expand Down
10 changes: 6 additions & 4 deletions Console/Audio/PcmPacket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

namespace Console.Audio;

internal struct PcmPacket(byte[] Data, int Lenght) : IDisposable
internal readonly struct PcmPacket<T>(T[] Data, int Lenght, TimeSpan Time) : IDisposable
where T : struct
{
private readonly byte[] _data = Data;
public readonly ReadOnlySpan<byte> Data => _data.AsSpan()[..Lenght];
private readonly T[] _data = Data;
public readonly Span<T> Data => _data.AsSpan()[..Lenght];
public int Lenght { get; } = Lenght;
public TimeSpan Time { get; } = Time;

public readonly void Dispose() => ArrayPool<byte>.Shared.Return(_data);
public readonly void Dispose() => ArrayPool<T>.Shared.Return(_data);
}
8 changes: 8 additions & 0 deletions Console/Audio/PlayState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Console.Audio;

public enum PlayState
{
Playing,
Paused,
Stopped
}
Loading

0 comments on commit 33a37d8

Please sign in to comment.