Skip to content

Commit

Permalink
Use cross-plat IWavePlayer to eliminate loading times for sound playe…
Browse files Browse the repository at this point in the history
…rs (#34)
  • Loading branch information
jonko0493 authored Feb 14, 2023
1 parent 08462d3 commit 77ac229
Show file tree
Hide file tree
Showing 27 changed files with 938 additions and 107 deletions.
11 changes: 2 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,9 @@ The following prerequisites need to be installed in order to use Serial Loops:
- Using the Windows graphical installer, you can simply select the devkitARM (Nintendo DS) workloads
- On macOS and Linux, run `sudo skp-pacman -S nds-dev` from the terminal after installing the devkitPro pacman distribution.

Additionally, on Linux, the following are prerequisites to using the program:
* libvlc
* VLC
* libx11

On Ubuntu/Debian (which are the distros we test on), these can be installed in three commands:
Additionally, on Linux, you will need to install OpenAL. On Ubuntu/Debian (which are the distros we test on), it can be installed in a single command:
```
sudo apt install libvlc-dev
sudo apt install vlc
sudo apt install libx11
sudo apt install libopenal-dev
```

## Bugs
Expand Down
20 changes: 19 additions & 1 deletion SerialLoops.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SerialLoops.Tests", "test\S
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SerialLoops.Lib", "src\SerialLoops.Lib\SerialLoops.Lib.csproj", "{CC729174-57CD-499A-B6E7-1FA6F9CE255F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerialLoops.Gtk", "src\SerialLoops.Gtk\SerialLoops.Gtk.csproj", "{EA5F17A9-CA95-43A0-BE14-33F41EDF02BB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerialLoops.Mac", "src\SerialLoops.Mac\SerialLoops.Mac.csproj", "{2DF676C8-9917-48EA-A8CF-435CE29BB8A8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerialLoops.Wpf", "src\SerialLoops.Wpf\SerialLoops.Wpf.csproj", "{2DF0B35B-5A1C-43C7-A6BD-95D4D48FE39A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -27,12 +33,24 @@ Global
{CC729174-57CD-499A-B6E7-1FA6F9CE255F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC729174-57CD-499A-B6E7-1FA6F9CE255F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC729174-57CD-499A-B6E7-1FA6F9CE255F}.Release|Any CPU.Build.0 = Release|Any CPU
{EA5F17A9-CA95-43A0-BE14-33F41EDF02BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA5F17A9-CA95-43A0-BE14-33F41EDF02BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA5F17A9-CA95-43A0-BE14-33F41EDF02BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA5F17A9-CA95-43A0-BE14-33F41EDF02BB}.Release|Any CPU.Build.0 = Release|Any CPU
{2DF676C8-9917-48EA-A8CF-435CE29BB8A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2DF676C8-9917-48EA-A8CF-435CE29BB8A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2DF676C8-9917-48EA-A8CF-435CE29BB8A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2DF676C8-9917-48EA-A8CF-435CE29BB8A8}.Release|Any CPU.Build.0 = Release|Any CPU
{2DF0B35B-5A1C-43C7-A6BD-95D4D48FE39A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2DF0B35B-5A1C-43C7-A6BD-95D4D48FE39A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2DF0B35B-5A1C-43C7-A6BD-95D4D48FE39A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2DF0B35B-5A1C-43C7-A6BD-95D4D48FE39A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {922A1BAC-5F5E-4225-8E10-69036C42B786}
SolutionGuid = {A7259B55-BEEA-4ECC-AC0A-85CC8FA1BD53}
SolutionGuid = {922A1BAC-5F5E-4225-8E10-69036C42B786}
EndGlobalSection
EndGlobal
5 changes: 4 additions & 1 deletion azure-pipelines-official.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ jobs:
matrix:
Linux:
imageName: 'ubuntu-latest'
platformName: 'Gtk'
macOS:
imageName: 'macOS-latest'
platformName: 'Mac'
Windows:
imageName: 'windows-latest'
platformName: 'Wpf'
displayName: Build & Publish
pool:
vmImage: $(imageName)
Expand All @@ -24,7 +27,7 @@ jobs:
- task: DotNetCoreCLI@2
inputs:
command: 'publish'
projects: $(Build.SourcesDirectory)/src/SerialLoops/SerialLoops.csproj
projects: $(Build.SourcesDirectory)/src/SerialLoops.$(platformName)/SerialLoops.$(platformName).csproj
arguments: '-c Release -o $(Build.ArtifactStagingDirectory)'
publishWebProjects: false
displayName: Build & Publish Serial Loops
Expand Down
5 changes: 4 additions & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ jobs:
matrix:
Linux:
imageName: 'ubuntu-latest'
platformName: 'Gtk'
macOS:
imageName: 'macOS-latest'
platformName: 'Mac'
Windows:
imageName: 'windows-latest'
platformName: 'Wpf'
displayName: Build & Test
pool:
vmImage: $(imageName)
Expand All @@ -23,7 +26,7 @@ jobs:
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: $(Build.SourcesDirectory)/SerialLoops.sln
projects: $(Build.SourcesDirectory)/src/SerialLoops.$(platformName)/SerialLoops.$(platformName).csproj
displayName: Build solution

- task: DotNetCoreCLI@2
Expand Down
286 changes: 286 additions & 0 deletions src/SerialLoops.Gtk/ALWavePlayer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
using NAudio.Wave;
using OpenTK.Audio.OpenAL;
using System;
using System.Threading.Tasks;

namespace SerialLoops.Gtk
{
// Adapted from https://gist.github.com/VisualMelon/d02edcd7c44fadcd6f5745e92c449a90
public class ALAudioContext : IDisposable
{
public ALDevice Device { get; private set; }
public ALContext Context { get; private set; }

public ALAudioContext()
{
Init();
}

private unsafe void Init()
{
Device = ALC.OpenDevice(null);
Context = ALC.CreateContext(Device, (int*)null);
ALC.MakeContextCurrent(Context);
}

~ALAudioContext()
{
Dispose(false);
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected void Dispose(bool disposing)
{
if (Context != ALContext.Null)
{
ALC.MakeContextCurrent(ALContext.Null);
ALC.DestroyContext(Context);
}
Context = ALContext.Null;

if (Device != IntPtr.Zero)
{
ALC.CloseDevice(Device);
}
Device = ALDevice.Null;
}
}

public class ALWavePlayer : IWavePlayer, IDisposable
{
private float _volume;
public float Volume
{
get => _volume;
set
{
_volume = value;
}
}

public PlaybackState PlaybackState { get; private set; }

public event EventHandler<StoppedEventArgs> PlaybackStopped;

private ALAudioContext Context { get; }
public int BufferSize { get; }

private IWaveProvider WaveProvider;

private int _source;
private int _nextBuffer;
private int _otherBuffer;

private byte[] _buffer;
private Accumulator _accumulator;

private System.Threading.ManualResetEventSlim _signaller;

private System.Threading.CancellationTokenSource _playerCanceller;
private Task Player;

public WaveFormat OutputWaveFormat => WaveProvider.WaveFormat;

public ALWavePlayer(ALAudioContext context, int bufferSize)
{
Context = context;
BufferSize = bufferSize;
}

public unsafe void Init(IWaveProvider waveProvider)
{
WaveProvider = waveProvider;

AL.GenSources(1, ref _source);
AL.GenBuffers(1, ref _nextBuffer);
AL.GenBuffers(1, ref _otherBuffer);

_buffer = new byte[BufferSize];
_accumulator = new Accumulator(waveProvider, _buffer);

_signaller = new System.Threading.ManualResetEventSlim(false);
PlaybackState = PlaybackState.Paused;
}

public void Pause()
{
if (PlaybackState == PlaybackState.Stopped)
throw new InvalidOperationException("Stopped");

PlaybackState = PlaybackState.Paused;
_playerCanceller?.Cancel();
_playerCanceller = null;
AL.SourcePause(_source);
}

public void Play()
{
if (PlaybackState == PlaybackState.Stopped)
throw new InvalidOperationException("Stopped");

PlaybackState = PlaybackState.Playing;
if (_playerCanceller == null)
{
_playerCanceller = new System.Threading.CancellationTokenSource();
Player = PlayLoop(_playerCanceller.Token).ContinueWith(PlayerStopped);
}
}

private void PlayerStopped(Task t)
{
PlaybackStopped?.Invoke(this, new StoppedEventArgs(t?.Exception));
}

public void Stop()
{
if (PlaybackState == PlaybackState.Stopped)
{
throw new InvalidOperationException("Already stopped");
}

PlaybackState = PlaybackState.Stopped;
if (_playerCanceller != null)
{
_playerCanceller?.Cancel();
_playerCanceller = null;
AL.SourceStop(_source);
}
else
{
PlaybackStopped?.Invoke(this, new StoppedEventArgs());
}
}

private async Task PlayLoop(System.Threading.CancellationToken ct)
{
AL.SourcePlay(_source);
await Task.Yield();

while (true)
{
AL.GetSource(_source, ALGetSourcei.BuffersQueued, out int queued);
AL.GetSource(_source, ALGetSourcei.BuffersProcessed, out int processed);
AL.GetSource(_source, ALGetSourcei.SourceState, out int state);

if ((ALSourceState)state != ALSourceState.Playing)
{
AL.SourcePlay(_source);
}

if (processed == 0 && queued == 2)
{
await Task.Delay(1);
continue;
}

if (processed > 0)
{
AL.SourceUnqueueBuffers(_source, processed);
}

var notFinished = await _accumulator.Accumulate(_buffer, ct);

if (!notFinished)
{
return;
}

AL.BufferData(_nextBuffer, TranslateFormat(WaveProvider.WaveFormat), _buffer, WaveProvider.WaveFormat.SampleRate);
AL.SourceQueueBuffer(_source, _nextBuffer);
await Task.Delay(10, ct);

(_nextBuffer, _otherBuffer) = (_otherBuffer, _nextBuffer);
}
}

~ALWavePlayer()
{
Dispose(false);
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected void Dispose(bool disposing)
{
AL.DeleteSource(_source);
AL.DeleteBuffer(_nextBuffer);
AL.DeleteBuffer(_otherBuffer);
}

public static ALFormat TranslateFormat(WaveFormat format)
{
if (format.Channels == 2)
{
if (format.BitsPerSample == 32)
{
return ALFormat.StereoFloat32Ext;
}
else if (format.BitsPerSample == 16)
{
return ALFormat.Stereo16;
}
else if (format.BitsPerSample == 8)
{
return ALFormat.Stereo8;
}
}
else if (format.Channels == 1)
{
if (format.BitsPerSample == 32)
{
return ALFormat.MonoFloat32Ext;
}
else if (format.BitsPerSample == 16)
{
return ALFormat.Mono16;
}
else if (format.BitsPerSample == 8)
{
return ALFormat.Mono8;
}
}

throw new FormatException("Cannot translate WaveFormat.");
}
}

public class Accumulator
{
public Accumulator(IWaveProvider provider, byte[] buffer)
{
Provider = provider ?? throw new ArgumentNullException(nameof(provider));
}

public IWaveProvider Provider { get; }
//private object Locker = new();

public async Task<bool> Accumulate(byte[] buffer, System.Threading.CancellationToken ct)
{
await Task.Yield();

int position = 0;
while (position < buffer.Length)
{
if (ct.IsCancellationRequested)
throw new TaskCanceledException();
var read = Provider.Read(buffer, position, buffer.Length - position);

if (read == 0)
return false;

position += read;
}

return true;
}
}
}
Loading

0 comments on commit 77ac229

Please sign in to comment.