Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance #4

Merged
merged 31 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,19 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: PulseAudio
run: sudo apt-get install pulseaudio && pulseaudio --start
- name: Null sink
run: pactl load-module module-null-sink sink_name=DummySink
- name: Set up PulseAudio
run: |
sudo apt-get update
sudo apt-get install -y pulseaudio
- name: Start PulseAudio
run: |
pulseaudio --start
- name: Create default PulseAudio virtual sink
run: |
pactl load-module module-null-sink sink_name=VirtualSink sink_properties=device.description=Virtual_Sink
- name: Set default sink to VirtualSink
run: |
pactl set-default-sink VirtualSink
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand Down
21 changes: 12 additions & 9 deletions Console/Audio/AudioSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Console.Audio;

internal class AudioSender(int sourceId, ALFormat targetFormat) : IAsyncDisposable
{
private readonly Channel<byte[]> _queue = Channel.CreateBounded<byte[]>(50);
private readonly Channel<PcmPacket> _queue = Channel.CreateBounded<PcmPacket>(50);
public readonly int SampleRate = 48000;
public readonly int Channels = 2;
private readonly int[] _buffers = AL.GenBuffers(50);
Expand All @@ -14,10 +14,13 @@ internal class AudioSender(int sourceId, ALFormat targetFormat) : IAsyncDisposab
public void ClearBuffer()
{
_clearBuffer = true;
while (_queue.Reader.TryRead(out var _)) { }
while (_queue.Reader.TryRead(out var next))
{
next.Dispose();
}
}

public async ValueTask Add(byte[] data) => await _queue.Writer.WriteAsync(data);
public async ValueTask Add(PcmPacket data) => await _queue.Writer.WriteAsync(data);

private async ValueTask ClearBufferAL(CancellationToken token)
{
Expand All @@ -32,8 +35,8 @@ private async ValueTask ClearBufferAL(CancellationToken token)
AL.SourceUnqueueBuffers(sourceId, queuedCount, bufferIds);
foreach (var buffer in bufferIds)
{
var next = await _queue.Reader.ReadAsync(token);
AL.BufferData(buffer, targetFormat, next, SampleRate);
using var next = await _queue.Reader.ReadAsync(token);
AL.BufferData(buffer, targetFormat, next.Data, SampleRate);
AL.SourceQueueBuffer(sourceId, buffer);
}
}
Expand Down Expand Up @@ -61,8 +64,8 @@ public async Task StartSending(CancellationToken token = default)

for (int i = 0; i < fillBuffers.Count; i++)
{
var item = fillBuffers[i];
AL.BufferData(_buffers[i], targetFormat, item, SampleRate);
using var item = fillBuffers[i];
AL.BufferData(_buffers[i], targetFormat, item.Data, SampleRate);
AL.SourceQueueBuffer(sourceId, _buffers[i]);
}

Expand Down Expand Up @@ -91,8 +94,8 @@ out int releasedCount
AL.SourceUnqueueBuffers(sourceId, releasedCount, bufferIds);
foreach (var buffer in bufferIds)
{
var next = await _queue.Reader.ReadAsync(token);
AL.BufferData(buffer, targetFormat, next, SampleRate);
using var next = await _queue.Reader.ReadAsync(token);
AL.BufferData(buffer, targetFormat, next.Data, SampleRate);
AL.SourceQueueBuffer(sourceId, buffer);
}
}
Expand Down
19 changes: 17 additions & 2 deletions Console/Audio/Containers/Matroska/HttpSegmentedStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ internal sealed class HttpSegmentedStream : Stream
private readonly HttpClient _httpClient;
private Stream? _httpStream;
private bool _positionChanged;
private long _chunkPosition;

