diff --git a/Vocaluxe/Lib/Sound/Record/CBuffer.cs b/Vocaluxe/Lib/Sound/Record/CBuffer.cs index df6284a3..755aa118 100644 --- a/Vocaluxe/Lib/Sound/Record/CBuffer.cs +++ b/Vocaluxe/Lib/Sound/Record/CBuffer.cs @@ -23,6 +23,7 @@ using System.Collections.Generic; using System.IO; using System.Windows.Forms; using System.Diagnostics; +using Pitch; using VocaluxeLib; using VocaluxeLib.Utils; @@ -289,6 +290,10 @@ namespace Vocaluxe.Lib.Sound.Record const int maxSamplesPerBatch = 512; CAnalyzer analyzer2 = new CAnalyzer(); CPtDyWa analyzer3 = new CPtDyWa(); + PitchTracker analyzer4 = new PitchTracker {SampleRate = 44100, DetectLevelThreshold = 0.001f}; + int[] detectedTones = new int[4]; + int[] oks = new int[4]; + int lCt = 0; while (wavFile.NumSamplesLeft > maxSamplesPerBatch) { byte[] samples = wavFile.GetNextSamples16BitAsBytes(maxSamplesPerBatch, 1); @@ -300,6 +305,12 @@ namespace Vocaluxe.Lib.Sound.Record analyzer3.Input(samples); analyzer2.Process(); analyzer3.Process(); + short[] samplesShort = new short[samples.Length / 2]; + Buffer.BlockCopy(samples, 0, samplesShort, 0, samples.Length); + float[] samplesFloat = new float[samplesShort.Length]; + for (int i = 0; i < samplesShort.Length; i++) + samplesFloat[i] = (float)samplesShort[i] / short.MaxValue; + analyzer4.ProcessBuffer(samplesFloat); while (curTimeIndex + 1 < tones.Count && time >= tones[curTimeIndex + 1].Time) { curTimeIndex++; @@ -307,25 +318,39 @@ namespace Vocaluxe.Lib.Sound.Record } if (curNote < 0) continue; - int tone1 = ToneValid ? ToneAbs : -1; - int tone2 = (int)Math.Round(analyzer2.FindNote(64, 1770)); - int tone3 = (int)Math.Round(analyzer3.GetNote()); - /*ct++; - if (!ToneValid) + detectedTones[0] = ToneValid ? ToneAbs : -1; + detectedTones[1] = (int)Math.Round(analyzer2.FindNote(64, 1770)); + detectedTones[2] = (int)Math.Round(analyzer3.GetNote()); + detectedTones[3] = analyzer4.CurrentPitchRecord.MidiNote - 15 - 21; + lCt++; + /*if (!ToneValid) CBase.Log.LogDebug("Note " + _ToneToNote(curNote) + " at " + time + "ms not detected"); else if (ToneAbs % 12 != curNote % 12) CBase.Log.LogDebug("Note " + _ToneToNote(curNote) + " at " + time + "ms wrongly detected as " + _ToneToNote(ToneAbs)); else ok++;*/ - bool ok1 = _IsNoteValid(tone1, time, tones); - bool ok2 = _IsNoteValid(tone2, time, tones); - bool ok3 = _IsNoteValid(tone3, time, tones); - if (!ok1 || !ok2 || !ok3) + bool[] valids = new bool[4]; + bool error = false; + for (int i = 0; i < 4; i++) { - CBase.Log.LogDebug("Note " + _ToneToNote(curNote) + " at " + time + "ms detected as " + _ToneToNote(tone1) + (ok1 ? "" : "(!)") + "; " + _ToneToNote(tone2) + - (ok2 ? "" : "(!)") + "; " + _ToneToNote(tone3) + (ok3 ? "" : "(!)") + "; "); + valids[i] = _IsNoteValid(detectedTones[i], time, tones); + if (valids[i]) + oks[i]++; + else + error = true; + } + if (error) + { + string msg = "Note " + _ToneToNote(curNote) + " at " + time + "ms detected as "; + for (int i = 0; i < 4; i++) + msg += _ToneToNote(detectedTones[i]) + (valids[i] ? "" : "(!)") + "; "; + CBase.Log.LogDebug(msg); } } + string msg2 = "Detected "; + for (int i = 0; i < 4; i++) + msg2 += oks[i] + "|" + (lCt - oks[i]) + " "; + CBase.Log.LogDebug(msg2); } catch (Exception e) { diff --git a/Vocaluxe/Lib/Sound/Record/Pitch/CircularBuffer.cs b/Vocaluxe/Lib/Sound/Record/Pitch/CircularBuffer.cs new file mode 100644 index 00000000..d784e2df --- /dev/null +++ b/Vocaluxe/Lib/Sound/Record/Pitch/CircularBuffer.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections; + +namespace Pitch +{ + public class CircularBuffer : IDisposable + { + int m_bufSize; + int m_begBufOffset; + int m_availBuf; + long m_startPos; // total circular buffer position + T[] m_buffer; + + /// + /// Constructor + /// + public CircularBuffer() + { + } + + /// + /// Constructor + /// + /// + public CircularBuffer(int bufCount) + { + SetSize(bufCount); + } + + /// + /// Dispose + /// + public void Dispose() + { + SetSize(0); + } + + /// + /// Reset to the beginning of the buffer + /// + public void Reset() + { + m_begBufOffset = 0; + m_availBuf = 0; + m_startPos = 0; + } + + /// + /// Set the buffer to the specified size + /// + /// + public void SetSize(int newSize) + { + Reset(); + + if (m_bufSize == newSize) + return; + + if (m_buffer != null) + m_buffer = null; + + m_bufSize = newSize; + + if (m_bufSize > 0) + m_buffer = new T[m_bufSize]; + } + + /// + /// Clear the buffer + /// + public void Clear() + { + Array.Clear(m_buffer, 0, m_buffer.Length); + } + + /// + /// Get or set the start position + /// + public long StartPosition + { + get { return m_startPos; } + set { m_startPos = value; } + } + + /// + /// Get the end position + /// + public long EndPosition + { + get { return m_startPos + m_availBuf; } + } + + /// + /// Get or set the amount of avaliable space + /// + public int Available + { + get { return m_availBuf; } + set { m_availBuf = Math.Min(value, m_bufSize); } + } + + /// + /// Write data into the buffer + /// + /// + /// + /// + public int WriteBuffer(T[] m_pInBuffer, int count) + { + count = Math.Min(count, m_bufSize); + + var startPos = m_availBuf != m_bufSize ? m_availBuf : m_begBufOffset; + var pass1Count = Math.Min(count, m_bufSize - startPos); + var pass2Count = count - pass1Count; + + PitchDsp.CopyBuffer(m_pInBuffer, 0, m_buffer, startPos, pass1Count); + + if (pass2Count > 0) + PitchDsp.CopyBuffer(m_pInBuffer, pass1Count, m_buffer, 0, pass2Count); + + if (pass2Count == 0) + { + // did not wrap around + if (m_availBuf != m_bufSize) + m_availBuf += count; // have never wrapped around + else + { + m_begBufOffset += count; + m_startPos += count; + } + } + else + { + // wrapped around + if (m_availBuf != m_bufSize) + m_startPos += pass2Count; // first time wrap-around + else + m_startPos += count; + + m_begBufOffset = pass2Count; + m_availBuf = m_bufSize; + } + + return count; + } + + /// + /// Read from the buffer + /// + /// + /// + /// + /// + public bool ReadBuffer(T[] outBuffer, long startRead, int readCount) + { + var endRead = (int)(startRead + readCount); + var endAvail = (int)(m_startPos + m_availBuf); + + if (startRead < m_startPos || endRead > endAvail) + return false; + + var startReadPos = (int)(((startRead - m_startPos) + m_begBufOffset) % m_bufSize); + var block1Samples = Math.Min(readCount, m_bufSize - startReadPos); + var block2Samples = readCount - block1Samples; + + PitchDsp.CopyBuffer(m_buffer, startReadPos, outBuffer, 0, block1Samples); + + if (block2Samples > 0) + PitchDsp.CopyBuffer(m_buffer, 0, outBuffer, block1Samples, block2Samples); + + return true; + } + } +} diff --git a/Vocaluxe/Lib/Sound/Record/Pitch/Extensions.cs b/Vocaluxe/Lib/Sound/Record/Pitch/Extensions.cs new file mode 100644 index 00000000..2487ea18 --- /dev/null +++ b/Vocaluxe/Lib/Sound/Record/Pitch/Extensions.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Pitch +{ + public static class Extensions + { + /// + /// Clear the buffer + /// + /// + /// + static public void Clear(this float[] buffer) + { + Array.Clear(buffer, 0, buffer.Length); + } + + /// + /// Clear the buffer + /// + /// + /// + static public void Clear(this double[] buffer) + { + Array.Clear(buffer, 0, buffer.Length); + } + + /// + /// Copy the values from one buffer to a different or the same buffer. + /// It is safe to copy to the same buffer, even if the areas overlap + /// + /// + /// + /// + /// + static public void Copy(this float[] fromBuffer, float[] toBuffer, int fromStart, int toStart, int length) + { + if (toBuffer == null || fromBuffer.Length == 0 || toBuffer.Length == 0) + return; + + var fromBegIdx = fromStart; + var fromEndIdx = fromStart + length; + var toBegIdx = toStart; + var toEndIdx = toStart + length; + + if (fromBegIdx < 0) + { + toBegIdx -= fromBegIdx; + fromBegIdx = 0; + } + + if (toBegIdx < 0) + { + fromBegIdx -= toBegIdx; + toBegIdx = 0; + } + + if (fromEndIdx >= fromBuffer.Length) + { + toEndIdx -= fromEndIdx - fromBuffer.Length + 1; + fromEndIdx = fromBuffer.Length - 1; + } + + if (toEndIdx >= toBuffer.Length) + { + fromEndIdx -= toEndIdx - toBuffer.Length + 1; + toEndIdx = fromBuffer.Length - 1; + } + + if (fromBegIdx < toBegIdx) + { + // Shift right, so start at the right + for (int fromIdx = fromEndIdx, toIdx = toEndIdx; fromIdx >= fromBegIdx; fromIdx--, toIdx--) + toBuffer[toIdx] = fromBuffer[fromIdx]; + } + else + { + // Shift left, so start at the left + for (int fromIdx = fromBegIdx, toIdx = toBegIdx; fromIdx <= fromEndIdx; fromIdx++, toIdx++) + toBuffer[toIdx] = fromBuffer[fromIdx]; + } + } + + /// + /// + /// + /// + /// + static public void Clear(this float[] buffer, int startIdx, int endIdx) + { + Array.Clear(buffer, startIdx, endIdx - startIdx + 1); + } + + /// + /// Fill the buffer with the specified value + /// + /// + /// + /// + static public void Fill(this double[] buffer, double value) + { + for (int idx = 0; idx < buffer.Length; idx++) + buffer[idx] = value; + } + } +} diff --git a/Vocaluxe/Lib/Sound/Record/Pitch/IIRFilter.cs b/Vocaluxe/Lib/Sound/Record/Pitch/IIRFilter.cs new file mode 100644 index 00000000..8759c77e --- /dev/null +++ b/Vocaluxe/Lib/Sound/Record/Pitch/IIRFilter.cs @@ -0,0 +1,616 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Pitch +{ + /// + /// Infinite impulse response filter (old style analog filters) + /// + class IIRFilter + { + /// + /// The type of filter + /// + public enum FilterType + { + None = 0, + LP, + HP, + BP + } + + /// + /// The filter prototype + /// + public enum ProtoType + { + None = 0, + Butterworth, + Chebyshev, + } + + const int kHistMask = 31; + const int kHistSize = 32; + + private int m_order; + private ProtoType m_protoType; + private FilterType m_filterType; + + private float m_fp1; + private float m_fp2; + private float m_fN; + private float m_ripple; + private float m_sampleRate; + private double[] m_real; + private double[] m_imag; + private double[] m_z; + private double[] m_aCoeff; + private double[] m_bCoeff; + private double[] m_inHistory; + private double[] m_outHistory; + private int m_histIdx; + private bool m_invertDenormal; + + public IIRFilter() + { + } + + /// + /// Returns true if all the filter parameters are valid + /// + public bool FilterValid + { + get + { + if (m_order < 1 || m_order > 16 || + m_protoType == ProtoType.None || + m_filterType == FilterType.None || + m_sampleRate <= 0.0f || + m_fN <= 0.0f) + return false; + + switch (m_filterType) + { + case FilterType.LP: + if (m_fp2 <= 0.0f) + return false; + break; + + case FilterType.BP: + if (m_fp1 <= 0.0f || m_fp2 <= 0.0f || m_fp1 >= m_fp2) + return false; + break; + + case FilterType.HP: + if (m_fp1 <= 0.0f) + return false; + break; + } + + // For bandpass, the order must be even + if (m_filterType == FilterType.BP && (m_order & 1) != 0) + return false; + + return true; + } + } + + /// + /// Set the filter prototype + /// + public ProtoType Proto + { + get { return m_protoType; } + + set + { + m_protoType = value; + Design(); + } + } + + /// + /// Set the filter type + /// + public FilterType Type + { + get { return m_filterType; } + + set + { + m_filterType = value; + Design(); + } + } + + public int Order + { + get { return m_order; } + + set + { + m_order = Math.Min(16, Math.Max(1, Math.Abs(value))); + + if (m_filterType == FilterType.BP && Odd(m_order)) + m_order++; + + Design(); + } + } + + public float SampleRate + { + get { return m_sampleRate; } + + set + { + m_sampleRate = value; + m_fN = 0.5f * m_sampleRate; + Design(); + } + } + + public float FreqLow + { + get { return m_fp1; } + + set + { + m_fp1 = value; + Design(); + } + } + + public float FreqHigh + { + get { return m_fp2; } + + set + { + m_fp2 = value; + Design(); + } + } + + public float Ripple + { + get { return m_ripple; } + + set + { + m_ripple = value; + Design(); + } + } + + /// + /// Returns true if n is odd + /// + /// + /// + private bool Odd(int n) + { + return (n & 1) == 1; + } + + /// + /// Square + /// + /// + /// + private float Sqr(float value) + { + return value * value; + } + + /// + /// Square + /// + /// + /// + private double Sqr(double value) + { + return value * value; + } + + /// + /// Determines poles and zeros of IIR filter + /// based on bilinear transform method + /// + private void LocatePolesAndZeros() + { + m_real = new double[m_order + 1]; + m_imag = new double[m_order + 1]; + m_z = new double[m_order + 1]; + double ln10 = Math.Log(10.0); + + // Butterworth, Chebyshev parameters + int n = m_order; + + if (m_filterType == FilterType.BP) + n = n / 2; + + int ir = n % 2; + int n1 = n + ir; + int n2 = (3 * n + ir) / 2 - 1; + double f1; + + switch (m_filterType) + { + case FilterType.LP: + f1 = m_fp2; + break; + + case FilterType.HP: + f1 = m_fN - m_fp1; + break; + + case FilterType.BP: + f1 = m_fp2 - m_fp1; + break; + + default: + f1 = 0.0; + break; + } + + double tanw1 = Math.Tan(0.5 * Math.PI * f1 / m_fN); + double tansqw1 = Sqr(tanw1); + + // Real and Imaginary parts of low-pass poles + double t, a = 1.0, r = 1.0, i = 1.0; + + for (int k = n1; k <= n2; k++) + { + t = 0.5 * (2 * k + 1 - ir) * Math.PI / (double)n; + + switch (m_protoType) + { + case ProtoType.Butterworth: + double b3 = 1.0 - 2.0 * tanw1 * Math.Cos(t) + tansqw1; + r = (1.0 - tansqw1) / b3; + i = 2.0 * tanw1 * Math.Sin(t) / b3; + break; + + case ProtoType.Chebyshev: + double d = 1.0 - Math.Exp(-0.05 * m_ripple * ln10); + double e = 1.0 / Math.Sqrt(1.0 / Sqr(1.0 - d) - 1.0); + double x = Math.Pow(Math.Sqrt(e * e + 1.0) + e, 1.0 / (double)n); + a = 0.5 * (x - 1.0 / x); + double b = 0.5 * (x + 1.0 / x); + double c3 = a * tanw1 * Math.Cos(t); + double c4 = b * tanw1 * Math.Sin(t); + double c5 = Sqr(1.0 - c3) + Sqr(c4); + r = 2.0 * (1.0 - c3) / c5 - 1.0; + i = 2.0 * c4 / c5; + break; + } + + int m = 2 * (n2 - k) + 1; + m_real[m + ir] = r; + m_imag[m + ir] = Math.Abs(i); + m_real[m + ir + 1] = r; + m_imag[m + ir + 1] = -Math.Abs(i); + } + + if (Odd(n)) + { + if (m_protoType == ProtoType.Butterworth) + r = (1.0 - tansqw1) / (1.0 + 2.0 * tanw1 + tansqw1); + + if (m_protoType == ProtoType.Chebyshev) + r = 2.0 / (1.0 + a * tanw1) - 1.0; + + m_real[1] = r; + m_imag[1] = 0.0; + } + + switch (m_filterType) + { + case FilterType.LP: + for (int m = 1; m <= n; m++) + m_z[m] = -1.0; + break; + + case FilterType.HP: + // Low-pass to high-pass transformation + for (int m = 1; m <= n; m++) + { + m_real[m] = -m_real[m]; + m_z[m] = 1.0; + } + break; + + case FilterType.BP: + // Low-pass to bandpass transformation + for (int m = 1; m <= n; m++) + { + m_z[m] = 1.0; + m_z[m + n] = -1.0; + } + + double f4 = 0.5 * Math.PI * m_fp1 / m_fN; + double f5 = 0.5 * Math.PI * m_fp2 / m_fN; + double aa = Math.Cos(f4 + f5) / Math.Cos(f5 - f4); + double aR, aI, h1, h2, p1R, p2R, p1I, p2I; + + for (int m1 = 0; m1 <= (m_order - 1) / 2; m1++) + { + int m = 1 + 2 * m1; + aR = m_real[m]; + aI = m_imag[m]; + + if (Math.Abs(aI) < 0.0001) + { + h1 = 0.5 * aa * (1.0 + aR); + h2 = Sqr(h1) - aR; + if (h2 > 0.0) + { + p1R = h1 + Math.Sqrt(h2); + p2R = h1 - Math.Sqrt(h2); + p1I = 0.0; + p2I = 0.0; + } + else + { + p1R = h1; + p2R = h1; + p1I = Math.Sqrt(Math.Abs(h2)); + p2I = -p1I; + } + } + else + { + double fR = aa * 0.5 * (1.0 + aR); + double fI = aa * 0.5 * aI; + double gR = Sqr(fR) - Sqr(fI) - aR; + double gI = 2 * fR * fI - aI; + double sR = Math.Sqrt(0.5 * Math.Abs(gR + Math.Sqrt(Sqr(gR) + Sqr(gI)))); + double sI = gI / (2.0 * sR); + p1R = fR + sR; + p1I = fI + sI; + p2R = fR - sR; + p2I = fI - sI; + } + + m_real[m] = p1R; + m_real[m + 1] = p2R; + m_imag[m] = p1I; + m_imag[m + 1] = p2I; + } + + if (Odd(n)) + { + m_real[2] = m_real[n + 1]; + m_imag[2] = m_imag[n + 1]; + } + + for (int k = n; k >= 1; k--) + { + int m = 2 * k - 1; + m_real[m] = m_real[k]; + m_real[m + 1] = m_real[k]; + m_imag[m] = Math.Abs(m_imag[k]); + m_imag[m + 1] = -Math.Abs(m_imag[k]); + } + + break; + } + } + + /// + /// Calculate all the values + /// + public void Design() + { + if (!this.FilterValid) + return; + + m_aCoeff = new double[m_order + 1]; + m_bCoeff = new double[m_order + 1]; + m_inHistory = new double[kHistSize]; + m_outHistory = new double[kHistSize]; + + double[] newA = new double[m_order + 1]; + double[] newB = new double[m_order + 1]; + + // Find filter poles and zeros + LocatePolesAndZeros(); + + // Compute filter coefficients from pole/zero values + m_aCoeff[0] = 1.0; + m_bCoeff[0] = 1.0; + + for (int i = 1; i <= m_order; i++) + { + m_aCoeff[i] = 0.0; + m_bCoeff[i] = 0.0; + } + + int k = 0; + int n = m_order; + int pairs = n / 2; + + if (Odd(m_order)) + { + // First subfilter is first order + m_aCoeff[1] = -m_z[1]; + m_bCoeff[1] = -m_real[1]; + k = 1; + } + + for (int p = 1; p <= pairs; p++) + { + int m = 2 * p - 1 + k; + double alpha1 = -(m_z[m] + m_z[m + 1]); + double alpha2 = m_z[m] * m_z[m + 1]; + double beta1 = -2.0 * m_real[m]; + double beta2 = Sqr(m_real[m]) + Sqr(m_imag[m]); + + newA[1] = m_aCoeff[1] + alpha1 * m_aCoeff[0]; + newB[1] = m_bCoeff[1] + beta1 * m_bCoeff[0]; + + for (int i = 2; i <= n; i++) + { + newA[i] = m_aCoeff[i] + alpha1 * m_aCoeff[i - 1] + alpha2 * m_aCoeff[i - 2]; + newB[i] = m_bCoeff[i] + beta1 * m_bCoeff[i - 1] + beta2 * m_bCoeff[i - 2]; + } + + for (int i = 1; i <= n; i++) + { + m_aCoeff[i] = newA[i]; + m_bCoeff[i] = newB[i]; + } + } + + // Ensure the filter is normalized + FilterGain(1000); + } + + /// + /// Reset the history buffers + /// + public void Reset() + { + if (m_inHistory != null) + m_inHistory.Clear(); + + if (m_outHistory != null) + m_outHistory.Clear(); + + m_histIdx = 0; + } + + /// + /// Reset the filter, and fill the appropriate history buffers with the specified value + /// + /// + public void Reset(double startValue) + { + m_histIdx = 0; + + if (m_inHistory == null || m_outHistory == null) + return; + + m_inHistory.Fill(startValue); + + if (m_inHistory != null) + { + switch (m_filterType) + { + case FilterType.LP: + m_outHistory.Fill(startValue); + break; + + default: + m_outHistory.Clear(); + break; + } + } + } + + /// + /// Apply the filter to the buffer + /// + /// + public void FilterBuffer(float[] srcBuf, long srcPos, float[] dstBuf, long dstPos, long nLen) + { + const double kDenormal = 0.000000000000001; + double denormal = m_invertDenormal ? -kDenormal : kDenormal; + m_invertDenormal = !m_invertDenormal; + + for (int sampleIdx = 0; sampleIdx < nLen; sampleIdx++) + { + double sum = 0.0f; + + m_inHistory[m_histIdx] = srcBuf[srcPos + sampleIdx] + denormal; + + for (int idx = 0; idx < m_aCoeff.Length; idx++) + sum += m_aCoeff[idx] * m_inHistory[(m_histIdx - idx) & kHistMask]; + + for (int idx = 1; idx < m_bCoeff.Length; idx++) + sum -= m_bCoeff[idx] * m_outHistory[(m_histIdx - idx) & kHistMask]; + + m_outHistory[m_histIdx] = sum; + m_histIdx = (m_histIdx + 1) & kHistMask; + dstBuf[dstPos + sampleIdx] = (float)sum; + } + } + + public float FilterSample(float inVal) + { + double sum = 0.0f; + + m_inHistory[m_histIdx] = inVal; + + for (int idx = 0; idx < m_aCoeff.Length; idx++) + sum += m_aCoeff[idx] * m_inHistory[(m_histIdx - idx) & kHistMask]; + + for (int idx = 1; idx < m_bCoeff.Length; idx++) + sum -= m_bCoeff[idx] * m_outHistory[(m_histIdx - idx) & kHistMask]; + + m_outHistory[m_histIdx] = sum; + m_histIdx = (m_histIdx + 1) & kHistMask; + + return (float)sum; + } + + /// + /// Get the gain at the specified number of frequency points + /// + /// + /// + public float[] FilterGain(int freqPoints) + { + // Filter gain at uniform frequency intervals + float[] g = new float[freqPoints]; + double theta, s, c, sac, sas, sbc, sbs; + float gMax = -100.0f; + float sc = 10.0f / (float)Math.Log(10.0f); + double t = Math.PI / (freqPoints - 1); + + for (int i = 0; i < freqPoints; i++) + { + theta = i * t; + + if (i == 0) + theta = Math.PI * 0.0001; + + if (i == freqPoints - 1) + theta = Math.PI * 0.9999; + + sac = 0.0f; + sas = 0.0f; + sbc = 0.0f; + sbs = 0.0f; + + for (int k = 0; k <= m_order; k++) + { + c = Math.Cos(k * theta); + s = Math.Sin(k * theta); + sac += c * m_aCoeff[k]; + sas += s * m_aCoeff[k]; + sbc += c * m_bCoeff[k]; + sbs += s * m_bCoeff[k]; + } + + g[i] = sc * (float)Math.Log((Sqr(sac) + Sqr(sas)) / (Sqr(sbc) + Sqr(sbs))); + gMax = Math.Max(gMax, g[i]); + } + + // Normalize to 0 dB maximum gain + for (int i = 0; i < freqPoints; i++) + g[i] -= gMax; + + // Normalize numerator (a) coefficients + float normFactor = (float)Math.Pow(10.0, -0.05 * gMax); + + for (int i = 0; i <= m_order; i++) + m_aCoeff[i] *= normFactor; + + return g; + } + } +} diff --git a/Vocaluxe/Lib/Sound/Record/Pitch/PitchDsp.cs b/Vocaluxe/Lib/Sound/Record/Pitch/PitchDsp.cs new file mode 100644 index 00000000..b04f5bbf --- /dev/null +++ b/Vocaluxe/Lib/Sound/Record/Pitch/PitchDsp.cs @@ -0,0 +1,577 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices; +using System.Collections; + +namespace Pitch +{ + /// + /// Pitch related DSP + /// + public class PitchDsp + { + public static readonly double InverseLog2 = 1.0 / Math.Log10(2.0); + + private const int kCourseOctaveSteps = 96; + private const int kScanHiSize = 31; + private const float kScanHiFreqStep = 1.005f; + private const int kMinMidiNote = 21; // A0 + private const int kMaxMidiNote = 108; // C8 + + private float m_minPitch; + private float m_maxPitch; + private int m_minNote; + private int m_maxNote; + private int m_blockLen14; // 1/4 block len + private int m_blockLen24; // 2/4 block len + private int m_blockLen34; // 3/4 block len + private int m_blockLen44; // 4/4 block len + private double m_sampleRate; + private float m_detectLevelThreshold; + + private int m_numCourseSteps; + private float[] m_pCourseFreqOffset; + private float[] m_pCourseFreq; + private float[] m_scanHiOffset = new float[kScanHiSize]; + private float[] m_peakBuf = new float[kScanHiSize]; + private int m_prevPitchIdx; + private float[] m_detectCurve; + + /// + /// Constructor + /// + /// + /// + /// + /// + public PitchDsp(double sampleRate, float minPitch, float maxPitch, float detectLevelThreshold) + { + m_sampleRate = sampleRate; + m_minPitch = minPitch; + m_maxPitch = maxPitch; + m_detectLevelThreshold = detectLevelThreshold; + + m_minNote = (int)(PitchToMidiNote(m_minPitch) + 0.5f) + 2; + m_maxNote = (int)(PitchToMidiNote(m_maxPitch) + 0.5f) - 2; + + m_blockLen44 = (int)(m_sampleRate / m_minPitch + 0.5f); + m_blockLen34 = (m_blockLen44 * 3) / 4; + m_blockLen24 = m_blockLen44 / 2; + m_blockLen14 = m_blockLen44 / 4; + + m_numCourseSteps = (int)((Math.Log((double)m_maxPitch / (double)m_minPitch) / Math.Log(2.0)) * kCourseOctaveSteps + 0.5) + 3; + + m_pCourseFreqOffset = new float[m_numCourseSteps + 10000]; + m_pCourseFreq = new float[m_numCourseSteps + 10000]; + + m_detectCurve = new float[m_numCourseSteps]; + + double freqStep = 1.0 / Math.Pow(2.0, 1.0 / kCourseOctaveSteps); + double curFreq = m_maxPitch / freqStep; + + // frequency is stored from high to low + for (int idx = 0; idx < m_numCourseSteps; idx++) + { + m_pCourseFreq[idx] = (float)curFreq; + m_pCourseFreqOffset[idx] = (float)(m_sampleRate / curFreq); + curFreq *= freqStep; + } + + for (int idx = 0; idx < kScanHiSize; idx++) + m_scanHiOffset[idx] = (float)Math.Pow(kScanHiFreqStep, (kScanHiSize / 2) - idx); + } + + /// + /// Get the max detected pitch + /// + public float MaxPitch + { + get { return m_maxPitch; } + } + + /// + /// Get the min detected pitch + /// + public float MinPitch + { + get { return m_minPitch; } + } + + /// + /// Get the max note + /// + public int MaxNote + { + get { return m_maxNote; } + } + + /// + /// Get the min note + /// + public int MinNote + { + get { return m_minNote; } + } + + /// + /// Detect the pitch + /// + public float DetectPitch(float[] samplesLo, float[] samplesHi, int numSamples) + { + float pitch = 0.0f; + + if (!LevelIsAbove(samplesLo, numSamples, m_detectLevelThreshold) && + !LevelIsAbove(samplesHi, numSamples, m_detectLevelThreshold)) + { + // Level is too low + return 0.0f; + } + + pitch = DetectPitchLo(samplesLo, samplesHi); + return pitch; + } + + /// + /// Low resolution pitch detection + /// + /// + /// + /// + /// + /// + /// + private float DetectPitchLo(float[] samplesLo, float[] samplesHi) + { + m_detectCurve.Clear(); + + const int skipSize = 8; + const int peakScanSize = 23; + const int peakScanSizeHalf = peakScanSize / 2; + + float peakThresh1 = 200.0f; + float peakThresh2 = 600.0f; + bool bufferSwitched = false; + + for (int idx = 0; idx < m_numCourseSteps; idx += skipSize) + { + int blockLen = Math.Min(m_blockLen44, (int)m_pCourseFreqOffset[idx] * 2); + float[] curSamples = null; + + // 258 is at 250 Hz, which is the switchover frequency for the two filters + bool loBuffer = idx >= 258; + + if (loBuffer) + { + if (!bufferSwitched) + { + m_detectCurve.Clear(258 - peakScanSizeHalf, 258 + peakScanSizeHalf); + bufferSwitched = true; + } + + curSamples = samplesLo; + } + else + curSamples = samplesHi; + + int stepSizeLoRes = blockLen / 10; + int stepSizeHiRes = Math.Max(1, Math.Min(5, idx * 5 / m_numCourseSteps)); + + float fValue = RatioAbsDiffLinear(curSamples, idx, blockLen, stepSizeLoRes, false); + + if (fValue > peakThresh1) + { + // Do a closer search for the peak + int peakIdx = -1; + float peakVal = 0.0f; + float prevVal = 0.0f; + int dir = 4; // start going forward + int curPos = idx; // start at center of the scan range + int begSearch = Math.Max(idx - peakScanSizeHalf, 0); + int endSearch = Math.Min(idx + peakScanSizeHalf, m_numCourseSteps - 1); + + while (curPos >= begSearch && curPos < endSearch) + { + float curVal = RatioAbsDiffLinear(curSamples, curPos, blockLen, stepSizeHiRes, true); + + if (peakVal < curVal) + { + peakVal = curVal; + peakIdx = curPos; + } + + if (prevVal > curVal) + { + dir = -dir >> 1; + + if (dir == 0) + { + if (peakVal > peakThresh2 && peakIdx >= 6 && peakIdx <= m_numCourseSteps - 7) + { + float fValL = RatioAbsDiffLinear(curSamples, peakIdx - 5, blockLen, stepSizeHiRes, true); + float fValR = RatioAbsDiffLinear(curSamples, peakIdx + 5, blockLen, stepSizeHiRes, true); + float fPointy = peakVal / (fValL + fValR) * 2.0f; + + float minPointy = (m_prevPitchIdx > 0 && Math.Abs(m_prevPitchIdx - peakIdx) < 10) ? 1.2f : 1.5f; + + if (fPointy > minPointy) + { + float pitchHi = DetectPitchHi(curSamples, peakIdx); + + if (pitchHi > 1.0f) + { + m_prevPitchIdx = peakIdx; + return pitchHi; + } + } + } + + break; + } + } + + prevVal = curVal; + curPos += dir; + } + } + } + + m_prevPitchIdx = 0; + return 0.0f; + } + + /// + /// High resolution pitch detection + /// + /// + /// + /// + private float DetectPitchHi(float[] samples, int lowFreqIdx) + { + int peakIdx = -1; + float prevVal = 0.0f; + int dir = 4; // start going forward + int curPos = kScanHiSize >> 1; // start at center of the scan range + + m_peakBuf.Clear(); + + float offset = m_pCourseFreqOffset[lowFreqIdx]; + + while (curPos >= 0 && curPos < kScanHiSize) + { + if (m_peakBuf[curPos] == 0.0f) + m_peakBuf[curPos] = SumAbsDiffHermite(samples, offset * m_scanHiOffset[curPos], m_blockLen44, 1); + + if (peakIdx < 0 || m_peakBuf[peakIdx] < m_peakBuf[curPos]) + peakIdx = curPos; + + if (prevVal > m_peakBuf[curPos]) + { + dir = -dir >> 1; + + if (dir == 0) + { + // found the peak + float minVal = Math.Min(m_peakBuf[peakIdx - 1], m_peakBuf[peakIdx + 1]); + + minVal -= minVal * (1.0f / 32.0f); + + var y1 = (float)Math.Log10(m_peakBuf[peakIdx - 1] - minVal); + var y2 = (float)Math.Log10(m_peakBuf[peakIdx] - minVal); + var y3 = (float)Math.Log10(m_peakBuf[peakIdx + 1] - minVal); + + float fIdx = (float)peakIdx + (y3 - y1) / (2.0f * (2.0f * y2 - y1 - y3)); + + return (float)Math.Pow(kScanHiFreqStep, fIdx - (kScanHiSize / 2)) * m_pCourseFreq[lowFreqIdx]; + } + } + + prevVal = m_peakBuf[curPos]; + curPos += dir; + } + + return 0.0f; + } + + /// + /// Create a sine wave with the specified frequency, amplitude and starting angle. + /// Returns the updated angle. + /// + /// + /// + /// + /// + /// + /// + public static double CreateSineWave(float[] buffer, int numSamples, float sampleRate, + float freq, float amplitude, double startAngle) + { + double angleStep = freq / sampleRate * Math.PI * 2.0; + double curAngle = startAngle; + + for (int idx = 0; idx < numSamples; idx++) + { + buffer[idx] = (float)Math.Sin(curAngle) * amplitude; + + curAngle += angleStep; + + while (curAngle > Math.PI) + curAngle -= Math.PI * 2.0; + } + + return curAngle; + } + + /// + /// Returns true if the level is above the specified value + /// + /// + /// + /// + /// + /// + public bool LevelIsAbove(float[] buffer, int len, float level) + { + if (buffer == null || buffer.Length == 0) + return false; + + int endIdx = Math.Min(buffer.Length, len); + + for (int idx = 0; idx < endIdx; idx++) + { + if (Math.Abs(buffer[idx]) >= level) + return true; + } + + return false; + } + + /// + /// Copy the data from the source to the destination buffer + /// + /// + /// + /// + /// + public static void CopyBuffer(T[] source, int srcStart, T[] destination, int dstStart, int length) + { + if (length < 0) + throw new ArgumentOutOfRangeException("length"); + + if (source == null || source.Length < srcStart + length) + throw new Exception("Source buffer is null or not large enough"); + + if (destination == null || destination.Length < dstStart + length) + throw new Exception("Destination buffer is null or not large enough"); + + int srcIdx = srcStart; + int dstIdx = dstStart; + + for (int idx = 0; idx < length; idx++) + destination[dstIdx++] = source[srcIdx++]; + } + + /// + /// // 4-point, 3rd-order Hermite (x-form) + /// + private float InterpolateHermite(float fY0, float fY1, float fY2, float fY3, float frac) + { + float c1 = 0.5f * (fY2 - fY0); + float c3 = 1.5f * (fY1 - fY2) + 0.5f * (fY3 - fY0); + float c2 = fY0 - fY1 + c1 - c3; + + return ((c3 * frac + c2) * frac + c1) * frac + fY1; + } + + /// + /// Linear interpolation + /// nFrac is based on 1.0 = 256 + /// + private float InterpolateLinear(float y0, float y1, float frac) + { + return y0 * (1.0f - frac) + y1 * frac; + } + + /// + /// Medium Low res SumAbsDiff + /// + private float RatioAbsDiffLinear(float[] samples, int freqIdx, int blockLen, int stepSize, bool hiRes) + { + if (hiRes && m_detectCurve[freqIdx] > 0.0f) + return m_detectCurve[freqIdx]; + + var offsetInt = (int)m_pCourseFreqOffset[freqIdx]; + float offsetFrac = m_pCourseFreqOffset[freqIdx] - offsetInt; + float rect = 0.0f; + float absDiff = 0.01f; // prevent divide by zero + int count = 0; + float interp; + float sample; + + // Do a scan using linear interpolation and the specified step size + for (int idx = 0; idx < blockLen; idx += stepSize, count++) + { + sample = samples[idx]; + interp = InterpolateLinear(samples[offsetInt + idx], samples[offsetInt + idx + 1], offsetFrac); + absDiff += Math.Abs(sample - interp); + rect += Math.Abs(sample) + Math.Abs(interp); + } + + float finalVal = rect / absDiff * 100.0f; + + if (hiRes) + m_detectCurve[freqIdx] = finalVal; + + return finalVal; + } + + /// + /// Medium High res SumAbsDiff + /// + private float SumAbsDiffHermite(float[] samples, float fOffset, int blockLen, int stepSize) + { + var offsetInt = (int)fOffset; + float offsetFrac = fOffset - offsetInt; + float value = 0.001f; // prevent divide by zero + int count = 0; + + // do a scan using linear interpolation and the specified step size + for (int idx = 0; idx < blockLen; idx += stepSize, count++) + { + int offsetIdx = offsetInt + idx; + + value += Math.Abs(samples[idx] - InterpolateHermite(samples[offsetIdx - 1], + samples[offsetIdx], + samples[offsetIdx + 1], + samples[offsetIdx + 2], + offsetFrac)); + } + + return count / value; + } + + /// + /// Get the MIDI note and cents of the pitch + /// + /// + /// + /// + /// + public static bool PitchToMidiNote(float pitch, out int note, out int cents) + { + if (pitch < 20.0f) + { + note = 0; + cents = 0; + return false; + } + + float fNote = (float)((12.0 * Math.Log10(pitch / 55.0) * InverseLog2)) + 33.0f; + note = (int)(fNote + 0.5f); + cents = (int)((note - fNote) * 100); + return true; + } + + /// + /// Get the pitch from the MIDI note + /// + /// + /// + public static float PitchToMidiNote(float pitch) + { + if (pitch < 20.0f) + return 0.0f; + + return (float)(12.0 * Math.Log10(pitch / 55.0) * InverseLog2) + 33.0f; + } + + /// + /// Get the pitch from the MIDI note + /// + /// + /// + public float MidiNoteToPitch(float note) + { + if (note < 33.0f) + return 0.0f; + + float pitch = (float)Math.Pow(10.0, (note - 33.0f) / InverseLog2 / 12.0f) * 55.0f; + return pitch <= m_maxPitch ? pitch : 0.0f; + } + + /// + /// Format a midi note to text + /// + /// + /// + /// + /// + public static string GetNoteName(int note, bool sharps, bool showOctave) + { + if (note < kMinMidiNote || note > kMaxMidiNote) + return null; + + note -= kMinMidiNote; + + int octave = (note + 9) / 12; + note = note % 12; + string noteText = null; + + switch (note) + { + case 0: + noteText = "A"; + break; + + case 1: + noteText = sharps ? "A#" : "Bb"; + break; + + case 2: + noteText = "B"; + break; + + case 3: + noteText = "C"; + break; + + case 4: + noteText = sharps ? "C#" : "Db"; + break; + + case 5: + noteText = "D"; + break; + + case 6: + noteText = sharps ? "D#" : "Eb"; + break; + + case 7: + noteText = "E"; + break; + + case 8: + noteText = "F"; + break; + + case 9: + noteText = sharps ? "F#" : "Gb"; + break; + + case 10: + noteText = "G"; + break; + + case 11: + noteText = sharps ? "G#" : "Ab"; + break; + } + + if (showOctave) + noteText += " " + octave.ToString(); + + return noteText; + } + } +} \ No newline at end of file diff --git a/Vocaluxe/Lib/Sound/Record/Pitch/PitchTracker.cs b/Vocaluxe/Lib/Sound/Record/Pitch/PitchTracker.cs new file mode 100644 index 00000000..d496ea16 --- /dev/null +++ b/Vocaluxe/Lib/Sound/Record/Pitch/PitchTracker.cs @@ -0,0 +1,414 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections; +using System.Threading; + +namespace Pitch +{ + /// + /// Tracks pitch + /// + public class PitchTracker + { + private const int kOctaveSteps = 96; + private const int kStepOverlap = 4; + private const float kMinFreq = 50.0f; // A1, Midi note 33, 55.0Hz + private const float kMaxFreq = 1800.0f; // A#6. Midi note 92 + private const int kStartCircular = 40; // how far into the sample buffer do we start checking (allow for filter settling) + private const float kDetectOverlapSec = 0.005f; + private const float kMaxOctaveSecRate = 10.0f; + + private const float kAvgOffset = 0.005f; // time offset between pitch averaging values + private const int kAvgCount = 1; // number of average pitch samples to take + private const float kCircularBufSaveTime = 1.0f; // Amount of samples to store in the history buffer + + private PitchDsp m_dsp; + private CircularBuffer m_circularBufferLo; + private CircularBuffer m_circularBufferHi; + private double m_sampleRate; + private float m_detectLevelThreshold = 0.01f; // -40dB + private int m_pitchRecordsPerSecond = 50; // default is 50, or one record every 20ms + + private float[] m_pitchBufLo; + private float[] m_pitchBufHi; + private int m_pitchBufSize; + private int m_samplesPerPitchBlock; + private int m_curPitchIndex; + private long m_curPitchSamplePos; + + private int m_detectOverlapSamples; + private float m_maxOverlapDiff; + + private bool m_recordPitchRecords; + private int m_pitchRecordHistorySize; + private List m_pitchRecords = new List(); + private PitchRecord m_curPitchRecord = new PitchRecord(); + + private IIRFilter m_iirFilterLoLo; + private IIRFilter m_iirFilterLoHi; + private IIRFilter m_iirFilterHiLo; + private IIRFilter m_iirFilterHiHi; + + public delegate void PitchDetectedHandler(PitchTracker sender, PitchRecord pitchRecord); + + public event PitchDetectedHandler PitchDetected; + + /// + /// Constructor + /// + public PitchTracker() {} + + /// + /// Set the sample rate + /// + public double SampleRate + { + set + { + if (m_sampleRate == value) + return; + + m_sampleRate = value; + Setup(); + } + } + + /// + /// Set the detect level threshold, The value must be between 0.0001f and 1.0f (-80 dB to 0 dB) + /// + public float DetectLevelThreshold + { + set + { + float newValue = Math.Max(0.0001f, Math.Min(1.0f, value)); + + if (m_detectLevelThreshold == newValue) + return; + + m_detectLevelThreshold = newValue; + Setup(); + } + } + + /// + /// Return the samples per pitch block + /// + public int SamplesPerPitchBlock + { + get { return m_samplesPerPitchBlock; } + } + + /// + /// Get or set the number of pitch records per second (default is 50, or one record every 20ms) + /// + public int PitchRecordsPerSecond + { + get { return m_pitchRecordsPerSecond; } + set + { + m_pitchRecordsPerSecond = Math.Max(1, Math.Min(100, value)); + Setup(); + } + } + + /// + /// Get or set whether pitch records should be recorded into a history buffer + /// + public bool RecordPitchRecords + { + get { return m_recordPitchRecords; } + set + { + if (m_recordPitchRecords == value) + return; + + m_recordPitchRecords = value; + + if (!m_recordPitchRecords) + m_pitchRecords = new List(); + } + } + + /// + /// Get or set the max number of pitch records to keep. A value of 0 means no limit. + /// Don't leave this at 0 when RecordPitchRecords is true and this is used in a realtime + /// application since the buffer will grow indefinately! + /// + public int PitchRecordHistorySize + { + get { return m_pitchRecordHistorySize; } + set + { + m_pitchRecordHistorySize = value; + + m_pitchRecords.Capacity = m_pitchRecordHistorySize; + } + } + + /// + /// Get the current pitch records + /// + public IList PitchRecords + { + get { return m_pitchRecords.AsReadOnly(); } + } + + /// + /// Get the latest pitch record + /// + public PitchRecord CurrentPitchRecord + { + get { return m_curPitchRecord; } + } + + /// + /// Get the current pitch position + /// + public long CurrentPitchSamplePosition + { + get { return m_curPitchSamplePos; } + } + + /// + /// Get the minimum frequency that can be detected + /// + public static float MinDetectFrequency + { + get { return kMinFreq; } + } + + /// + /// Get the maximum frequency that can be detected + /// + public static float MaxDetectFrequency + { + get { return kMaxFreq; } + } + + /// + /// Get the frequency step + /// + public static double FrequencyStep + { + get { return Math.Pow(2.0, 1.0 / kOctaveSteps); } + } + + /// + /// Get the number of samples that the detected pitch is offset from the input buffer. + /// This is just an estimate to sync up the samples and detected pitch + /// + public int DetectSampleOffset + { + get { return (m_pitchBufSize + m_detectOverlapSamples) / 2; } + } + + /// + /// Reset the pitch tracker. Call this when the sample position is + /// not consecutive from the previous position + /// + public void Reset() + { + m_curPitchIndex = 0; + m_curPitchSamplePos = 0; + m_pitchRecords.Clear(); + m_iirFilterLoLo.Reset(); + m_iirFilterLoHi.Reset(); + m_iirFilterHiLo.Reset(); + m_iirFilterHiHi.Reset(); + m_circularBufferLo.Reset(); + m_circularBufferLo.Clear(); + m_circularBufferHi.Reset(); + m_circularBufferHi.Clear(); + m_pitchBufLo.Clear(); + m_pitchBufHi.Clear(); + + m_circularBufferLo.StartPosition = -m_detectOverlapSamples; + m_circularBufferLo.Available = m_detectOverlapSamples; + m_circularBufferHi.StartPosition = -m_detectOverlapSamples; + m_circularBufferHi.Available = m_detectOverlapSamples; + } + + /// + /// Process the passed in buffer of data. During this call, the PitchDetected event will + /// be fired zero or more times, depending how many pitch records will fit in the new + /// and previously cached buffer. + /// + /// This means that there is no size restriction on the buffer that is passed into ProcessBuffer. + /// For instance, ProcessBuffer can be called with one very large buffer that contains all of the + /// audio to be processed (many PitchDetected events will be fired), or just a small buffer at + /// a time which is more typical for realtime applications. In the latter case, the PitchDetected + /// event might not be fired at all since additional calls must first be made to accumulate enough + /// data do another pitch detect operation. + /// + /// Input buffer. Samples must be in the range -1.0 to 1.0 + /// Number of samples to process. Zero means all samples in the buffer + public void ProcessBuffer(float[] inBuffer, int sampleCount = 0) + { + if (inBuffer == null) + throw new ArgumentNullException("inBuffer", "Input buffer cannot be null"); + + int samplesProcessed = 0; + int srcLength = sampleCount == 0 ? inBuffer.Length : Math.Min(sampleCount, inBuffer.Length); + + while (samplesProcessed < srcLength) + { + int frameCount = Math.Min(srcLength - samplesProcessed, m_pitchBufSize + m_detectOverlapSamples); + + m_iirFilterLoLo.FilterBuffer(inBuffer, samplesProcessed, m_pitchBufLo, 0, frameCount); + m_iirFilterLoHi.FilterBuffer(m_pitchBufLo, 0, m_pitchBufLo, 0, frameCount); + + m_iirFilterHiLo.FilterBuffer(inBuffer, samplesProcessed, m_pitchBufHi, 0, frameCount); + m_iirFilterHiHi.FilterBuffer(m_pitchBufHi, 0, m_pitchBufHi, 0, frameCount); + + m_circularBufferLo.WriteBuffer(m_pitchBufLo, frameCount); + m_circularBufferHi.WriteBuffer(m_pitchBufHi, frameCount); + + // Loop while there is enough samples in the circular buffer + while (m_circularBufferLo.ReadBuffer(m_pitchBufLo, m_curPitchSamplePos, m_pitchBufSize + m_detectOverlapSamples)) + { + float pitch1; + float pitch2 = 0.0f; + float detectedPitch = 0.0f; + + m_circularBufferHi.ReadBuffer(m_pitchBufHi, m_curPitchSamplePos, m_pitchBufSize + m_detectOverlapSamples); + + pitch1 = m_dsp.DetectPitch(m_pitchBufLo, m_pitchBufHi, m_pitchBufSize); + + if (pitch1 > 0.0f) + { + // Shift the buffers left by the overlapping amount + m_pitchBufLo.Copy(m_pitchBufLo, m_detectOverlapSamples - 1, 0, m_pitchBufSize); + m_pitchBufHi.Copy(m_pitchBufHi, m_detectOverlapSamples - 1, 0, m_pitchBufSize); + + pitch2 = m_dsp.DetectPitch(m_pitchBufLo, m_pitchBufHi, m_pitchBufSize); + + if (pitch2 > 0.0f) + { + float fDiff = Math.Max(pitch1, pitch2) / Math.Min(pitch1, pitch2) - 1.0f; + + if (fDiff < m_maxOverlapDiff) + detectedPitch = (pitch1 + pitch2) * 0.5f; + } + } + + // Log the pitch record + AddPitchRecord(detectedPitch); + + m_curPitchSamplePos += m_samplesPerPitchBlock; + m_curPitchIndex++; + } + + samplesProcessed += frameCount; + } + } + + /// + /// Setup + /// + private void Setup() + { + if (m_sampleRate < 1.0f) + return; + + m_dsp = new PitchDsp(m_sampleRate, kMinFreq, kMaxFreq, m_detectLevelThreshold); + + m_iirFilterLoLo = new IIRFilter(); + m_iirFilterLoLo.Proto = IIRFilter.ProtoType.Butterworth; + m_iirFilterLoLo.Type = IIRFilter.FilterType.HP; + m_iirFilterLoLo.Order = 5; + m_iirFilterLoLo.FreqLow = 45.0f; + m_iirFilterLoLo.SampleRate = (float)m_sampleRate; + + m_iirFilterLoHi = new IIRFilter(); + m_iirFilterLoHi.Proto = IIRFilter.ProtoType.Butterworth; + m_iirFilterLoHi.Type = IIRFilter.FilterType.LP; + m_iirFilterLoHi.Order = 5; + m_iirFilterLoHi.FreqHigh = 280.0f; + m_iirFilterLoHi.SampleRate = (float)m_sampleRate; + + m_iirFilterHiLo = new IIRFilter(); + m_iirFilterHiLo.Proto = IIRFilter.ProtoType.Butterworth; + m_iirFilterHiLo.Type = IIRFilter.FilterType.HP; + m_iirFilterHiLo.Order = 5; + m_iirFilterHiLo.FreqLow = 45.0f; + m_iirFilterHiLo.SampleRate = (float)m_sampleRate; + + m_iirFilterHiHi = new IIRFilter(); + m_iirFilterHiHi.Proto = IIRFilter.ProtoType.Butterworth; + m_iirFilterHiHi.Type = IIRFilter.FilterType.LP; + m_iirFilterHiHi.Order = 5; + m_iirFilterHiHi.FreqHigh = 2000.0f; + m_iirFilterHiHi.SampleRate = (float)m_sampleRate; + + m_detectOverlapSamples = (int)(kDetectOverlapSec * m_sampleRate); + m_maxOverlapDiff = kMaxOctaveSecRate * kDetectOverlapSec; + + m_pitchBufSize = (int)(((1.0f / (float)kMinFreq) * 2.0f + ((kAvgCount - 1) * kAvgOffset)) * m_sampleRate) + 16; + m_pitchBufLo = new float[m_pitchBufSize + m_detectOverlapSamples]; + m_pitchBufHi = new float[m_pitchBufSize + m_detectOverlapSamples]; + m_samplesPerPitchBlock = (int)Math.Round(m_sampleRate / m_pitchRecordsPerSecond); + + m_circularBufferLo = new CircularBuffer((int)(kCircularBufSaveTime * m_sampleRate + 0.5f) + 10000); + m_circularBufferHi = new CircularBuffer((int)(kCircularBufSaveTime * m_sampleRate + 0.5f) + 10000); + } + + /// + /// The pitch was detected - add the record + /// + /// + private void AddPitchRecord(float pitch) + { + int midiNote = 0; + int midiCents = 0; + + PitchDsp.PitchToMidiNote(pitch, out midiNote, out midiCents); + + var record = new PitchRecord(); + + record.RecordIndex = m_curPitchIndex; + record.Pitch = pitch; + record.MidiNote = midiNote; + record.MidiCents = midiCents; + + m_curPitchRecord = record; + + if (m_recordPitchRecords) + { + if (m_pitchRecordHistorySize > 0 && m_pitchRecords.Count >= m_pitchRecordHistorySize) + m_pitchRecords.RemoveAt(0); + + m_pitchRecords.Add(record); + } + + if (this.PitchDetected != null) + this.PitchDetected(this, record); + } + + /// + /// Stores one record + /// + public struct PitchRecord + { + /// + /// The index of the pitch record since the last Reset call + /// + public int RecordIndex { get; set; } + + /// + /// The detected pitch + /// + public float Pitch { get; set; } + + /// + /// The detected MIDI note, or 0 for no pitch + /// + public int MidiNote { get; set; } + + /// + /// The offset from the detected MIDI note in cents, from -50 to +50. + /// + public int MidiCents { get; set; } + } + } +} \ No newline at end of file diff --git a/Vocaluxe/Vocaluxe.csproj b/Vocaluxe/Vocaluxe.csproj index d0c8f9ba..bc2260e4 100644 --- a/Vocaluxe/Vocaluxe.csproj +++ b/Vocaluxe/Vocaluxe.csproj @@ -268,6 +268,11 @@ + + + + +