From e1661d1a78627a1bbb943dccb4d30abdd1016a97 Mon Sep 17 00:00:00 2001 From: Justin Unterreiner Date: Sun, 12 Jan 2025 01:13:27 -0800 Subject: [PATCH] Implement audio downsampling and audio queue batching --- emulator/Emulator.cs | 3 +- emulator/Platform/Platform.cs | 77 +++++++++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/emulator/Emulator.cs b/emulator/Emulator.cs index 4250a72..70f719d 100644 --- a/emulator/Emulator.cs +++ b/emulator/Emulator.cs @@ -186,8 +186,7 @@ private static void PacManPCB_OnRender(RenderEventArgs eventArgs) */ private static void PacManPCB_OnAudioSample(AudioSampleEventArgs eventArgs) { - foreach (var sample in eventArgs.Samples) - _platform.QueueAudioSamples(sample); + _platform.QueueAudioSamples(eventArgs.Samples); } private static void PacManPCB_OnBreakpointHit() diff --git a/emulator/Platform/Platform.cs b/emulator/Platform/Platform.cs index 16d719b..d636f55 100644 --- a/emulator/Platform/Platform.cs +++ b/emulator/Platform/Platform.cs @@ -44,6 +44,16 @@ partial class Platform : IDisposable // A flag that allows us to make a keypress behave as a toggle switch. private bool _allowChangeBoardTestSwitch = true; + // Determines the audio sampling rate for playback on the target platform. + // Pac-Man uses 96 kHz; if set to any other value downsampling will occur. + // See QueueAudioSamples() for more details. + // private const int AUDIO_SAMPLE_RATE = 96000; + private const int AUDIO_SAMPLE_RATE = 44100; + // private const int AUDIO_SAMPLE_RATE = 22050; + // private const int AUDIO_SAMPLE_RATE = 11025; + + private bool _hasEmittedDownsamplingAlert = false; + #endregion #region Events @@ -98,10 +108,7 @@ public void Initialize(string title, int width = 640, int height = 480, float sc // Setup our audio format. SDL.SDL_AudioSpec audioSpec = new SDL.SDL_AudioSpec(); - audioSpec.freq = 96000; // sampling rate - // audioSpec.freq = 44100; // sampling rate - // audioSpec.freq = 22050; // sampling rate - // audioSpec.freq = 11025; // sampling rate + audioSpec.freq = AUDIO_SAMPLE_RATE; // sampling rate audioSpec.format = SDL.AUDIO_S8; // sample format: 8-bit, signed audioSpec.channels = 1; // number of channels audioSpec.samples = 4096; // buffer size @@ -324,30 +331,66 @@ public void StartLoop() * Used to queue the given audio samples for playback. The parameter is expected * to be three 8-bit, signed values, one for each voice. */ - public void QueueAudioSamples(byte[] samples) + public void QueueAudioSamples(byte[][] samplesByChannel) { - // Merge all three voices into one. - var sampleFull = samples[0] + samples[1] + samples[2]; + var sourceSamples = new sbyte[samplesByChannel.Length]; + + for (int i = 0; i < sourceSamples.Length; i++) + { + var sampleChannel1 = samplesByChannel[i][0]; + var sampleChannel2 = samplesByChannel[i][1]; + var sampleChannel3 = samplesByChannel[i][2]; + + // Merge all three voices into one. + var sample = sampleChannel1 + sampleChannel2 + sampleChannel3; - // Clamp the value to the min/max of a 8-bit signed value to avoid distortion. - if (sampleFull > 127) - sampleFull = 127; - else if (sampleFull < -128) - sampleFull = -128; + // Clamp the value to the min/max of a 8-bit signed value to avoid distortion. + if (sample > 127) + sample = 127; + else if (sample < -128) + sample = -128; - var sample = (sbyte)sampleFull; + sourceSamples[i] = (sbyte)sample; + } + + var targetSamples = sourceSamples; + + var sourceFrameSize = sourceSamples.Length; + var targetFrameSize = AUDIO_SAMPLE_RATE / 60; + var shouldDownsample = sourceFrameSize != targetFrameSize; + + if (shouldDownsample) + { + if (!_hasEmittedDownsamplingAlert) + { + _hasEmittedDownsamplingAlert = true; + Console.WriteLine($"Audio: Downsampling from {sourceFrameSize*60} hz to {targetFrameSize*60} hz."); + } + + float factor = (float)sourceFrameSize / (float)targetFrameSize; + + var downsampledSamples = new sbyte[targetFrameSize]; + + for (var i = 0; i < targetFrameSize; i++) + { + var offset = (int)(factor * i); + downsampledSamples[i] = sourceSamples[offset]; + } + + targetSamples = downsampledSamples; + } // Now that we have the combined sample, we need to allocate it as a pinned object // on the heap so that we can pass it through to the unmanaged SDL2 code. - var samplePinned = GCHandle.Alloc(sample, GCHandleType.Pinned); - var pointer = samplePinned.AddrOfPinnedObject(); + var samplesPinned = GCHandle.Alloc(targetSamples, GCHandleType.Pinned); + var pointer = samplesPinned.AddrOfPinnedObject(); // Pass the value to SDL to be queued up for playback. - uint sample_size = sizeof(sbyte) * 1; + uint sample_size = (uint)(sizeof(sbyte) * 1 * targetSamples.Length); SDL.SDL_QueueAudio(_audioDevice, pointer, sample_size); // Unpin this so the GC can clean it up. - samplePinned.Free(); + samplesPinned.Free(); } /**