diff --git a/BizHawk.Emulation/BizHawk.Emulation.csproj b/BizHawk.Emulation/BizHawk.Emulation.csproj index 6a62b17f65..78c6dcb950 100644 --- a/BizHawk.Emulation/BizHawk.Emulation.csproj +++ b/BizHawk.Emulation/BizHawk.Emulation.csproj @@ -357,6 +357,9 @@ + + + diff --git a/BizHawk.Emulation/Consoles/Nintendo/SNES/LibsnesCore.cs b/BizHawk.Emulation/Consoles/Nintendo/SNES/LibsnesCore.cs index 9d988c767a..8ab5ebb941 100644 --- a/BizHawk.Emulation/Consoles/Nintendo/SNES/LibsnesCore.cs +++ b/BizHawk.Emulation/Consoles/Nintendo/SNES/LibsnesCore.cs @@ -193,7 +193,7 @@ namespace BizHawk.Emulation.Consoles.Nintendo.SNES string key = "P" + (1 + port) + " "; if ((LibsnesDll.SNES_DEVICE)device == LibsnesDll.SNES_DEVICE.JOYPAD) { - switch((LibsnesDll.SNES_DEVICE_ID)id) + switch ((LibsnesDll.SNES_DEVICE_ID)id) { case LibsnesDll.SNES_DEVICE_ID.JOYPAD_A: key += "A"; break; case LibsnesDll.SNES_DEVICE_ID.JOYPAD_B: key += "B"; break; @@ -262,11 +262,11 @@ namespace BizHawk.Emulation.Consoles.Nintendo.SNES int IVideoProvider.BackgroundColor { get { return 0; } } int[] IVideoProvider.GetVideoBuffer() { return vidBuffer; } int IVideoProvider.VirtualWidth { get { return vidWidth; } } - int IVideoProvider.BufferWidth { get { return vidWidth; } } + int IVideoProvider.BufferWidth { get { return vidWidth; } } int IVideoProvider.BufferHeight { get { return vidHeight; } } - int[] vidBuffer = new int[256*256]; - int vidWidth=256, vidHeight=256; + int[] vidBuffer = new int[256 * 256]; + int vidWidth = 256, vidHeight = 256; public IVideoProvider VideoProvider { get { return this; } } public ISoundProvider SoundProvider { get { return this; } } @@ -287,7 +287,7 @@ namespace BizHawk.Emulation.Consoles.Nintendo.SNES "P1 Up", "P1 Down", "P1 Left", "P1 Right", "P1 Select", "P1 Start", "P1 B", "P1 A", "P1 X", "P1 Y", "P1 L", "P1 R", "Reset", } }; - + int timeFrameCounter; public int Frame { get { return timeFrameCounter; } } public int LagCount { get; set; } @@ -302,7 +302,7 @@ namespace BizHawk.Emulation.Consoles.Nintendo.SNES return LibsnesDll.snes_get_memory_size(LibsnesDll.SNES_MEMORY.CARTRIDGE_RAM) != 0; } } - + public byte[] ReadSaveRam { get { return snes_get_memory_data_read(LibsnesDll.SNES_MEMORY.CARTRIDGE_RAM); } } public static byte[] snes_get_memory_data_read(LibsnesDll.SNES_MEMORY id) { @@ -310,7 +310,7 @@ namespace BizHawk.Emulation.Consoles.Nintendo.SNES if (size == 0) return new byte[0]; var data = LibsnesDll.snes_get_memory_data(id); var ret = new byte[size]; - Marshal.Copy(data,ret,0,size); + Marshal.Copy(data, ret, 0, size); return ret; } @@ -335,7 +335,7 @@ namespace BizHawk.Emulation.Consoles.Nintendo.SNES state.ReadFromHex(hex); LoadStateBinary(new BinaryReader(new MemoryStream(state))); } - + public void SaveStateBinary(BinaryWriter writer) { int size = LibsnesDll.snes_serialize_size(); @@ -376,7 +376,7 @@ namespace BizHawk.Emulation.Consoles.Nintendo.SNES int mask = size - 1; byte* blockptr = (byte*)block.ToPointer(); MemoryDomain md; - + //have to bitmask these somehow because it's unmanaged memory and we would hate to clobber things or make them nondeterministic if (Util.IsPowerOfTwo(size)) { @@ -406,7 +406,7 @@ namespace BizHawk.Emulation.Consoles.Nintendo.SNES void SetupMemoryDomains(byte[] romData) { MemoryDomains = new List(); - + var romDomain = new MemoryDomain("CARTROM", romData.Length, Endian.Little, (addr) => romData[addr], (addr, value) => romData[addr] = value); @@ -423,88 +423,88 @@ namespace BizHawk.Emulation.Consoles.Nintendo.SNES public MemoryDomain MainMemory { get; private set; } - Queue AudioBuffer = new Queue(); + + + #region audio stuff + + /// stores input samples as they come from the libsnes core + Queue AudioInBuffer = new Queue(); + /// stores samples that have been converted to 44100hz + Queue AudioOutBuffer = new Queue(); GCHandle _gc_snes_audio_sample; + + /// total number of samples (left and right combined) in the InBuffer before we ask for resampling + const int resamplechunk = 1000; + /// actual sampling factor used + const double sfactor = 44100.0 / 32040.5; + + Sound.Utilities.BizhawkResampler resampler = new Sound.Utilities.BizhawkResampler(false, sfactor, sfactor); + + Sound.MetaspuSoundProvider metaspu = new Sound.MetaspuSoundProvider(Sound.ESynchMethod.ESynchMethod_Z); + void snes_audio_sample(ushort left, ushort right) { - AudioBuffer.Enqueue((short)left); - AudioBuffer.Enqueue((short)right); - } + AudioInBuffer.Enqueue((short)left); + AudioInBuffer.Enqueue((short)right); - - /// - /// basic linear audio resampler. sampling rate is inferred from buffer sizes - /// - /// stereo s16 - /// stereo s16 - static void LinearDownsampler(short[] input, short[] output) - { - // TODO - this also appears in YM2612.cs ... move to common if it's found useful - - double samplefactor = (input.Length - 2) / (double)output.Length; - - for (int i = 0; i < output.Length / 2; i++) + try { - // exact position on input stream - double inpos = i * samplefactor; - // selected interpolation points and weights - int pt0 = (int)inpos; // pt1 = pt0 + 1 - double wt1 = inpos - pt0; // wt0 = 1 - wt1 - double wt0 = 1.0 - wt1; + // fixme: i get all sorts of crashes if i do the resampling in here. what? - output[i * 2 + 0] = (short)(input[pt0 * 2 + 0] * wt0 + input[pt0 * 2 + 2] * wt1); - output[i * 2 + 1] = (short)(input[pt0 * 2 + 1] * wt0 + input[pt0 * 2 + 3] * wt1); + /* + if (AudioInBuffer.Count >= resamplechunk) + { + resampler.process(sfactor, false, AudioInBuffer, AudioOutBuffer); + // drain into the metaspu immediately + // we could skip this step and drain directly by changing SampleBuffers + while (AudioOutBuffer.Count > 0) + metaspu.buffer.enqueue_sample(AudioOutBuffer.Dequeue(), AudioOutBuffer.Dequeue()); + } + */ } + catch (Exception e) + { + System.Windows.Forms.MessageBox.Show(e.ToString()); + AudioOutBuffer.Clear(); + } + } - // TODO: replace this with something that took more than 5 seconds to write + + //BinaryWriter dbgs = new BinaryWriter(File.Open("dbgwav.raw", FileMode.Create, FileAccess.Write)); + public void GetSamples(short[] samples) { - // resample approximately 32k->44k - int inputcount = samples.Length * 32040 / 44100; - inputcount /= 2; + // do resampling here, instead of when the samples are generated; see "fixme" comment in snes_audio_sample() - if (inputcount < 2) inputcount = 2; - - short[] input = new short[inputcount * 2]; - - int i; - - for (i = 0; i < inputcount * 2 && AudioBuffer.Count > 0; i++) - input[i] = AudioBuffer.Dequeue(); - short lastl, lastr; - if (i >= 2) + if (true) { - lastl = input[i - 2]; - lastr = input[i - 1]; + resampler.process(sfactor, false, AudioInBuffer, AudioOutBuffer); + + // drain into the metaspu immediately + // we could skip this step and drain directly by changing SampleBuffers implementation + while (AudioOutBuffer.Count > 0) + { + short l = AudioOutBuffer.Dequeue(); + short r = AudioOutBuffer.Dequeue(); + //metaspu.buffer.enqueue_sample(AudioOutBuffer.Dequeue(), AudioOutBuffer.Dequeue()); + metaspu.buffer.enqueue_sample(l, r); + //dbgs.Write(l); + //dbgs.Write(r); + } } - else - lastl = lastr = 0; - for (; i < inputcount * 2; ) - { - input[i++] = lastl; - input[i++] = lastr; - } - - - LinearDownsampler(input, samples); - - // drop if too many - if (AudioBuffer.Count > samples.Length * 3) - AudioBuffer.Clear(); + metaspu.GetSamples(samples); } public void DiscardSamples() { - AudioBuffer.Clear(); + metaspu.DiscardSamples(); } - // ignore for now - public int MaxVolume - { - get; - set; - } + public int MaxVolume { get; set; } + + #endregion audio stuff + } } \ No newline at end of file diff --git a/BizHawk.Emulation/Sound/Utilities/FilterKit.cs b/BizHawk.Emulation/Sound/Utilities/FilterKit.cs new file mode 100644 index 0000000000..5a696caedc --- /dev/null +++ b/BizHawk.Emulation/Sound/Utilities/FilterKit.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BizHawk.Emulation.Sound.Utilities +{ + /****************************************************************************** + * + * libresample4j + * Copyright (c) 2009 Laszlo Systems, Inc. All Rights Reserved. + * + * libresample4j is a Java port of Dominic Mazzoni's libresample 0.1.3, + * which is in turn based on Julius Smith's Resample 1.7 library. + * http://www-ccrma.stanford.edu/~jos/resample/ + * + * License: LGPL -- see the file LICENSE.txt for more information + * + *****************************************************************************/ + + /** + * This file provides Kaiser-windowed low-pass filter support, + * including a function to create the filter coefficients, and + * two functions to apply the filter at a particular point. + * + *
+	 * reference: "Digital Filters, 2nd edition"
+	 *            R.W. Hamming, pp. 178-179
+	 *
+	 * Izero() computes the 0th order modified bessel function of the first kind.
+	 *    (Needed to compute Kaiser window).
+	 *
+	 * LpFilter() computes the coeffs of a Kaiser-windowed low pass filter with
+	 *    the following characteristics:
+	 *
+	 *       c[]  = array in which to store computed coeffs
+	 *       frq  = roll-off frequency of filter
+	 *       N    = Half the window length in number of coeffs
+	 *       Beta = parameter of Kaiser window
+	 *       Num  = number of coeffs before 1/frq
+	 *
+	 * Beta trades the rejection of the lowpass filter against the transition
+	 *    width from passband to stopband.  Larger Beta means a slower
+	 *    transition and greater stopband rejection.  See Rabiner and Gold
+	 *    (Theory and Application of DSP) under Kaiser windows for more about
+	 *    Beta.  The following table from Rabiner and Gold gives some feel
+	 *    for the effect of Beta:
+	 *
+	 * All ripples in dB, width of transition band = D*N where N = window length
+	 *
+	 *               BETA    D       PB RIP   SB RIP
+	 *               2.120   1.50  +-0.27      -30
+	 *               3.384   2.23    0.0864    -40
+	 *               4.538   2.93    0.0274    -50
+	 *               5.658   3.62    0.00868   -60
+	 *               6.764   4.32    0.00275   -70
+	 *               7.865   5.0     0.000868  -80
+	 *               8.960   5.7     0.000275  -90
+	 *               10.056  6.4     0.000087  -100
+	 * 
+ */ + public static class FilterKit + { + + // Max error acceptable in Izero + private static double IzeroEPSILON = 1E-21; + + private static double Izero(double x) + { + double sum, u, halfx, temp; + int n; + + sum = u = n = 1; + halfx = x / 2.0; + do + { + temp = halfx / (double)n; + n += 1; + temp *= temp; + u *= temp; + sum += u; + } while (u >= IzeroEPSILON * sum); + return (sum); + } + + public static void lrsLpFilter(double[] c, int N, double frq, double Beta, int Num) + { + double IBeta, temp, temp1, inm1; + int i; + + // Calculate ideal lowpass filter impulse response coefficients: + c[0] = 2.0 * frq; + for (i = 1; i < N; i++) + { + temp = Math.PI * (double)i / (double)Num; + c[i] = Math.Sin(2.0 * temp * frq) / temp; // Analog sinc function, + // cutoff = frq + } + + /* + * Calculate and Apply Kaiser window to ideal lowpass filter. Note: last + * window value is IBeta which is NOT zero. You're supposed to really + * truncate the window here, not ramp it to zero. This helps reduce the + * first sidelobe. + */ + IBeta = 1.0 / Izero(Beta); + inm1 = 1.0 / ((double)(N - 1)); + for (i = 1; i < N; i++) + { + temp = (double)i * inm1; + temp1 = 1.0 - temp * temp; + temp1 = (temp1 < 0 ? 0 : temp1); /* + * make sure it's not negative + * since we're taking the square + * root - this happens on Pentium + * 4's due to tiny roundoff errors + */ + c[i] *= Izero(Beta * Math.Sqrt(temp1)) * IBeta; + } + } + + /// + /// + /// + /// impulse response + /// impulse response deltas + /// length of one wing of filter + /// Interpolate coefs using deltas? + /// Current sample array + /// Current sample index + /// Phase + /// increment (1 for right wing or -1 for left) + /// + public static float lrsFilterUp(float[] Imp, float[] ImpD, int Nwing, bool Interp, float[] Xp_array, int Xp_index, double Ph, int Inc) + { + double a = 0; + float v, t; + + Ph *= Resampler.Npc; // Npc is number of values per 1/delta in impulse + // response + + v = 0.0f; // The output value + + float[] Hp_array = Imp; + int Hp_index = (int)Ph; + + float[] End_array = Imp; + int End_index = Nwing; + + float[] Hdp_array = ImpD; + int Hdp_index = (int)Ph; + + if (Interp) + { + // Hdp = &ImpD[(int)Ph]; + a = Ph - Math.Floor(Ph); /* fractional part of Phase */ + } + + if (Inc == 1) // If doing right wing... + { // ...drop extra coeff, so when Ph is + End_index--; // 0.5, we don't do too many mult's + if (Ph == 0) // If the phase is zero... + { // ...then we've already skipped the + Hp_index += Resampler.Npc; // first sample, so we must also + Hdp_index += Resampler.Npc; // skip ahead in Imp[] and ImpD[] + } + } + + if (Interp) + while (Hp_index < End_index) + { + t = Hp_array[Hp_index]; /* Get filter coeff */ + t += (float)(Hdp_array[Hdp_index] * a); /* t is now interp'd filter coeff */ + Hdp_index += Resampler.Npc; /* Filter coeff differences step */ + t *= Xp_array[Xp_index]; /* Mult coeff by input sample */ + v += t; /* The filter output */ + Hp_index += Resampler.Npc; /* Filter coeff step */ + Xp_index += Inc; /* Input signal step. NO CHECK ON BOUNDS */ + } + else + while (Hp_index < End_index) + { + t = Hp_array[Hp_index]; /* Get filter coeff */ + t *= Xp_array[Xp_index]; /* Mult coeff by input sample */ + v += t; /* The filter output */ + Hp_index += Resampler.Npc; /* Filter coeff step */ + Xp_index += Inc; /* Input signal step. NO CHECK ON BOUNDS */ + } + + return v; + } + + /// + /// + /// + /// impulse response + /// impulse response deltas + /// length of one wing of filter + /// Interpolate coefs using deltas? + /// Current sample array + /// Current sample index + /// Phase + /// increment (1 for right wing or -1 for left) + /// filter sampling period + /// + public static float lrsFilterUD(float[] Imp, float[] ImpD, int Nwing, bool Interp, float[] Xp_array, int Xp_index, double Ph, int Inc, double dhb) + { + float a; + float v, t; + double Ho; + + v = 0.0f; // The output value + Ho = Ph * dhb; + + float[] End_array = Imp; + int End_index = Nwing; + + if (Inc == 1) // If doing right wing... + { // ...drop extra coeff, so when Ph is + End_index--; // 0.5, we don't do too many mult's + if (Ph == 0) // If the phase is zero... + Ho += dhb; // ...then we've already skipped the + } // first sample, so we must also + // skip ahead in Imp[] and ImpD[] + + float[] Hp_array = Imp; + int Hp_index; + + if (Interp) + { + float[] Hdp_array = ImpD; + int Hdp_index; + + while ((Hp_index = (int)Ho) < End_index) + { + t = Hp_array[Hp_index]; // Get IR sample + Hdp_index = (int)Ho; // get interp bits from diff table + a = (float)(Ho - Math.Floor(Ho)); // a is logically between 0 + // and 1 + t += Hdp_array[Hdp_index] * a; // t is now interp'd filter coeff + t *= Xp_array[Xp_index]; // Mult coeff by input sample + v += t; // The filter output + Ho += dhb; // IR step + Xp_index += Inc; // Input signal step. NO CHECK ON BOUNDS + } + } + else + { + while ((Hp_index = (int)Ho) < End_index) + { + t = Hp_array[Hp_index]; // Get IR sample + t *= Xp_array[Xp_index]; // Mult coeff by input sample + v += t; // The filter output + Ho += dhb; // IR step + Xp_index += Inc; // Input signal step. NO CHECK ON BOUNDS + } + } + + return v; + } + + } +} diff --git a/BizHawk.Emulation/Sound/Utilities/Resampler.cs b/BizHawk.Emulation/Sound/Utilities/Resampler.cs new file mode 100644 index 0000000000..f1709f7a56 --- /dev/null +++ b/BizHawk.Emulation/Sound/Utilities/Resampler.cs @@ -0,0 +1,644 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BizHawk.Emulation.Sound.Utilities +{ + /****************************************************************************** + * + * libresample4j + * Copyright (c) 2009 Laszlo Systems, Inc. All Rights Reserved. + * + * libresample4j is a Java port of Dominic Mazzoni's libresample 0.1.3, + * which is in turn based on Julius Smith's Resample 1.7 library. + * http://www-ccrma.stanford.edu/~jos/resample/ + * + * License: LGPL -- see the file LICENSE.txt for more information + * + *****************************************************************************/ + + //import java.nio.FloatBuffer; + + public class Resampler + { + + public class Result + { + public int inputSamplesConsumed { get; private set; } + public int outputSamplesGenerated { get; private set; } + + public Result(int inputSamplesConsumed, int outputSamplesGenerated) + { + this.inputSamplesConsumed = inputSamplesConsumed; + this.outputSamplesGenerated = outputSamplesGenerated; + } + } + + /// number of values per 1/delta in impulse response + public const int Npc = 4096; + + private float[] Imp; + private float[] ImpD; + private float LpScl; + private int Nmult; + private int Nwing; + private double minFactor; + private double maxFactor; + private int XSize; + private float[] X; + /// + /// Current "now"-sample pointer for input + /// + private int Xp; + /// + /// Position to put new samples + /// + private int Xread; + private int Xoff; + private float[] Y; + private int Yp; + private double Time; + + /** + * Clone an existing resampling session. Faster than creating one from scratch. + * + * @param other + */ + public Resampler(Resampler other) + { + this.Imp = (float[])other.Imp.Clone(); + this.ImpD = (float[])other.ImpD.Clone(); + this.LpScl = other.LpScl; + this.Nmult = other.Nmult; + this.Nwing = other.Nwing; + this.minFactor = other.minFactor; + this.maxFactor = other.maxFactor; + this.XSize = other.XSize; + this.X = (float[])other.X.Clone(); + this.Xp = other.Xp; + this.Xread = other.Xread; + this.Xoff = other.Xoff; + this.Y = (float[])other.Y.Clone(); + this.Yp = other.Yp; + this.Time = other.Time; + } + + /** + * Create a new resampling session. + * + * @param highQuality true for better quality, slower processing time + * @param minFactor lower bound on resampling factor for this session + * @param maxFactor upper bound on resampling factor for this session + * @throws IllegalArgumentException if minFactor or maxFactor is not + * positive, or if maxFactor is less than minFactor + */ + public Resampler(bool highQuality, double minFactor, double maxFactor) + { + if (minFactor <= 0.0 || maxFactor <= 0.0) + throw new ArgumentException("minFactor and maxFactor must be positive"); + + if (maxFactor < minFactor) + throw new ArgumentException("minFactor must be <= maxFactor"); + + this.minFactor = minFactor; + this.maxFactor = maxFactor; + this.Nmult = highQuality ? 35 : 11; + this.LpScl = 1.0f; + this.Nwing = Npc * (this.Nmult - 1) / 2; // # of filter coeffs in right wing + + double Rolloff = 0.90; + double Beta = 6; + + double[] Imp64 = new double[this.Nwing]; + + FilterKit.lrsLpFilter(Imp64, this.Nwing, 0.5 * Rolloff, Beta, Npc); + this.Imp = new float[this.Nwing]; + this.ImpD = new float[this.Nwing]; + + for (int i = 0; i < this.Nwing; i++) + { + this.Imp[i] = (float)Imp64[i]; + } + + // Storing deltas in ImpD makes linear interpolation + // of the filter coefficients faster + for (int i = 0; i < this.Nwing - 1; i++) + { + this.ImpD[i] = this.Imp[i + 1] - this.Imp[i]; + } + + // Last coeff. not interpolated + this.ImpD[this.Nwing - 1] = -this.Imp[this.Nwing - 1]; + + // Calc reach of LP filter wing (plus some creeping room) + int Xoff_min = (int)(((this.Nmult + 1) / 2.0) * Math.Max(1.0, 1.0 / minFactor) + 10); + int Xoff_max = (int)(((this.Nmult + 1) / 2.0) * Math.Max(1.0, 1.0 / maxFactor) + 10); + this.Xoff = Math.Max(Xoff_min, Xoff_max); + + // Make the inBuffer size at least 4096, but larger if necessary + // in order to store the minimum reach of the LP filter and then some. + // Then allocate the buffer an extra Xoff larger so that + // we can zero-pad up to Xoff zeros at the end when we reach the + // end of the input samples. + this.XSize = Math.Max(2 * this.Xoff + 10, 4096); + this.X = new float[this.XSize + this.Xoff]; + this.Xp = this.Xoff; + this.Xread = this.Xoff; + + // Make the outBuffer long enough to hold the entire processed + // output of one inBuffer + int YSize = (int)(((double)this.XSize) * maxFactor + 2.0); + this.Y = new float[YSize]; + this.Yp = 0; + + this.Time = (double)this.Xoff; // Current-time pointer for converter + } + + public int getFilterWidth() + { + return this.Xoff; + } + + /** + * Process a batch of samples. There is no guarantee that the input buffer will be drained. + * + * @param factor factor at which to resample this batch + * @param buffers sample buffer for producing input and consuming output + * @param lastBatch true if this is known to be the last batch of samples + * @return true iff resampling is complete (ie. no input samples consumed and no output samples produced) + */ + public bool process(double factor, SampleBuffers buffers, bool lastBatch) + { + if (factor < this.minFactor || factor > this.maxFactor) + { + throw new ArgumentException("factor " + factor + " is not between minFactor=" + minFactor + + " and maxFactor=" + maxFactor); + } + + int outBufferLen = buffers.getOutputBufferLength(); + int inBufferLen = buffers.getInputBufferLength(); + + float[] Imp = this.Imp; + float[] ImpD = this.ImpD; + float LpScl = this.LpScl; + int Nwing = this.Nwing; + bool interpFilt = false; // TRUE means interpolate filter coeffs + + int inBufferUsed = 0; + int outSampleCount = 0; + + // Start by copying any samples still in the Y buffer to the output + // buffer + if ((this.Yp != 0) && (outBufferLen - outSampleCount) > 0) + { + int len = Math.Min(outBufferLen - outSampleCount, this.Yp); + + buffers.consumeOutput(this.Y, 0, len); + //for (int i = 0; i < len; i++) { + // outBuffer[outBufferOffset + outSampleCount + i] = this.Y[i]; + //} + + outSampleCount += len; + for (int i = 0; i < this.Yp - len; i++) + { + this.Y[i] = this.Y[i + len]; + } + this.Yp -= len; + } + + // If there are still output samples left, return now - we need + // the full output buffer available to us... + if (this.Yp != 0) + { + return inBufferUsed == 0 && outSampleCount == 0; + } + + // Account for increased filter gain when using factors less than 1 + if (factor < 1) + { + LpScl = (float)(LpScl * factor); + } + + while (true) + { + + // This is the maximum number of samples we can process + // per loop iteration + + /* + * #ifdef DEBUG + * printf("XSize: %d Xoff: %d Xread: %d Xp: %d lastFlag: %d\n", + * this.XSize, this.Xoff, this.Xread, this.Xp, lastFlag); #endif + */ + + // Copy as many samples as we can from the input buffer into X + int len = this.XSize - this.Xread; + + if (len >= inBufferLen - inBufferUsed) + { + len = inBufferLen - inBufferUsed; + } + + buffers.produceInput(this.X, this.Xread, len); + //for (int i = 0; i < len; i++) { + // this.X[this.Xread + i] = inBuffer[inBufferOffset + inBufferUsed + i]; + //} + + inBufferUsed += len; + this.Xread += len; + + int Nx; + if (lastBatch && (inBufferUsed == inBufferLen)) + { + // If these are the last samples, zero-pad the + // end of the input buffer and make sure we process + // all the way to the end + Nx = this.Xread - this.Xoff; + for (int i = 0; i < this.Xoff; i++) + { + this.X[this.Xread + i] = 0; + } + } + else + { + Nx = this.Xread - 2 * this.Xoff; + } + + /* + * #ifdef DEBUG fprintf(stderr, "new len=%d Nx=%d\n", len, Nx); + * #endif + */ + + if (Nx <= 0) + { + break; + } + + // Resample stuff in input buffer + int Nout; + if (factor >= 1) + { // SrcUp() is faster if we can use it */ + Nout = lrsSrcUp(this.X, this.Y, factor, /* &this.Time, */Nx, Nwing, LpScl, Imp, ImpD, interpFilt); + } + else + { + Nout = lrsSrcUD(this.X, this.Y, factor, /* &this.Time, */Nx, Nwing, LpScl, Imp, ImpD, interpFilt); + } + + /* + * #ifdef DEBUG + * printf("Nout: %d\n", Nout); + * #endif + */ + + this.Time -= Nx; // Move converter Nx samples back in time + this.Xp += Nx; // Advance by number of samples processed + + // Calc time accumulation in Time + int Ncreep = (int)(this.Time) - this.Xoff; + if (Ncreep != 0) + { + this.Time -= Ncreep; // Remove time accumulation + this.Xp += Ncreep; // and add it to read pointer + } + + // Copy part of input signal that must be re-used + int Nreuse = this.Xread - (this.Xp - this.Xoff); + + for (int i = 0; i < Nreuse; i++) + { + this.X[i] = this.X[i + (this.Xp - this.Xoff)]; + } + + /* + #ifdef DEBUG + printf("New Xread=%d\n", Nreuse); + #endif */ + + this.Xread = Nreuse; // Pos in input buff to read new data into + this.Xp = this.Xoff; + + this.Yp = Nout; + + // Copy as many samples as possible to the output buffer + if (this.Yp != 0 && (outBufferLen - outSampleCount) > 0) + { + len = Math.Min(outBufferLen - outSampleCount, this.Yp); + + buffers.consumeOutput(this.Y, 0, len); + //for (int i = 0; i < len; i++) { + // outBuffer[outBufferOffset + outSampleCount + i] = this.Y[i]; + //} + + outSampleCount += len; + for (int i = 0; i < this.Yp - len; i++) + { + this.Y[i] = this.Y[i + len]; + } + this.Yp -= len; + } + + // If there are still output samples left, return now, + // since we need the full output buffer available + if (this.Yp != 0) + { + break; + } + } + + return inBufferUsed == 0 && outSampleCount == 0; + } + + /** + * Process a batch of samples. Convenience method for when the input and output are both floats. + * + * @param factor factor at which to resample this batch + * @param inputBuffer contains input samples in the range -1.0 to 1.0 + * @param outputBuffer output samples will be deposited here + * @param lastBatch true if this is known to be the last batch of samples + * @return true iff resampling is complete (ie. no input samples consumed and no output samples produced) + */ + /* + public bool process(double factor, FloatBuffer inputBuffer, bool lastBatch, FloatBuffer outputBuffer) { + SampleBuffers sampleBuffers = new SampleBuffers() { + public int getInputBufferLength() { + return inputBuffer.remaining(); + } + + public int getOutputBufferLength() { + return outputBuffer.remaining(); + } + + public void produceInput(float[] array, int offset, int length) { + inputBuffer.get(array, offset, length); + } + + public void consumeOutput(float[] array, int offset, int length) { + outputBuffer.put(array, offset, length); + } + }; + return process(factor, sampleBuffers, lastBatch); + } + */ + + /** + * Process a batch of samples. Alternative interface if you prefer to work with arrays. + * + * @param factor resampling rate for this batch + * @param inBuffer array containing input samples in the range -1.0 to 1.0 + * @param inBufferOffset offset into inBuffer at which to start processing + * @param inBufferLen number of valid elements in the inputBuffer + * @param lastBatch pass true if this is the last batch of samples + * @param outBuffer array to hold the resampled data + * @return the number of samples consumed and generated + */ + /* + public Result process(double factor, float[] inBuffer, int inBufferOffset, int inBufferLen, boolean lastBatch, float[] outBuffer, int outBufferOffset, int outBufferLen) { + FloatBuffer inputBuffer = FloatBuffer.wrap(inBuffer, inBufferOffset, inBufferLen); + FloatBuffer outputBuffer = FloatBuffer.wrap(outBuffer, outBufferOffset, outBufferLen); + + process(factor, inputBuffer, lastBatch, outputBuffer); + + return new Result(inputBuffer.position() - inBufferOffset, outputBuffer.position() - outBufferOffset); + } + */ + + /// + /// Sampling rate up-conversion only subroutine; Slightly faster than down-conversion + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + private int lrsSrcUp(float[] X, float[] Y, double factor, int Nx, int Nwing, float LpScl, float[] Imp, + float[] ImpD, bool Interp) + { + + float[] Xp_array = X; + int Xp_index; + + float[] Yp_array = Y; + int Yp_index = 0; + + float v; + + double CurrentTime = this.Time; + double dt; // Step through input signal + double endTime; // When Time reaches EndTime, return to user + + dt = 1.0 / factor; // Output sampling period + + endTime = CurrentTime + Nx; + while (CurrentTime < endTime) + { + double LeftPhase = CurrentTime - Math.Floor(CurrentTime); + double RightPhase = 1.0 - LeftPhase; + + Xp_index = (int)CurrentTime; // Ptr to current input sample + // Perform left-wing inner product + v = FilterKit.lrsFilterUp(Imp, ImpD, Nwing, Interp, Xp_array, Xp_index++, LeftPhase, -1); + // Perform right-wing inner product + v += FilterKit.lrsFilterUp(Imp, ImpD, Nwing, Interp, Xp_array, Xp_index, RightPhase, 1); + + v *= LpScl; // Normalize for unity filter gain + + Yp_array[Yp_index++] = v; // Deposit output + CurrentTime += dt; // Move to next sample by time increment + } + + this.Time = CurrentTime; + return Yp_index; // Return the number of output samples + } + + private int lrsSrcUD(float[] X, float[] Y, double factor, int Nx, int Nwing, float LpScl, float[] Imp, + float[] ImpD, bool Interp) + { + + float[] Xp_array = X; + int Xp_index; + + float[] Yp_array = Y; + int Yp_index = 0; + + float v; + + double CurrentTime = this.Time; + double dh; // Step through filter impulse response + double dt; // Step through input signal + double endTime; // When Time reaches EndTime, return to user + + dt = 1.0 / factor; // Output sampling period + + dh = Math.Min(Npc, factor * Npc); // Filter sampling period + + endTime = CurrentTime + Nx; + while (CurrentTime < endTime) + { + double LeftPhase = CurrentTime - Math.Floor(CurrentTime); + double RightPhase = 1.0 - LeftPhase; + + Xp_index = (int)CurrentTime; // Ptr to current input sample + // Perform left-wing inner product + v = FilterKit.lrsFilterUD(Imp, ImpD, Nwing, Interp, Xp_array, Xp_index++, LeftPhase, -1, dh); + // Perform right-wing inner product + v += FilterKit.lrsFilterUD(Imp, ImpD, Nwing, Interp, Xp_array, Xp_index, RightPhase, 1, dh); + + v *= LpScl; // Normalize for unity filter gain + + Yp_array[Yp_index++] = v; // Deposit output + + CurrentTime += dt; // Move to next sample by time increment + } + + this.Time = CurrentTime; + return Yp_index; // Return the number of output samples + } + + } + + /// + /// implements a pair of Resamplers that work off of interleaved stereo buffers of shorts + /// this is what's used inside most of bizhawk + /// + public class BizhawkResampler + { + private Resampler left; + private Resampler right; + + public BizhawkResampler(bool highQuality, double minFactor, double maxFactor) + { + left = new Resampler(highQuality, minFactor, maxFactor); + right = new Resampler(highQuality, minFactor, maxFactor); + } + + + + /// + /// resample some audio. + /// + /// outrate / inrate + /// true to finalize (empty buffers and finish) + /// input samples, as interleaved stereo shorts. in general, all won't be used unless lastbatch = true + /// recieves output samples, as interleaved stereo shorts + /// true if processing finished; only possible with lastbatch = true + public bool process(double factor, bool lastBatch, Queue input, Queue output) + { + LeftBuffer lb = new LeftBuffer(input, output); + + SampleBuffers rb = lb.GetRightBuffer(); + + bool doneleft = left.process(factor, lb, lastBatch); + bool doneright = right.process(factor, rb, lastBatch); + + return doneleft && doneright; + } + + /// + /// ugly wrapper class that handles interleaved writes and reads + /// made under the assumption that LeftBuffer gets called first + /// both buffers must consume and produce the same amount of data, or else + /// + class LeftBuffer : SampleBuffers + { + int inbufferlen; + + Queue input; + Queue output; + + Queue rightin = new Queue(); + Queue leftout = new Queue(); + + RightBuffer child; + + public SampleBuffers GetRightBuffer() + { + return child; + } + + public LeftBuffer(Queue input, Queue output) + { + this.input = input; + this.output = output; + if (input.Count % 2 != 0) + throw new ArgumentException("Input must contain an even number of samples! (stereo stacked)"); + inbufferlen = input.Count / 2; + child = new RightBuffer(this); + } + + public int getInputBufferLength() + { + return inbufferlen; + } + + public int getOutputBufferLength() + { + // queues resize, so don't care + return 1000000; + } + + public void produceInput(float[] array, int offset, int length) + { + for (int i = offset; i < length + offset; i++) + { + // remove left sample + short left = input.Dequeue(); + // remove right sample and save for later + rightin.Enqueue(input.Dequeue()); + + array[i] = left / 32768.0f; + } + } + + public void consumeOutput(float[] array, int offset, int length) + { + for (int i = offset; i < length + offset; i++) + leftout.Enqueue(array[i]); + } + + class RightBuffer : SampleBuffers + { + LeftBuffer parent; + public RightBuffer(LeftBuffer parent) + { + this.parent = parent; + } + + public int getInputBufferLength() + { + return parent.getInputBufferLength(); + } + + public int getOutputBufferLength() + { + return parent.getOutputBufferLength(); + } + + public void produceInput(float[] array, int offset, int length) + { + for (int i = offset; i < length + offset; i++) + array[i] = parent.rightin.Dequeue() / 32768.0f; + } + + public void consumeOutput(float[] array, int offset, int length) + { + for (int i = offset; i < length + offset; i++) + { + parent.output.Enqueue((short)(parent.leftout.Dequeue() * 32768.0f)); + parent.output.Enqueue((short)(array[i] * 32768.0f)); + } + } + } + + } + + + } + +} diff --git a/BizHawk.Emulation/Sound/Utilities/SampleBuffers.cs b/BizHawk.Emulation/Sound/Utilities/SampleBuffers.cs new file mode 100644 index 0000000000..eee23758ac --- /dev/null +++ b/BizHawk.Emulation/Sound/Utilities/SampleBuffers.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BizHawk.Emulation.Sound.Utilities +{ + /****************************************************************************** + * + * libresample4j + * Copyright (c) 2009 Laszlo Systems, Inc. All Rights Reserved. + * + * libresample4j is a Java port of Dominic Mazzoni's libresample 0.1.3, + * which is in turn based on Julius Smith's Resample 1.7 library. + * http://www-ccrma.stanford.edu/~jos/resample/ + * + * License: LGPL -- see the file LICENSE.txt for more information + * + *****************************************************************************/ + + /// + /// Callback for producing and consuming samples. Enables on-the-fly conversion between sample types + /// (signed 16-bit integers to floats, for example) and/or writing directly to an output stream. + /// + public interface SampleBuffers + { + /// number of input samples available + int getInputBufferLength(); + + /// number of samples the output buffer has room for + int getOutputBufferLength(); + + /// + /// Copy length samples from the input buffer to the given array, starting at the given offset. + /// Samples should be in the range -1.0f to 1.0f. + /// + /// array to hold samples from the input buffer + /// start writing samples here + /// write this many samples + void produceInput(float[] array, int offset, int length); + + /// + /// Copy length samples from the given array to the output buffer, starting at the given offset. + /// + /// array to read from + /// start reading samples here + /// read this many samples + void consumeOutput(float[] array, int offset, int length); + } +}