SyncSoundMixer: improved and moved out of ZXSpectrum into Cores.Sound (as the CPC will use this and future cores may find it useful)

This commit is contained in:
Asnivor 2019-04-04 12:15:58 +01:00
parent 244b9d2231
commit b2584145d7
4 changed files with 358 additions and 227 deletions

View File

@ -370,7 +370,6 @@
<Compile Include="Computers\SinclairSpectrum\Media\Tape\WAV\WavConverter.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\WAV\WavHeader.cs" />
<Compile Include="Computers\SinclairSpectrum\Media\Tape\WAV\WavStreamReader.cs" />
<Compile Include="Computers\SinclairSpectrum\SoundProviderMixer.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\MachineType.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\SpectrumBase.cs" />
<Compile Include="Computers\SinclairSpectrum\Machine\SpectrumBase.Input.cs">
@ -1584,6 +1583,7 @@
<Compile Include="Sound\DualSyncSound.cs" />
<Compile Include="Sound\OneBitBeeper.cs" />
<Compile Include="Sound\SN76489sms.cs" />
<Compile Include="Sound\SyncSoundMixer.cs" />
<Compile Include="Waterbox\CustomSaverammer.cs" />
<Compile Include="Waterbox\ElfRunner.cs" />
<Compile Include="FileID.cs" />

View File

@ -1,213 +0,0 @@
using BizHawk.Emulation.Common;
using System;
using System.Collections.Generic;
using System.Linq;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
{
/// <summary>
/// My attempt at mixing multiple ISoundProvider sources together and outputting another ISoundProvider
/// Currently only supports SyncSoundMode.Sync
/// Attached ISoundProvider sources must already be stereo 44.1khz and ideally sound buffers should be the same length (882)
/// (if not, only 882 samples of their buffer will be used)
/// </summary>
internal sealed class SoundProviderMixer : ISoundProvider
{
private class Provider
{
public ISoundProvider SoundProvider { get; set; }
public string ProviderDescription { get; set; }
public int MaxVolume { get; set; }
public short[] Buffer { get; set; }
public int NSamp { get; set; }
}
private bool _stereo = true;
public bool Stereo
{
get { return _stereo; }
set { _stereo = value; }
}
private readonly List<Provider> SoundProviders;
public SoundProviderMixer(params ISoundProvider[] soundProviders)
{
SoundProviders = new List<Provider>();
foreach (var s in soundProviders)
{
SoundProviders.Add(new Provider
{
SoundProvider = s,
MaxVolume = short.MaxValue,
});
}
EqualizeVolumes();
}
public SoundProviderMixer(short maxVolume, string description, params ISoundProvider[] soundProviders)
{
SoundProviders = new List<Provider>();
foreach (var s in soundProviders)
{
SoundProviders.Add(new Provider
{
SoundProvider = s,
MaxVolume = maxVolume,
ProviderDescription = description
});
}
EqualizeVolumes();
}
public void AddSource(ISoundProvider source, string description)
{
SoundProviders.Add(new Provider
{
SoundProvider = source,
MaxVolume = short.MaxValue,
ProviderDescription = description
});
EqualizeVolumes();
}
public void AddSource(ISoundProvider source, short maxVolume, string description)
{
SoundProviders.Add(new Provider
{
SoundProvider = source,
MaxVolume = maxVolume,
ProviderDescription = description
});
EqualizeVolumes();
}
public void DisableSource(ISoundProvider source)
{
var sp = SoundProviders.Where(a => a.SoundProvider == source);
if (sp.Count() == 1)
SoundProviders.Remove(sp.First());
else if (sp.Count() > 1)
foreach (var s in sp)
SoundProviders.Remove(s);
EqualizeVolumes();
}
public void EqualizeVolumes()
{
if (SoundProviders.Count < 1)
return;
int eachVolume = short.MaxValue / SoundProviders.Count;
foreach (var source in SoundProviders)
{
source.MaxVolume = eachVolume;
}
}
#region ISoundProvider
public bool CanProvideAsync => false;
public SyncSoundMode SyncMode => SyncSoundMode.Sync;
public void SetSyncMode(SyncSoundMode mode)
{
if (mode != SyncSoundMode.Sync)
throw new InvalidOperationException("Only Sync mode is supported.");
}
public void GetSamplesAsync(short[] samples)
{
throw new NotSupportedException("Async is not available");
}
public void DiscardSamples()
{
foreach (var soundSource in SoundProviders)
{
soundSource.SoundProvider.DiscardSamples();
}
}
public void GetSamplesSync(out short[] samples, out int nsamp)
{
samples = null;
nsamp = 0;
// get samples from all the providers
foreach (var sp in SoundProviders)
{
int sampCount;
short[] samp;
sp.SoundProvider.GetSamplesSync(out samp, out sampCount);
sp.NSamp = sampCount;
sp.Buffer = samp;
}
// are all the sample lengths the same?
var firstEntry = SoundProviders.First();
bool sameCount = SoundProviders.All(s => s.NSamp == firstEntry.NSamp);
if (!sameCount)
{
// this is a bit hacky, really all ISoundProviders should be supplying 44100 with 882 samples per frame.
// we will make sure this happens (no matter how it sounds)
if (SoundProviders.Count > 1)
{
for (int i = 0; i < SoundProviders.Count; i++)
{
int ns = SoundProviders[i].NSamp;
short[] buff = new short[882 * 2];
for (int b = 0; b < 882 * 2; b++)
{
if (b == SoundProviders[i].Buffer.Length - 1)
{
// end of source buffer
break;
}
buff[b] = SoundProviders[i].Buffer[b];
}
// save back to the soundprovider
SoundProviders[i].NSamp = 882;
SoundProviders[i].Buffer = buff;
}
}
else
{
// just process what we have as-is
}
}
// mix the soundproviders together
nsamp = 882;
samples = new short[nsamp * 2];
for (int i = 0; i < samples.Length; i++)
{
short sectorVal = 0;
foreach (var sp in SoundProviders)
{
if (sp.Buffer[i] > sp.MaxVolume)
sectorVal += (short)sp.MaxVolume;
else
sectorVal += sp.Buffer[i];
}
samples[i] = sectorVal;
}
}
#endregion
}
}

