317 lines
11 KiB
C#
317 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
using BizHawk.Client.Common;
|
|
using BizHawk.Emulation.Common;
|
|
|
|
namespace BizHawk.Client.EmuHawk
|
|
{
|
|
// This is intended to be a buffer between a synchronous sound provider and the
|
|
// output device (e.g. DirectSound). The idea is to take advantage of the samples
|
|
// buffered up in the output device so that we don't need to keep a bunch buffered
|
|
// up here. This will keep the latency at a minimum. The goal is to keep zero extra
|
|
// samples here on average. As long as we're within +/-5 milliseconds we don't need
|
|
// to touch the source audio. Once it goes outside of that window, we'll start to
|
|
// perform a "soft" correction by resampling it to hopefully get back inside our
|
|
// window shortly. If it ends up going too low or too high, we will perform a
|
|
// "hard" correction by generating silence or discarding samples.
|
|
public class SoundOutputProvider
|
|
{
|
|
private const int SampleRate = 44100;
|
|
private const int ChannelCount = 2;
|
|
private const int SoftCorrectionThresholdSamples = 5 * SampleRate / 1000;
|
|
private const int StartupMaxSamplesSurplusDeficit = 10 * SampleRate / 1000;
|
|
private const int MaxSamplesSurplus = 50 * SampleRate / 1000;
|
|
private const int UsableHistoryLength = 20;
|
|
private const int MaxHistoryLength = 60;
|
|
private const int SoftCorrectionLength = 240;
|
|
private const int BaseMaxConsecutiveEmptyFrames = 1;
|
|
private const int BaseSampleRateUsableHistoryLength = 60;
|
|
private const int BaseSampleRateMaxHistoryLength = 300;
|
|
private const int MinResamplingDistanceSamples = 3;
|
|
|
|
private Queue<short> _buffer = new Queue<short>();
|
|
|
|
private Queue<int> _extraCountHistory = new Queue<int>();
|
|
private Queue<int> _outputCountHistory = new Queue<int>();
|
|
private Queue<bool> _hardCorrectionHistory = new Queue<bool>();
|
|
|
|
private int _baseConsecutiveEmptyFrames;
|
|
private Queue<bool> _baseEmptyFrameCorrectionHistory = new Queue<bool>();
|
|
|
|
private double _lastAdvertisedSamplesPerFrame;
|
|
private Queue<int> _baseSamplesPerFrame = new Queue<int>();
|
|
|
|
private short[] _outputBuffer = new short[0];
|
|
|
|
private short[] _resampleBuffer = new short[0];
|
|
private double _resampleLengthRoundingError;
|
|
|
|
public SoundOutputProvider()
|
|
{
|
|
}
|
|
|
|
public int MaxSamplesDeficit { get; set; }
|
|
|
|
public ISyncSoundProvider BaseSoundProvider { get; set; }
|
|
|
|
public void DiscardSamples()
|
|
{
|
|
_buffer.Clear();
|
|
_extraCountHistory.Clear();
|
|
_outputCountHistory.Clear();
|
|
_hardCorrectionHistory.Clear();
|
|
_baseConsecutiveEmptyFrames = 0;
|
|
_baseEmptyFrameCorrectionHistory.Clear();
|
|
_lastAdvertisedSamplesPerFrame = 0.0;
|
|
_baseSamplesPerFrame.Clear();
|
|
_outputBuffer = new short[0];
|
|
_resampleBuffer = new short[0];
|
|
_resampleLengthRoundingError = 0.0;
|
|
|
|
if (BaseSoundProvider != null)
|
|
{
|
|
BaseSoundProvider.DiscardSamples();
|
|
}
|
|
}
|
|
|
|
// To let us know about buffer underruns, rewinding, fast-forwarding, etc.
|
|
public void OnVolatility()
|
|
{
|
|
_extraCountHistory.Clear();
|
|
_outputCountHistory.Clear();
|
|
_hardCorrectionHistory.Clear();
|
|
}
|
|
|
|
public bool LogDebug { get; set; }
|
|
|
|
private double AdvertisedSamplesPerFrame
|
|
{
|
|
get { return SampleRate / Global.Emulator.CoreComm.VsyncRate; }
|
|
}
|
|
|
|
public void GetSamples(int idealSampleCount, out short[] samples, out int sampleCount)
|
|
{
|
|
double scaleFactor = 1.0;
|
|
|
|
if (_extraCountHistory.Count >= UsableHistoryLength && !_hardCorrectionHistory.Any(c => c))
|
|
{
|
|
double offsetFromTarget = CalculatePowerMean(_extraCountHistory, 0.6);
|
|
if (Math.Abs(offsetFromTarget) > SoftCorrectionThresholdSamples)
|
|
{
|
|
double correctionSpan = _outputCountHistory.Average() * SoftCorrectionLength;
|
|
scaleFactor *= correctionSpan / (correctionSpan + offsetFromTarget);
|
|
}
|
|
}
|
|
|
|
GetSamplesFromBase(ref scaleFactor);
|
|
|
|
int bufferSampleCount = _buffer.Count / ChannelCount;
|
|
int extraSampleCount = bufferSampleCount - idealSampleCount;
|
|
int maxSamplesDeficit = _extraCountHistory.Count >= UsableHistoryLength ?
|
|
MaxSamplesDeficit : Math.Min(StartupMaxSamplesSurplusDeficit, MaxSamplesDeficit);
|
|
int maxSamplesSurplus = _extraCountHistory.Count >= UsableHistoryLength ?
|
|
MaxSamplesSurplus : Math.Min(StartupMaxSamplesSurplusDeficit, MaxSamplesSurplus);
|
|
bool hardCorrected = false;
|
|
|
|
if (extraSampleCount < -maxSamplesDeficit)
|
|
{
|
|
int generateSampleCount = -extraSampleCount;
|
|
if (LogDebug) Console.WriteLine("Generating " + generateSampleCount + " samples");
|
|
for (int i = 0; i < generateSampleCount * ChannelCount; i++)
|
|
{
|
|
_buffer.Enqueue(0);
|
|
}
|
|
hardCorrected = true;
|
|
}
|
|
else if (extraSampleCount > maxSamplesSurplus)
|
|
{
|
|
int discardSampleCount = extraSampleCount;
|
|
if (LogDebug) Console.WriteLine("Discarding " + discardSampleCount + " samples");
|
|
for (int i = 0; i < discardSampleCount * ChannelCount; i++)
|
|
{
|
|
_buffer.Dequeue();
|
|
}
|
|
hardCorrected = true;
|
|
}
|
|
|
|
bufferSampleCount = _buffer.Count / ChannelCount;
|
|
extraSampleCount = bufferSampleCount - idealSampleCount;
|
|
|
|
int outputSampleCount = Math.Min(idealSampleCount, bufferSampleCount);
|
|
|
|
UpdateHistory(_extraCountHistory, extraSampleCount, MaxHistoryLength);
|
|
UpdateHistory(_outputCountHistory, outputSampleCount, MaxHistoryLength);
|
|
UpdateHistory(_hardCorrectionHistory, hardCorrected, MaxHistoryLength);
|
|
|
|
if (LogDebug)
|
|
{
|
|
Console.WriteLine("Avg: {0:0.0} ms, Min: {1:0.0}, Max: {2:0.0}, Scale: {3:0.0000}",
|
|
CalculatePowerMean(_extraCountHistory, 0.6) * 1000.0 / SampleRate,
|
|
_extraCountHistory.Min() * 1000.0 / SampleRate,
|
|
_extraCountHistory.Max() * 1000.0 / SampleRate,
|
|
scaleFactor);
|
|
}
|
|
|
|
sampleCount = outputSampleCount;
|
|
samples = GetOutputBuffer(sampleCount);
|
|
GetSamplesFromBuffer(samples, sampleCount);
|
|
}
|
|
|
|
private void GetSamplesFromBase(ref double scaleFactor)
|
|
{
|
|
short[] samples;
|
|
int count;
|
|
|
|
BaseSoundProvider.GetSamples(out samples, out count);
|
|
|
|
bool correctedEmptyFrame = false;
|
|
if (count == 0)
|
|
{
|
|
_baseConsecutiveEmptyFrames++;
|
|
if (_baseConsecutiveEmptyFrames > BaseMaxConsecutiveEmptyFrames)
|
|
{
|
|
int silenceCount = (int)Math.Round(AdvertisedSamplesPerFrame);
|
|
samples = Resample(samples, count, silenceCount);
|
|
count = silenceCount;
|
|
correctedEmptyFrame = true;
|
|
}
|
|
}
|
|
else if (_baseConsecutiveEmptyFrames != 0)
|
|
{
|
|
_baseConsecutiveEmptyFrames = 0;
|
|
}
|
|
UpdateHistory(_baseEmptyFrameCorrectionHistory, correctedEmptyFrame, MaxHistoryLength);
|
|
|
|
if (AdvertisedSamplesPerFrame != _lastAdvertisedSamplesPerFrame)
|
|
{
|
|
_baseSamplesPerFrame.Clear();
|
|
_lastAdvertisedSamplesPerFrame = AdvertisedSamplesPerFrame;
|
|
}
|
|
UpdateHistory(_baseSamplesPerFrame, count, BaseSampleRateMaxHistoryLength);
|
|
|
|
if (_baseSamplesPerFrame.Count >= BaseSampleRateUsableHistoryLength &&
|
|
!_baseEmptyFrameCorrectionHistory.Any(n => n))
|
|
{
|
|
double baseAverageSamplesPerFrame = _baseSamplesPerFrame.Average();
|
|
if (baseAverageSamplesPerFrame != 0.0)
|
|
{
|
|
scaleFactor *= AdvertisedSamplesPerFrame / baseAverageSamplesPerFrame;
|
|
}
|
|
}
|
|
|
|
double newCountExact = (count * scaleFactor) + _resampleLengthRoundingError;
|
|
int newCount = (int)Math.Round(newCountExact);
|
|
// Due to small inaccuracies and rounding errors, it's pointless to resample by
|
|
// just a sample or two because those may be fluctuations that will average out
|
|
// over time. So instead of immediately resampling to cover small differences, we
|
|
// will just keep track of it as part of the rounding error and only resample later
|
|
// if a more significant difference accumulates.
|
|
if (Math.Abs(newCount - count) >= MinResamplingDistanceSamples)
|
|
{
|
|
samples = Resample(samples, count, newCount);
|
|
count = newCount;
|
|
}
|
|
// Although the rounding error may seem insignificant, it definitely matters over
|
|
// time so we need to keep track of it. With NTSC @ 59.94 FPS, for example, if we
|
|
// were to always round to 736 samples per frame ignoring the rounding error, we
|
|
// would drift by ~22 milliseconds per minute.
|
|
_resampleLengthRoundingError = newCountExact - count;
|
|
|
|
AddSamplesToBuffer(samples, count);
|
|
}
|
|
|
|
private static double CalculatePowerMean(IEnumerable<int> values, double power)
|
|
{
|
|
double x = values.Average(n => Math.Pow(Math.Abs(n), power) * Math.Sign(n));
|
|
return Math.Pow(Math.Abs(x), 1.0 / power) * Math.Sign(x);
|
|
}
|
|
|
|
private static void UpdateHistory<T>(Queue<T> queue, T value, int maxLength)
|
|
{
|
|
queue.Enqueue(value);
|
|
while (queue.Count > maxLength)
|
|
{
|
|
queue.Dequeue();
|
|
}
|
|
}
|
|
|
|
private void GetSamplesFromBuffer(short[] samples, int count)
|
|
{
|
|
for (int i = 0; i < count * ChannelCount; i++)
|
|
{
|
|
samples[i] = _buffer.Dequeue();
|
|
}
|
|
}
|
|
|
|
private void AddSamplesToBuffer(short[] samples, int count)
|
|
{
|
|
for (int i = 0; i < count * ChannelCount; i++)
|
|
{
|
|
_buffer.Enqueue(samples[i]);
|
|
}
|
|
}
|
|
|
|
private short[] GetOutputBuffer(int count)
|
|
{
|
|
if (_outputBuffer.Length < count * ChannelCount)
|
|
{
|
|
_outputBuffer = new short[count * ChannelCount];
|
|
}
|
|
return _outputBuffer;
|
|
}
|
|
|
|
private short[] GetResampleBuffer(int count)
|
|
{
|
|
if (_resampleBuffer.Length < count * ChannelCount)
|
|
{
|
|
_resampleBuffer = new short[count * ChannelCount];
|
|
}
|
|
return _resampleBuffer;
|
|
}
|
|
|
|
// This uses simple linear interpolation which is supposedly not a great idea for
|
|
// resampling audio, but it sounds surprisingly good to me. Maybe it works well
|
|
// because we are typically stretching by very small amounts.
|
|
private short[] Resample(short[] input, int inputCount, int outputCount)
|
|
{
|
|
if (inputCount == outputCount)
|
|
{
|
|
return input;
|
|
}
|
|
|
|
short[] output = GetResampleBuffer(outputCount);
|
|
|
|
if (inputCount == 0 || outputCount == 0)
|
|
{
|
|
Array.Clear(output, 0, outputCount * ChannelCount);
|
|
return output;
|
|
}
|
|
|
|
for (int iOutput = 0; iOutput < outputCount; iOutput++)
|
|
{
|
|
double iInput = ((double)iOutput / (outputCount - 1)) * (inputCount - 1);
|
|
int iInput0 = (int)iInput;
|
|
int iInput1 = iInput0 + 1;
|
|
double input0Weight = iInput1 - iInput;
|
|
double input1Weight = iInput - iInput0;
|
|
|
|
if (iInput1 == inputCount)
|
|
iInput1 = inputCount - 1;
|
|
|
|
for (int iChannel = 0; iChannel < ChannelCount; iChannel++)
|
|
{
|
|
double value =
|
|
input[iInput0 * ChannelCount + iChannel] * input0Weight +
|
|
input[iInput1 * ChannelCount + iChannel] * input1Weight;
|
|
|
|
output[iOutput * ChannelCount + iChannel] = (short)((int)(value + 32768.5) - 32768);
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
}
|
|
}
|