From b2584145d783cb8dd426e72c389ff8e6f12ed98d Mon Sep 17 00:00:00 2001 From: Asnivor Date: Thu, 4 Apr 2019 12:15:58 +0100 Subject: [PATCH] SyncSoundMixer: improved and moved out of ZXSpectrum into Cores.Sound (as the CPC will use this and future cores may find it useful) --- .../BizHawk.Emulation.Cores.csproj | 2 +- .../SinclairSpectrum/SoundProviderMixer.cs | 213 ----------- .../Computers/SinclairSpectrum/ZXSpectrum.cs | 30 +- .../Sound/SyncSoundMixer.cs | 340 ++++++++++++++++++ 4 files changed, 358 insertions(+), 227 deletions(-) delete mode 100644 BizHawk.Emulation.Cores/Computers/SinclairSpectrum/SoundProviderMixer.cs create mode 100644 BizHawk.Emulation.Cores/Sound/SyncSoundMixer.cs diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj index 098bc564b2..864f3b5e7a 100644 --- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj +++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj @@ -370,7 +370,6 @@ - @@ -1584,6 +1583,7 @@ + diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/SoundProviderMixer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/SoundProviderMixer.cs deleted file mode 100644 index 55df2265f9..0000000000 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/SoundProviderMixer.cs +++ /dev/null @@ -1,213 +0,0 @@ -using BizHawk.Emulation.Common; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum -{ - /// - /// 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) - /// - 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 SoundProviders; - - public SoundProviderMixer(params ISoundProvider[] soundProviders) - { - SoundProviders = new List(); - - 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(); - - 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 - - } -} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs index 5fe2b30996..1c51cbbc10 100644 --- a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs @@ -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 joysticks = new List(); - 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(_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 _tapeInfo = new List(); public List _diskInfo = new List(); - private SoundProviderMixer SoundMixer; + private SyncSoundMixer SoundMixer; private readonly List _files; diff --git a/BizHawk.Emulation.Cores/Sound/SyncSoundMixer.cs b/BizHawk.Emulation.Cores/Sound/SyncSoundMixer.cs new file mode 100644 index 0000000000..2052546040 --- /dev/null +++ b/BizHawk.Emulation.Cores/Sound/SyncSoundMixer.cs @@ -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 +{ + /// + /// 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 + /// + public sealed class SyncSoundMixer : ISoundProvider + { + /// + /// Currently attached ChildProviders + /// + private readonly List _soundProviders = new List(); + + /// + /// The final output max volume + /// + public short FinalMaxVolume + { + get { return _finalMaxVolume; } + set + { + _finalMaxVolume = value; + EqualizeVolumes(); + } + } + private short _finalMaxVolume; + + /// + /// How the sound sources are balanced against each other + /// + public SoundMixBalance MixBalanceMethod + { + get { return _mixBalanceMethod; } + set + { + _mixBalanceMethod = value; + EqualizeVolumes(); + } + } + private SoundMixBalance _mixBalanceMethod; + + /// + /// 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 + /// + private int? _targetSampleCount; + + /// + /// Constructor + /// + /// Whether each providers MaxVolume is reduced to an equal share of the final max volume value + /// The final 'master' max volume + /// + /// 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 + /// + public SyncSoundMixer(SoundMixBalance mixBalanceMethod = SoundMixBalance.Equalize, short maxVolume = short.MaxValue, int? targetSampleCount = null) + { + _mixBalanceMethod = mixBalanceMethod; + _finalMaxVolume = maxVolume; + _targetSampleCount = targetSampleCount; + } + + /// + /// Adds an ISoundProvider to the SyncSoundMixer + /// + /// The source ISoundProvider + /// An ident string for the ISoundProvider (useful when debugging) + /// If this is true then only half the samples should be present + public void PinSource(ISoundProvider source, string sourceDescription) + { + PinSource(source, sourceDescription, FinalMaxVolume); + } + + /// + /// Adds an ISoundProvider to the SyncSoundMixer + /// + /// The source ISoundProvider + /// An ident string for the ISoundProvider (useful when debugging) + /// The MaxVolume level for this particular ISoundProvider + /// If this is true then only half the samples should be present + public void PinSource(ISoundProvider source, string sourceDescription, short sourceMaxVolume) + { + _soundProviders.Add(new ChildProvider + { + SoundProvider = source, + ProviderDescription = sourceDescription, + MaxVolume = sourceMaxVolume + }); + + EqualizeVolumes(); + } + + /// + /// Removes an existing ISoundProvider from the SyncSoundMixer + /// + /// + 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(); + } + + /// + /// Sets each pinned sound provider's MaxVolume based on the MixBalanceMethod + /// + 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; + } + } + + /// + /// Returns the value of the highest nsamp in the SoundProviders collection + /// + /// + 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 + + /// + /// Instantiated for every ISoundProvider source that is added to the mixer + /// + private class ChildProvider + { + /// + /// The Child ISoundProvider + /// + public ISoundProvider SoundProvider; + + /// + /// Identification string + /// + public string ProviderDescription; + + /// + /// The max volume for this provider + /// + public int MaxVolume; + + /// + /// Stores the incoming samples + /// + public short[] InputBuffer; + + /// + /// The incoming number of samples + /// + public int InputNSamp; + + /// + /// Stores the processed samples ready for mixing + /// + public short[] OutputBuffer; + + /// + /// The output number of samples + /// + public int OutputNSamp; + + /// + /// Fetches sample data from the child ISoundProvider + /// + public void GetSamples() + { + SoundProvider.GetSamplesSync(out InputBuffer, out InputNSamp); + } + + /// + /// 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 + /// + /// + 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; + } + } + } + } + + /// + /// Defines how mixed sound sources should be balanced + /// + public enum SoundMixBalance + { + /// + /// Each sound source's max volume will be set to MaxVolume / nSources + /// + Equalize, + /// + /// Each sound source's individual max volume will be respected but the final MaxVolume will be limited to MaxVolume + /// + MasterHardLimit + } +}