View File

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using BizHawk.Emulation.Cores.Components;
using BizHawk.Emulation.Cores.Sound;
namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
@ -47,11 +48,11 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
PutSettings((ZXSpectrumSettings)settings ?? new ZXSpectrumSettings());
List<JoystickType> joysticks = new List<JoystickType>();
joysticks.Add(((ZXSpectrumSyncSettings)syncSettings as ZXSpectrumSyncSettings).JoystickType1);
joysticks.Add(((ZXSpectrumSyncSettings)syncSettings as ZXSpectrumSyncSettings).JoystickType2);
joysticks.Add(((ZXSpectrumSyncSettings)syncSettings as ZXSpectrumSyncSettings).JoystickType3);
joysticks.Add(((ZXSpectrumSyncSettings)syncSettings).JoystickType1);
joysticks.Add(((ZXSpectrumSyncSettings)syncSettings).JoystickType2);
joysticks.Add(((ZXSpectrumSyncSettings)syncSettings).JoystickType3);
deterministicEmulation = ((ZXSpectrumSyncSettings)syncSettings as ZXSpectrumSyncSettings).DeterministicEmulation;
deterministicEmulation = ((ZXSpectrumSyncSettings)syncSettings).DeterministicEmulation;
if (deterministic != null && deterministic == true)
{
@ -117,26 +118,29 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
ser.Register<IVideoProvider>(_machine.ULADevice);
// initialize sound mixer and attach the various ISoundProvider devices
SoundMixer = new SoundProviderMixer((int)(32767 / 10), "System Beeper", (ISoundProvider)_machine.BuzzerDevice);
SoundMixer.AddSource((ISoundProvider)_machine.TapeBuzzer, "Tape Audio");
if (_machine.AYDevice != null)
SoundMixer.AddSource(_machine.AYDevice, "AY-3-3912");
SoundMixer = new SyncSoundMixer(targetSampleCount: 882);
SoundMixer.PinSource(_machine.BuzzerDevice, "System Beeper", (int)(32767 / 10));
SoundMixer.PinSource(_machine.TapeBuzzer, "Tape Audio", (int)(32767 / 10));
if (_machine.AYDevice != null)
{
SoundMixer.PinSource(_machine.AYDevice, "AY-3-3912");
}
// set audio device settings
if (_machine.AYDevice != null && _machine.AYDevice.GetType() == typeof(AY38912))
{
((AY38912)_machine.AYDevice as AY38912).PanningConfiguration = ((ZXSpectrumSettings)settings as ZXSpectrumSettings).AYPanConfig;
_machine.AYDevice.Volume = ((ZXSpectrumSettings)settings as ZXSpectrumSettings).AYVolume;
((AY38912)_machine.AYDevice).PanningConfiguration = ((ZXSpectrumSettings)settings).AYPanConfig;
_machine.AYDevice.Volume = ((ZXSpectrumSettings)settings).AYVolume;
}
if (_machine.BuzzerDevice != null)
{
((OneBitBeeper)_machine.BuzzerDevice as OneBitBeeper).Volume = ((ZXSpectrumSettings)settings as ZXSpectrumSettings).EarVolume;
_machine.BuzzerDevice.Volume = ((ZXSpectrumSettings)settings).EarVolume;
}
if (_machine.TapeBuzzer != null)
{
((OneBitBeeper)_machine.TapeBuzzer as OneBitBeeper).Volume = ((ZXSpectrumSettings)settings as ZXSpectrumSettings).TapeVolume;
_machine.TapeBuzzer.Volume = ((ZXSpectrumSettings)settings).TapeVolume;
}
DCFilter dc = new DCFilter(SoundMixer, 512);
@ -160,7 +164,7 @@ namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum
public List<GameInfo> _tapeInfo = new List<GameInfo>();
public List<GameInfo> _diskInfo = new List<GameInfo>();
private SoundProviderMixer SoundMixer;
private SyncSoundMixer SoundMixer;
private readonly List<byte[]> _files;

View File

@ -0,0 +1,340 @@
using BizHawk.Emulation.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using BizHawk.Common;
namespace BizHawk.Emulation.Cores.Components
{
/// <summary>
/// ISoundProvider mixer that generates a single ISoundProvider output from multiple ISoundProvider sources
/// Currently only supports sync (not async)
///
/// Bizhawk expects ISoundProviders to output at 44100KHz, so this is what SyncSoundMixer does. Therefore, try to make
/// sure that your child ISoundProviders also do this I guess.
///
/// This is currently used in the ZX Spectrum and CPC cores but others may find it useful in future
/// </summary>
public sealed class SyncSoundMixer : ISoundProvider
{
/// <summary>
/// Currently attached ChildProviders
/// </summary>
private readonly List<ChildProvider> _soundProviders = new List<ChildProvider>();
/// <summary>
/// The final output max volume
/// </summary>
public short FinalMaxVolume
{
get { return _finalMaxVolume; }
set
{
_finalMaxVolume = value;
EqualizeVolumes();
}
}
private short _finalMaxVolume;
/// <summary>
/// How the sound sources are balanced against each other
/// </summary>
public SoundMixBalance MixBalanceMethod
{
get { return _mixBalanceMethod; }
set
{
_mixBalanceMethod = value;
EqualizeVolumes();
}
}
private SoundMixBalance _mixBalanceMethod;
/// <summary>
/// If specified the output buffer of the SyncSoundMixer will always contain this many samples
/// You should probably nearly always specify a value for this and get your ISoundProvider sources
/// to get as close to this nsamp value as possible. Otherwise the number of samples will
/// be based on the highest nsamp out of all the child providers for that specific frame
/// Useful examples:
/// 882 - 44100KHz - 50Hz
/// 735 - 44100Khz - 60Hz
/// </summary>
private int? _targetSampleCount;
/// <summary>
/// Constructor
/// </summary>
/// <param name="mixBalanceMethod">Whether each providers MaxVolume is reduced to an equal share of the final max volume value</param>
/// <param name="maxVolume">The final 'master' max volume</param>
/// <param name="targetSampleCount">
/// If specified the output buffer of the SyncSoundMixer will always contain this many samples
/// If left null the output buffer will contain the highest number of samples out of each of the providers every frame
/// </param>
public SyncSoundMixer(SoundMixBalance mixBalanceMethod = SoundMixBalance.Equalize, short maxVolume = short.MaxValue, int? targetSampleCount = null)
{
_mixBalanceMethod = mixBalanceMethod;
_finalMaxVolume = maxVolume;
_targetSampleCount = targetSampleCount;
}
/// <summary>
/// Adds an ISoundProvider to the SyncSoundMixer
/// </summary>
/// <param name="source">The source ISoundProvider</param>
/// <param name="sourceDescription">An ident string for the ISoundProvider (useful when debugging)</param>
/// <param name="isMono">If this is true then only half the samples should be present</param>
public void PinSource(ISoundProvider source, string sourceDescription)
{
PinSource(source, sourceDescription, FinalMaxVolume);
}
/// <summary>
/// Adds an ISoundProvider to the SyncSoundMixer
/// </summary>
/// <param name="source">The source ISoundProvider</param>
/// <param name="sourceDescription">An ident string for the ISoundProvider (useful when debugging)</param>
/// <param name="sourceMaxVolume">The MaxVolume level for this particular ISoundProvider</param>
/// <param name="isMono">If this is true then only half the samples should be present</param>
public void PinSource(ISoundProvider source, string sourceDescription, short sourceMaxVolume)
{
_soundProviders.Add(new ChildProvider
{
SoundProvider = source,
ProviderDescription = sourceDescription,
MaxVolume = sourceMaxVolume
});
EqualizeVolumes();
}
/// <summary>
/// Removes an existing ISoundProvider from the SyncSoundMixer
/// </summary>
/// <param name="source"></param>
public void UnPinSource(ISoundProvider source)
{
var sp = _soundProviders.Where(a => a.SoundProvider == source);
if (sp.Count() == 1)
{
_soundProviders.Remove(sp.First());
}
else if (sp.Count() > 1)
{
foreach (var s in sp)
{
_soundProviders.Remove(s);
}
}
EqualizeVolumes();
}
/// <summary>
/// Sets each pinned sound provider's MaxVolume based on the MixBalanceMethod
/// </summary>
public void EqualizeVolumes()
{
if (_soundProviders.Count < 1)
return;
switch (MixBalanceMethod)
{
case SoundMixBalance.Equalize:
var eachVolume = FinalMaxVolume / _soundProviders.Count;
foreach (var source in _soundProviders)
{
source.MaxVolume = eachVolume;
}
break;
case SoundMixBalance.MasterHardLimit:
foreach (var source in _soundProviders)
{
if (source.MaxVolume > FinalMaxVolume)
{
source.MaxVolume = FinalMaxVolume;
}
}
break;
}
}
/// <summary>
/// Returns the value of the highest nsamp in the SoundProviders collection
/// </summary>
/// <returns></returns>
private int GetHigestSampleCount()
{
var lookup = _soundProviders.OrderByDescending(x => x.InputNSamp)
.FirstOrDefault();
if (lookup == null)
{
return 0;
}
return lookup.InputNSamp;
}
#region ISoundProvider
public bool CanProvideAsync => false;
public SyncSoundMode SyncMode => SyncSoundMode.Sync;
public void SetSyncMode(SyncSoundMode mode)
{
if (mode != SyncSoundMode.Sync)
throw new InvalidOperationException("Only Sync mode is supported.");
}
public void GetSamplesAsync(short[] samples)
{
throw new NotSupportedException("Async is not available");
}
public void DiscardSamples()
{
foreach (var soundSource in _soundProviders)
{
soundSource.SoundProvider.DiscardSamples();
}
}
public void GetSamplesSync(out short[] samples, out int nsamp)
{
// fetch samples from all the providers
foreach (var sp in _soundProviders)
{
sp.GetSamples();
}
nsamp = _targetSampleCount ?? GetHigestSampleCount();
samples = new short[nsamp * 2];
// process the output buffers
foreach (var sp in _soundProviders)
{
sp.PrepareOutput(nsamp);
}
// mix the child providers together
for (int i = 0; i < samples.Length; i++)
{
int sampleVal = 0;
foreach (var sp in _soundProviders)
{
if (sp.OutputBuffer[i] > sp.MaxVolume)
{
sampleVal += (short)sp.MaxVolume;
}
else
{
sampleVal += sp.OutputBuffer[i];
}
}
// final hard limit
if (sampleVal > (int)FinalMaxVolume)
{
sampleVal = (int)FinalMaxVolume;
}
samples[i] = (short)sampleVal;
}
}
#endregion
/// <summary>
/// Instantiated for every ISoundProvider source that is added to the mixer
/// </summary>
private class ChildProvider
{
/// <summary>
/// The Child ISoundProvider
/// </summary>
public ISoundProvider SoundProvider;
/// <summary>
/// Identification string
/// </summary>
public string ProviderDescription;
/// <summary>
/// The max volume for this provider
/// </summary>
public int MaxVolume;
/// <summary>
/// Stores the incoming samples
/// </summary>
public short[] InputBuffer;
/// <summary>
/// The incoming number of samples
/// </summary>
public int InputNSamp;
/// <summary>
/// Stores the processed samples ready for mixing
/// </summary>
public short[] OutputBuffer;
/// <summary>
/// The output number of samples
/// </summary>
public int OutputNSamp;
/// <summary>
/// Fetches sample data from the child ISoundProvider
/// </summary>
public void GetSamples()
{
SoundProvider.GetSamplesSync(out InputBuffer, out InputNSamp);
}
/// <summary>
/// Ensures the output buffer is ready for mixing based on the supplied nsamp value
/// Overflow samples will be omitted and underflow samples will be empty air
/// </summary>
/// <param name="nsamp"></param>
public void PrepareOutput(int nsamp)
{
OutputNSamp = nsamp;
var outputBuffSize = OutputNSamp * 2;
if (OutputNSamp != InputNSamp || InputBuffer.Length != outputBuffSize)
{
OutputBuffer = new short[outputBuffSize];
var i = 0;
while (i < InputBuffer.Length && i < outputBuffSize)
{
OutputBuffer[i] = InputBuffer[i];
i++;
}
}
else
{
// buffer needs no modification
OutputBuffer = InputBuffer;
}
}
}
}
/// <summary>
/// Defines how mixed sound sources should be balanced
/// </summary>
public enum SoundMixBalance
{
/// <summary>
/// Each sound source's max volume will be set to MaxVolume / nSources
/// </summary>
Equalize,
/// <summary>
/// Each sound source's individual max volume will be respected but the final MaxVolume will be limited to MaxVolume
/// </summary>
MasterHardLimit
}
}