From 976ea4967b864a9d20d3745e79c69c9115eb7a85 Mon Sep 17 00:00:00 2001 From: adelikat Date: Sat, 9 May 2020 11:01:02 -0500 Subject: [PATCH] cleanup IAsyncSoundProvider hacks and consolidate code that is only used for PCE --- .../Consoles/PC Engine/ADPCM.cs | 61 +++-- .../Consoles/PC Engine/PCEngine.cs | 12 +- .../Consoles/PC Engine/VecnaSynchronizer.cs} | 241 +++++++++--------- src/BizHawk.Emulation.Cores/Sound/CDAudio.cs | 19 +- .../Sound/HuC6280PSG.cs | 30 ++- .../Sound/IAsyncSoundProvider.cs | 112 -------- .../Sound/SoundMixer.cs | 43 +++- 7 files changed, 238 insertions(+), 280 deletions(-) rename src/{BizHawk.Emulation.Common/Sound/Utilities/ISynchronizingAudioBuffer.cs => BizHawk.Emulation.Cores/Consoles/PC Engine/VecnaSynchronizer.cs} (81%) delete mode 100644 src/BizHawk.Emulation.Cores/Sound/IAsyncSoundProvider.cs diff --git a/src/BizHawk.Emulation.Cores/Consoles/PC Engine/ADPCM.cs b/src/BizHawk.Emulation.Cores/Consoles/PC Engine/ADPCM.cs index 64fa48f3a7..66b373d239 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/PC Engine/ADPCM.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/PC Engine/ADPCM.cs @@ -1,15 +1,16 @@ using System; using BizHawk.Common; +using BizHawk.Emulation.Common; using BizHawk.Emulation.Cores.Components; namespace BizHawk.Emulation.Cores.PCEngine { public sealed class ADPCM : IMixedSoundProvider { - ScsiCDBus SCSI; - PCEngine pce; - MetaspuSoundProvider SoundProvider = new MetaspuSoundProvider(); + private readonly ScsiCDBus _scsi; + private readonly PCEngine _pce; + private readonly VecnaSynchronizer _synchronizer = new VecnaSynchronizer(); // *************************************************************************** @@ -60,8 +61,8 @@ namespace BizHawk.Emulation.Cores.PCEngine public ADPCM(PCEngine pcEngine, ScsiCDBus scsi) { - pce = pcEngine; - SCSI = scsi; + _pce = pcEngine; + _scsi = scsi; MaxVolume = 24576; } @@ -156,24 +157,24 @@ namespace BizHawk.Emulation.Cores.PCEngine if (AdpcmCdDmaRequested) { - if (SCSI.REQ && SCSI.IO && !SCSI.CD && !SCSI.ACK) + if (_scsi.REQ && _scsi.IO && !_scsi.CD && !_scsi.ACK) { - byte dmaByte = SCSI.DataBits; + byte dmaByte = _scsi.DataBits; RAM[WriteAddress++] = dmaByte; AdpcmLength++; - SCSI.ACK = false; - SCSI.REQ = false; - SCSI.Think(); + _scsi.ACK = false; + _scsi.REQ = false; + _scsi.Think(); } - if (SCSI.DataTransferInProgress == false) + if (_scsi.DataTransferInProgress == false) Port180B = 0; } - pce.IntADPCM = HalfReached; - pce.IntStop = EndReached; - pce.RefreshIRQ2(); + _pce.IntADPCM = HalfReached; + _pce.IntStop = EndReached; + _pce.RefreshIRQ2(); } // *************************************************************************** @@ -281,7 +282,7 @@ namespace BizHawk.Emulation.Cores.PCEngine private void AdpcmEmitSample() { if (AdpcmIsPlaying == false) - SoundProvider.Buffer.EnqueueSample(0, 0); + _synchronizer.EnqueueSample(0, 0); else { if (nextSampleTimer <= 0) @@ -301,21 +302,37 @@ namespace BizHawk.Emulation.Cores.PCEngine } short adjustedSample = (short)((playingSample - 2048) * MaxVolume / 2048); - SoundProvider.Buffer.EnqueueSample(adjustedSample, adjustedSample); + _synchronizer.EnqueueSample(adjustedSample, adjustedSample); } } - public void GetSamples(short[] samples) + public void GetSamplesAsync(short[] samples) { - SoundProvider.GetSamplesAsync(samples); + _synchronizer.OutputSamples(samples, samples.Length / 2); } public void DiscardSamples() { - SoundProvider.DiscardSamples(); + _synchronizer.Clear(); } public int MaxVolume { get; set; } + public bool CanProvideAsync => true; + + public void SetSyncMode(SyncSoundMode mode) + { + if (mode != SyncSoundMode.Async) + { + throw new NotImplementedException("Only async currently supported."); + } + } + + public SyncSoundMode SyncMode => SyncSoundMode.Async; + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + throw new NotImplementedException("Sync sound not yet supported"); + } // *************************************************************************** @@ -351,9 +368,9 @@ namespace BizHawk.Emulation.Cores.PCEngine if (ser.IsReader) { Port180E = port180E; - pce.IntADPCM = HalfReached; - pce.IntStop = EndReached; - pce.RefreshIRQ2(); + _pce.IntADPCM = HalfReached; + _pce.IntStop = EndReached; + _pce.RefreshIRQ2(); } } } diff --git a/src/BizHawk.Emulation.Cores/Consoles/PC Engine/PCEngine.cs b/src/BizHawk.Emulation.Cores/Consoles/PC Engine/PCEngine.cs index 57fb5417aa..137cdb8ba7 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/PC Engine/PCEngine.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/PC Engine/PCEngine.cs @@ -155,7 +155,7 @@ namespace BizHawk.Emulation.Cores.PCEngine Cpu = new HuC6280(MemoryCallbacks); VCE = new VCE(); VDC1 = new VDC(this, Cpu, VCE); - PSG = new HuC6280PSG(); + PSG = new HuC6280PSG(735); SCSI = new ScsiCDBus(this, disc); Cpu.Logger = s => Tracer.Put(s); @@ -166,7 +166,7 @@ namespace BizHawk.Emulation.Cores.PCEngine Cpu.ReadMemory21 = ReadMemory; Cpu.WriteMemory21 = WriteMemory; Cpu.WriteVDC = VDC1.WriteVDC; - _soundProvider = new FakeSyncSound(PSG, 735); + _soundProvider = PSG; CDAudio = new CDAudio(null, 0); } @@ -178,7 +178,7 @@ namespace BizHawk.Emulation.Cores.PCEngine Cpu.ReadMemory21 = ReadMemorySGX; Cpu.WriteMemory21 = WriteMemorySGX; Cpu.WriteVDC = VDC1.WriteVDC; - _soundProvider = new FakeSyncSound(PSG, 735); + _soundProvider = PSG; CDAudio = new CDAudio(null, 0); } @@ -193,9 +193,9 @@ namespace BizHawk.Emulation.Cores.PCEngine CDAudio = new CDAudio(disc); SetCDAudioCallback(); PSG.MaxVolume = short.MaxValue * 3 / 4; - SoundMixer = new SoundMixer(PSG, CDAudio, ADPCM); - _soundProvider = new FakeSyncSound(SoundMixer, 735); - Cpu.ThinkAction = (cycles) => { SCSI.Think(); ADPCM.Think(cycles); }; + SoundMixer = new SoundMixer(735, PSG, CDAudio, ADPCM); + _soundProvider = SoundMixer; + Cpu.ThinkAction = cycles => { SCSI.Think(); ADPCM.Think(cycles); }; } if (rom.Length == 0x60000) diff --git a/src/BizHawk.Emulation.Common/Sound/Utilities/ISynchronizingAudioBuffer.cs b/src/BizHawk.Emulation.Cores/Consoles/PC Engine/VecnaSynchronizer.cs similarity index 81% rename from src/BizHawk.Emulation.Common/Sound/Utilities/ISynchronizingAudioBuffer.cs rename to src/BizHawk.Emulation.Cores/Consoles/PC Engine/VecnaSynchronizer.cs index 625a476f20..262e53abec 100644 --- a/src/BizHawk.Emulation.Common/Sound/Utilities/ISynchronizingAudioBuffer.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/PC Engine/VecnaSynchronizer.cs @@ -1,127 +1,114 @@ -using System.Collections.Generic; - -namespace BizHawk.Emulation.Common -{ - public interface ISynchronizingAudioBuffer - { - void EnqueueSample(short left, short right); - void Clear(); - - // returns the number of samples actually supplied, which may not match the number requested - // ^^ what the hell is that supposed to mean. - // the entire point of an ISynchronizingAudioBuffer - // is to provide exact amounts of output samples, - // even when the input provided varies.... - int OutputSamples(short[] buf, int samplesRequested); - } - - public class VecnaSynchronizer : ISynchronizingAudioBuffer - { - // vecna's attempt at a fully synchronous sound provider. - // It's similar in philosophy to my "BufferedAsync" provider, but BufferedAsync is not - // fully synchronous. - - // Like BufferedAsync, it tries to make most frames 100% correct and just suck it up - // periodically and have a big bad-sounding mistake frame if it has to. - - // It is significantly less ambitious and elaborate than the other methods. - // We'll see if it works better or not! - - // It has a min and maximum amount of excess buffer to deal with minor overflows. - // When fast-forwarding, it will discard samples above the maximum excess buffer. - - // When underflowing, it will attempt to resample to a certain thresh - // old. - // If it underflows beyond that threshold, it will give up and output silence. - // Since it has done this, it will go ahead and generate some excess silence in order - // to restock its excess buffer. - private struct Sample - { - public readonly short Left; - public readonly short Right; - - public Sample(short left, short right) - { - Left = left; - Right = right; - } - } - - private const int MaxExcessSamples = 2048; - - private readonly Queue _buffer; - private readonly Sample[] _resampleBuffer; - - public VecnaSynchronizer() - { - _buffer = new Queue(2048); - _resampleBuffer = new Sample[2730]; // 2048 * 1.25 - - // Give us a little buffer wiggle-room - for (int i = 0; i < 367; i++) - { - _buffer.Enqueue(new Sample(0, 0)); - } - } - - public void EnqueueSample(short left, short right) - { - if (_buffer.Count >= MaxExcessSamples - 1) - { - // if buffer is overfull, dequeue old samples to make room for new samples. - _buffer.Dequeue(); - } - - _buffer.Enqueue(new Sample(left, right)); - } - - public void Clear() - { - _buffer.Clear(); - } - - public int OutputSamples(short[] buf, int samplesRequested) - { - if (samplesRequested > _buffer.Count) - { - // underflow! - if (_buffer.Count > samplesRequested * 3 / 4) - { - // if we're within 75% of target, then I guess we suck it up and resample. - // we sample in a goofy way, we could probably do it a bit smarter, if we cared more. - int samplesAvailable = _buffer.Count; - for (int i = 0; _buffer.Count > 0; i++) - { - _resampleBuffer[i] = _buffer.Dequeue(); - } - - int index = 0; - for (int i = 0; i < samplesRequested; i++) - { - Sample sample = _resampleBuffer[i * samplesAvailable / samplesRequested]; - buf[index++] += sample.Left; - buf[index++] += sample.Right; - } - } - else - { - // we're outside of a "reasonable" underflow. Give up and output silence. - // Do nothing. The whole frame will be excess buffer. - } - } - else - { - // normal operation - int index = 0; - for (int i = 0; i < samplesRequested && _buffer.Count > 0; i++) - { - Sample sample = _buffer.Dequeue(); - buf[index++] += sample.Left; - buf[index++] += sample.Right; - } - } - - return samplesRequested; - } - } -} +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Components +{ + public class VecnaSynchronizer + { + // vecna's attempt at a fully synchronous sound provider. + // It's similar in philosophy to my "BufferedAsync" provider, but BufferedAsync is not + // fully synchronous. + + // Like BufferedAsync, it tries to make most frames 100% correct and just suck it up + // periodically and have a big bad-sounding mistake frame if it has to. + + // It is significantly less ambitious and elaborate than the other methods. + // We'll see if it works better or not! + + // It has a min and maximum amount of excess buffer to deal with minor overflows. + // When fast-forwarding, it will discard samples above the maximum excess buffer. + + // When underflowing, it will attempt to resample to a certain thresh + // old. + // If it underflows beyond that threshold, it will give up and output silence. + // Since it has done this, it will go ahead and generate some excess silence in order + // to restock its excess buffer. + private struct Sample + { + public readonly short Left; + public readonly short Right; + + public Sample(short left, short right) + { + Left = left; + Right = right; + } + } + + private const int MaxExcessSamples = 2048; + + private readonly Queue _buffer; + private readonly Sample[] _resampleBuffer; + + public VecnaSynchronizer() + { + _buffer = new Queue(2048); + _resampleBuffer = new Sample[2730]; // 2048 * 1.25 + + // Give us a little buffer wiggle-room + for (int i = 0; i < 367; i++) + { + _buffer.Enqueue(new Sample(0, 0)); + } + } + + public void EnqueueSample(short left, short right) + { + if (_buffer.Count >= MaxExcessSamples - 1) + { + // if buffer is overfull, dequeue old samples to make room for new samples. + _buffer.Dequeue(); + } + + _buffer.Enqueue(new Sample(left, right)); + } + + public void Clear() + { + _buffer.Clear(); + } + + public int OutputSamples(short[] buf, int samplesRequested) + { + if (samplesRequested > _buffer.Count) + { + // underflow! + if (_buffer.Count > samplesRequested * 3 / 4) + { + // if we're within 75% of target, then I guess we suck it up and resample. + // we sample in a goofy way, we could probably do it a bit smarter, if we cared more. + int samplesAvailable = _buffer.Count; + for (int i = 0; _buffer.Count > 0; i++) + { + _resampleBuffer[i] = _buffer.Dequeue(); + } + + int index = 0; + for (int i = 0; i < samplesRequested; i++) + { + Sample sample = _resampleBuffer[i * samplesAvailable / samplesRequested]; + buf[index++] += sample.Left; + buf[index++] += sample.Right; + } + } + else + { + // we're outside of a "reasonable" underflow. Give up and output silence. + // Do nothing. The whole frame will be excess buffer. + } + } + else + { + // normal operation + int index = 0; + for (int i = 0; i < samplesRequested && _buffer.Count > 0; i++) + { + Sample sample = _buffer.Dequeue(); + buf[index++] += sample.Left; + buf[index++] += sample.Right; + } + } + + return samplesRequested; + } + } +} diff --git a/src/BizHawk.Emulation.Cores/Sound/CDAudio.cs b/src/BizHawk.Emulation.Cores/Sound/CDAudio.cs index 25a2c99073..2d9f73535b 100644 --- a/src/BizHawk.Emulation.Cores/Sound/CDAudio.cs +++ b/src/BizHawk.Emulation.Cores/Sound/CDAudio.cs @@ -1,5 +1,6 @@ using System; using BizHawk.Common; +using BizHawk.Emulation.Common; using BizHawk.Emulation.DiscSystem; // The state of the cd player is quantized to the frame level. @@ -139,7 +140,23 @@ namespace BizHawk.Emulation.Cores.Components } } - public void GetSamples(short[] samples) + public bool CanProvideAsync => true; + public void SetSyncMode(SyncSoundMode mode) + { + if (mode != SyncSoundMode.Async) + { + throw new NotImplementedException("Only async currently supported."); + } + } + + public SyncSoundMode SyncMode => SyncSoundMode.Async; + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + throw new NotImplementedException("Sync sound not yet supported"); + } + + public void GetSamplesAsync(short[] samples) { if (Mode != CDAudioMode_Playing) return; diff --git a/src/BizHawk.Emulation.Cores/Sound/HuC6280PSG.cs b/src/BizHawk.Emulation.Cores/Sound/HuC6280PSG.cs index 898964f80c..21a233c9be 100644 --- a/src/BizHawk.Emulation.Cores/Sound/HuC6280PSG.cs +++ b/src/BizHawk.Emulation.Cores/Sound/HuC6280PSG.cs @@ -11,8 +11,10 @@ namespace BizHawk.Emulation.Cores.Components // It is embedded on the CPU and doesn't have its own part number. None the less, it is emulated separately from the 6280 CPU. // Sound refactor TODO: IMixedSoundProvider must inherit ISoundProvider - public sealed class HuC6280PSG : IMixedSoundProvider + // TODo: this provides "fake" sync sound by hardcoding the number of samples + public sealed class HuC6280PSG : ISoundProvider, IMixedSoundProvider { + private readonly int _spf; public class PSGChannel { public ushort Frequency; @@ -44,11 +46,11 @@ namespace BizHawk.Emulation.Cores.Components public byte MainVolumeLeft; public byte MainVolumeRight; - public int MaxVolume { get; set; } + public int MaxVolume { get; set; } = short.MaxValue; - public HuC6280PSG() + public HuC6280PSG(int spf) { - MaxVolume = short.MaxValue; + _spf = spf; Waves.InitWaves(); for (int i = 0; i < 8; i++) { @@ -147,7 +149,25 @@ namespace BizHawk.Emulation.Cores.Components public void DiscardSamples() { } - public void GetSamples(short[] samples) + public bool CanProvideAsync => true; + + public void SetSyncMode(SyncSoundMode mode) => SyncMode = mode; + public SyncSoundMode SyncMode { get; private set; } + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + if (SyncMode != SyncSoundMode.Sync) + { + throw new InvalidOperationException("Must be in sync mode to call a sync method"); + } + + short[] ret = new short[_spf * 2]; + GetSamplesAsync(ret); + samples = ret; + nsamp = _spf; + } + + public void GetSamplesAsync(short[] samples) { int elapsedCycles = (int)(frameStopTime - frameStartTime); int start = 0; diff --git a/src/BizHawk.Emulation.Cores/Sound/IAsyncSoundProvider.cs b/src/BizHawk.Emulation.Cores/Sound/IAsyncSoundProvider.cs deleted file mode 100644 index 1a1618aaf3..0000000000 --- a/src/BizHawk.Emulation.Cores/Sound/IAsyncSoundProvider.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using BizHawk.Emulation.Common; - -namespace BizHawk.Emulation.Cores.Components -{ - /// - /// This interface is for legacy sound implementations in some older cores - /// This needs to go away, but is provided here, for now - /// - internal interface IAsyncSoundProvider - { - void GetSamples(short[] samples); - void DiscardSamples(); - } - - /// - /// TODO: this is a shim for now, and needs to go away - /// turns an into a full ISoundProvider - /// This is used in cores that have an async only sound implementation - /// better is implement a sync sound option in those cores without the need for - /// this class or an IAsyncSoundProvider interface - /// - internal class FakeSyncSound : ISoundProvider - { - private readonly IAsyncSoundProvider _source; - private readonly int _spf; - - /// - /// Initializes a new instance of the class. - /// - /// The async sound provider - /// number of sample pairs to request and provide on each GetSamples() call - public FakeSyncSound(IAsyncSoundProvider source, int spf) - { - _source = source; - _spf = spf; - SyncMode = SyncSoundMode.Sync; - } - - public void GetSamplesSync(out short[] samples, out int nsamp) - { - if (SyncMode != SyncSoundMode.Sync) - { - throw new InvalidOperationException("Must be in sync mode to call a sync method"); - } - - short[] ret = new short[_spf * 2]; - _source.GetSamples(ret); - samples = ret; - nsamp = _spf; - } - - public void DiscardSamples() - { - _source.DiscardSamples(); - } - - public void GetSamplesAsync(short[] samples) - { - if (SyncMode != SyncSoundMode.Async) - { - throw new InvalidOperationException("Must be in async mode to call an async method"); - } - - _source.GetSamples(samples); - } - - public bool CanProvideAsync => true; - - public SyncSoundMode SyncMode { get; private set; } - - public void SetSyncMode(SyncSoundMode mode) - { - SyncMode = mode; - } - } - - // An async sound provider - // This class needs to go away, it takes an IAsyncSoundProvider - // and is only used by legacy sound implementations - internal class MetaspuSoundProvider : ISoundProvider - { - public ISynchronizingAudioBuffer Buffer { get; } = new VecnaSynchronizer(); - - public bool CanProvideAsync => true; - - public SyncSoundMode SyncMode => SyncSoundMode.Async; - - public void SetSyncMode(SyncSoundMode mode) - { - if (mode != SyncSoundMode.Async) - { - throw new NotSupportedException("Only Async mode is supported."); - } - } - - public void GetSamplesSync(out short[] samples, out int nsamp) - { - throw new InvalidOperationException("Sync mode is not supported."); - } - - public void GetSamplesAsync(short[] samples) - { - Buffer.OutputSamples(samples, samples.Length / 2); - } - - public void DiscardSamples() - { - Buffer.Clear(); - } - } -} diff --git a/src/BizHawk.Emulation.Cores/Sound/SoundMixer.cs b/src/BizHawk.Emulation.Cores/Sound/SoundMixer.cs index 0beb18ddf9..7754ac651e 100644 --- a/src/BizHawk.Emulation.Cores/Sound/SoundMixer.cs +++ b/src/BizHawk.Emulation.Cores/Sound/SoundMixer.cs @@ -1,24 +1,35 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using BizHawk.Emulation.Common; namespace BizHawk.Emulation.Cores.Components { - // TODO: Sound mixer is a good concept, but it needs to be refactored to use an ISoundProvider, it perhaps can enforce only receiving providers in Async mode + // TODO: Sound mixer is a good concept, but it needs to support sync mode /// /// An interface that extends a sound provider to provide mixing capabilities through the SoundMixer class /// - internal interface IMixedSoundProvider : IAsyncSoundProvider + internal interface IMixedSoundProvider : ISoundProvider { int MaxVolume { get; set; } } // This is a straightforward class to mix/chain multiple ISoundProvider sources. - internal sealed class SoundMixer : IAsyncSoundProvider + // Relies on a hack of passing in the samples per frame for sync sound abilities + internal sealed class SoundMixer : ISoundProvider { + private readonly int _spf; private readonly List _soundProviders; - public SoundMixer(params IMixedSoundProvider[] soundProviders) + public SoundMixer(int spf, params IMixedSoundProvider[] soundProviders) { + if (soundProviders.Any(s => !s.CanProvideAsync)) + { + throw new InvalidOperationException("Sound mixer only works with async sound currently"); + } + _soundProviders = new List(soundProviders); + _spf = spf; } public void DisableSource(IMixedSoundProvider source) @@ -34,11 +45,29 @@ namespace BizHawk.Emulation.Cores.Components } } - public void GetSamples(short[] samples) + public bool CanProvideAsync => true; + + public void SetSyncMode(SyncSoundMode mode) => SyncMode = mode; + public SyncSoundMode SyncMode { get; private set; } + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + if (SyncMode != SyncSoundMode.Sync) + { + throw new InvalidOperationException("Must be in sync mode to call a sync method"); + } + + short[] ret = new short[_spf * 2]; + GetSamplesAsync(ret); + samples = ret; + nsamp = _spf; + } + + public void GetSamplesAsync(short[] samples) { foreach (var soundSource in _soundProviders) { - soundSource.GetSamples(samples); + soundSource.GetSamplesAsync(samples); } }