cleanup IAsyncSoundProvider hacks and consolidate code that is only used for PCE

This commit is contained in:
adelikat 2020-05-09 11:01:02 -05:00
parent b16684b4c7
commit 976ea4967b
7 changed files with 238 additions and 280 deletions

View File

@ -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();
}
}
}

View File

@ -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)

View File

@ -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<Sample> _buffer;
private readonly Sample[] _resampleBuffer;
public VecnaSynchronizer()
{
_buffer = new Queue<Sample>(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<Sample> _buffer;
private readonly Sample[] _resampleBuffer;
public VecnaSynchronizer()
{
_buffer = new Queue<Sample>(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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -1,112 +0,0 @@
using System;
using BizHawk.Emulation.Common;
namespace BizHawk.Emulation.Cores.Components
{
/// <summary>
/// This interface is for legacy sound implementations in some older cores
/// This needs to go away, but is provided here, for now
/// </summary>
internal interface IAsyncSoundProvider
{
void GetSamples(short[] samples);
void DiscardSamples();
}
/// <summary>
/// TODO: this is a shim for now, and needs to go away
/// turns an <seealso cref="IAsyncSoundProvider"/> 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
/// </summary>
internal class FakeSyncSound : ISoundProvider
{
private readonly IAsyncSoundProvider _source;
private readonly int _spf;
/// <summary>
/// Initializes a new instance of the <see cref="FakeSyncSound"/> class.
/// </summary>
/// <param name="source">The async sound provider</param>
/// <param name="spf">number of sample pairs to request and provide on each GetSamples() call</param>
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();
}
}
}

View File

@ -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
/// <summary>
/// An interface that extends a sound provider to provide mixing capabilities through the SoundMixer class
/// </summary>
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<IMixedSoundProvider> _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<IMixedSoundProvider>(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);
}
}