BizHawk/BizHawk.Emulation.Cores/Sound/SyncSoundMixer.cs

338 lines
8.8 KiB
C#

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