private HttpSegmentedStream(
IDownloadUrlHandler downloadUrlHandler,
Expand Down Expand Up @@ -67,11 +68,21 @@ public override async ValueTask<int> ReadAsync(
CancellationToken cancellationToken = default
)
{
if (_httpStream is null || _positionChanged)
if (
_httpStream is null
|| _positionChanged
&& Position < _chunkPosition
&& Position > (_chunkPosition + _httpStream?.Length)
)
{
await ReadNextChunk(cancellationToken);
_positionChanged = false;
}
else if (_positionChanged)
{
_httpStream?.Seek(Math.Abs(Position - _chunkPosition), SeekOrigin.Begin);
_positionChanged = false;
}

int bytesLeftToRead;
var totalRead = 0;
Expand Down Expand Up @@ -153,13 +164,17 @@ private async Task ReadNextChunk(CancellationToken cancellationToken)
if (_httpStream is not null)
await _httpStream.DisposeAsync();

_httpStream = await _httpClient.GetStreamAsync(
var response = await _httpClient.GetAsync(
AppendRangeToUrl(
await _downloadUrlHandler.GetUrl(),
Position,
Position + BufferSize - 1
),
HttpCompletionOption.ResponseContentRead,
cancellationToken
);

_httpStream = await response.Content.ReadAsStreamAsync(cancellationToken);
_chunkPosition = Position;
}
}
55 changes: 34 additions & 21 deletions Console/Audio/Containers/Matroska/Matroska.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@
using Console.Audio.Containers.Matroska.Types;
using Console.Audio.DownloadHandlers;
using Console.Extensions;
using Nito.Disposables;

namespace Console.Audio.Containers.Matroska;

using static ElementTypes;

internal class Matroska
internal class Matroska : IDisposable, IAsyncDisposable
{
private readonly EbmlReader _ebmlReader;
private readonly AudioSender _sender;
public readonly Stream InputStream;
private readonly Stream _inputStream;
private readonly IOpusDecoder _decoder;

private List<AudioTrack>? _audioTracks;
private List<CuePoint>? _cuePoints;
private IMemoryOwner<byte> _memoryOwner = MemoryPool<byte>.Shared.Rent(1024);
Expand All @@ -32,7 +32,7 @@ internal class Matroska

private Matroska(Stream stream, AudioSender sender)
{
InputStream = stream;
_inputStream = stream;
_ebmlReader = new EbmlReader(stream);
_sender = sender;
_decoder = OpusCodecFactory.CreateDecoder(_sender.SampleRate, _sender.Channels);
Expand All @@ -42,7 +42,7 @@ private Matroska(Stream stream, AudioSender sender)
public void Dispose()
{
_ebmlReader.Dispose();
InputStream.Dispose();
_inputStream.Dispose();
_memoryOwner.Dispose();
CurrentTime = default;
TotalTime = default;
Expand All @@ -51,7 +51,7 @@ public void Dispose()
public async ValueTask DisposeAsync()
{
await _ebmlReader.DisposeAsync();
await InputStream.DisposeAsync();
await _inputStream.DisposeAsync();
_memoryOwner.Dispose();
CurrentTime = default;
TotalTime = default;
Expand Down Expand Up @@ -141,27 +141,40 @@ await WriteBlock(
}
}

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

return processedValues;
}

private async ValueTask AddOpusPacket(byte[] data)
private async ValueTask AddOpusPacket(ReadOnlyMemory<byte> data)
{
var frames = OpusPacketInfo.GetNumFrames(data);
var samplePerFrame = OpusPacketInfo.GetNumSamplesPerFrame(data, _sender.SampleRate);
var frames = OpusPacketInfo.GetNumFrames(data.Span);
var samplePerFrame = OpusPacketInfo.GetNumSamplesPerFrame(data.Span, _sender.SampleRate);
var frameSize = frames * samplePerFrame;
short[] pcm = new short[frameSize * _sender.Channels];
_decoder.Decode(data, pcm, frameSize);
var result = ShortsToBytes(pcm, 0, pcm.Length);
await _sender.Add(result);
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);
}
}

private async ValueTask WriteBlock(
Expand Down Expand Up @@ -189,7 +202,7 @@ CancellationToken cancellationToken
return;

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

return;
}
Expand Down
17 changes: 17 additions & 0 deletions Console/Audio/PcmPacket.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Console.Audio;

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

public readonly void Dispose() => ArrayPool<byte>.Shared.Return(_data);
}
28 changes: 15 additions & 13 deletions Console/Audio/PlayerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,19 @@ public int Volume
public ALSourceState? State => SourceState();
public IVideo? Song => _queue.ElementAtOrDefault(_currentSongIndex);
public IReadOnlyCollection<IVideo> Songs => _queue;
public LoopState LoopState { get; private set; }
public LoopState LoopState { get; set; }

public PlayerController()
{
_device = ALC.OpenDevice("");
_device = ALC.OpenDevice(null);
_context = ALC.CreateContext(_device, new ALContextAttributes());
ALC.MakeContextCurrent(_context);

var error = ALC.GetError(_device);
// Check for any errors
if (ALC.GetError(_device) != AlcError.NoError)
if (error != AlcError.NoError)
{
MessageBox.ErrorQuery("Error", "An error ocurred ", "Ok");
return;
throw new Exception($"Error code: {error}");
}

_sourceId = AL.GenSource();
Expand Down Expand Up @@ -128,6 +128,11 @@ public async Task SetAsync(ISearchResult item)
_currentSongTokenSource.Cancel();
_currentSongIndex = 0;

AL.SourceStop(_sourceId);
_audioSender?.ClearBuffer();
_currentSongTokenSource.Cancel();
_currentSongIndex = 0;

if (item is VideoSearchResult videoSearchResult)
{
_queue = [videoSearchResult];
Expand Down Expand Up @@ -179,6 +184,9 @@ public async Task PlayAsync()
return;
}

if (_audioSender is not null)
await _audioSender.DisposeAsync();

if (_matroskaPlayerBuffer is not null)
await _matroskaPlayerBuffer.DisposeAsync();

Expand All @@ -193,11 +201,11 @@ public async Task PlayAsync()
_audioSender,
_currentSongTokenSource.Token
);

_matroskaPlayerBuffer.OnFinish += async () =>
{
_currentSongTokenSource.Cancel();
await _audioSender.DisposeAsync();

OnFinish?.Invoke();
};

Expand Down Expand Up @@ -232,8 +240,8 @@ public async Task SkipAsync(bool bypassLoop = false)
if (LoopState == LoopState.ALL && _currentSongIndex >= _queue.Count)
_currentSongIndex = 0;

AL.SourceStop(_sourceId);
_audioSender?.ClearBuffer();
AL.SourceStop(_sourceId);
}
}

Expand All @@ -250,12 +258,6 @@ public async Task GoBackAsync()
}
}

public async Task SetLoop(LoopState newState)
{
using var _ = await _lock.LockAsync();
LoopState = newState;
}

public async Task PauseAsync()
{
using var _ = await _lock.LockAsync();
Expand Down
6 changes: 3 additions & 3 deletions Console/Views/PlayerView.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Console.Audio;
using Console.Audio;
using Console.Extensions;
using OpenTK.Audio.OpenAL;
using Terminal.Gui;
Expand Down Expand Up @@ -172,7 +172,7 @@ async Task NextSong(bool bypassLoop)
}
};

loopButton.Accept += async (_, args) =>
loopButton.Accept += (_, args) =>
{
LoopState nextState = player.LoopState switch
{
Expand All @@ -182,7 +182,7 @@ async Task NextSong(bool bypassLoop)
_ => throw new InvalidOperationException("Unknown loop state")
};

await player.SetLoop(nextState);
player.LoopState = nextState;

loopButton.Text = player.LoopState switch
{
Expand Down