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 } }