Skip to content

Commit

Permalink
(Hopefully) improved detection of frequencies.
Browse files Browse the repository at this point in the history
This scans through several possible harmonics to check if there
is a better match than the purely correlation-based approach.
  • Loading branch information
thetwom committed Sep 15, 2024
1 parent 66c555f commit 01accd5
Show file tree
Hide file tree
Showing 11 changed files with 781 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,27 @@
*/
package de.moekadu.tuner.notedetection

/** Store auto correlation result.
* @param size Number of values in auto correlation.
* @param dt Time shift between two values in auto correlation
*/
class AutoCorrelation(
val size: Int,
val dt: Float,

val dt: Float
) {
/** Array with time shift for each auto correlation entry. */
val times = FloatArray(size) { it * dt }
/** Correlation values. */
val values = FloatArray(size)

/** Values, normalized to range 0 to 1. */
val plotValuesNormalized = FloatArray(size)
/** The zero position in plotValuesNormalized. */
var plotValuesNormalizedZero = 0f

/** Obtain correlation value at given index.
* @param index Index where correlation value is needed.
* @return Correlation value
*/
operator fun get(index: Int) = values[index]
// operator fun set(index: Int, value: Float) {
// values[index] = value
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ package de.moekadu.tuner.notedetection
import de.moekadu.tuner.misc.MemoryPool
import kotlin.math.pow

/** Class to compute auto correlation.
* @param size Number of input values, which is a time series.
* @param windowType Type of windowing to apply for FFT (first step of computing auto correlation).
* WindowingFunction.Tophat disables the windowing.
*/
class Correlation (val size : Int, val windowType : WindowingFunction = WindowingFunction.Tophat) {

private val fft = RealFFT(2 * size)
Expand All @@ -32,8 +37,7 @@ class Correlation (val size : Int, val windowType : WindowingFunction = Windowin
getWindow(windowType, size).copyInto(window)
}

/// Autocorrelation of input.
/**
/** Auto correlation of input.
* @param input Input data which should be correlated (required size: size)
* @param output Output array where we store the autocorrelation, (required size: size+1)
* @param disableWindow if true, we disable windowing, even when it is defined in the constructor.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ class MemoryPoolFrequencyDetectionCollectedResults {
class FrequencyDetectionResultCollector(
private val frequencyMin: Float,
private val frequencyMax: Float,
private val subharmonicsTolerance: Float = 0.05f,
private val subharmonicsPeakRatio: Float = 0.8f,
private val harmonicTolerance: Float = 0.1f,
private val minimumFactorOverLocalMean: Float = 5f,
private val maxGapBetweenHarmonics: Int = 10,
private val maxNumHarmonicsForInharmonicity: Int = 8,
private val windowType: WindowingFunction = WindowingFunction.Tophat,
private val acousticWeighting: AcousticWeighting = AcousticCWeighting()
private val subharmonicsTolerance: Float, // = 0.05f,
private val subharmonicsPeakRatio: Float, // = 0.8f,
private val harmonicTolerance: Float, // = 0.1f,
private val minimumFactorOverLocalMean: Float, // = 5f,
private val maxGapBetweenHarmonics: Int, // = 10,
private val maxNumHarmonicsForInharmonicity: Int, // = 8,
private val windowType: WindowingFunction, // = WindowingFunction.Tophat,
private val acousticWeighting: AcousticWeighting, // = AcousticCWeighting()
) {
private val collectedResultsMemory = MemoryPoolFrequencyDetectionCollectedResults()
private val spectrumAndCorrelationMemory = MemoryPoolCorrelation()
Expand Down Expand Up @@ -148,17 +148,30 @@ class FrequencyDetectionResultCollector(
)
// Log.v("Tuner", "CollectedResults.collectResults: correlationBased frequency = ${collectedResults.memory.correlationBasedFrequency}")
if (collectedResults.memory.correlationBasedFrequency.frequency != 0f) {
findHarmonicsFromSpectrum(
collectedResults.memory.harmonics,
collectedResults.memory.correlationBasedFrequency.frequency,
collectedResults.memory.harmonics.findBestMatchingHarmonics(
collectedResults.memory.correlationBasedFrequency,
collectedResults.memory.autoCorrelation,
frequencyMin,
frequencyMax,
collectedResults.memory.frequencySpectrum,
collectedResults.memory.accuratePeakFrequency,
harmonicTolerance = harmonicTolerance,
minimumFactorOverLocalMean = minimumFactorOverLocalMean,
maxNumFail = maxGapBetweenHarmonics,
relativePeakThreshold = 5e-3f
)

// findHarmonicsFromSpectrum(
// collectedResults.memory.harmonics,
// collectedResults.memory.correlationBasedFrequency.frequency,
// frequencyMin,
// frequencyMax,
// collectedResults.memory.frequencySpectrum,
// collectedResults.memory.accuratePeakFrequency,
// harmonicTolerance = harmonicTolerance,
// minimumFactorOverLocalMean = minimumFactorOverLocalMean,
// maxNumFail = maxGapBetweenHarmonics,
// )
collectedResults.memory.harmonics.sort()

collectedResults.memory.harmonicStatistics.evaluate(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package de.moekadu.tuner.notedetection

/** Predict frequency of a given harmonic, based on previous harmonics.
* This class uses the function
* frequency = f1 * harmonic * (1 + beta * harmonic)
* which is the same as
* frequency = f1 * harmonic + alpha * harmonic**2
* (with alpha = beta * f1) as modelling function and uses a least squares fit on previous
* information to predict new harmonics.
*/
class HarmonicPredictor {
/** Sum of frequency * harmonicNumber */
private var sumFh = 0f
/** Sum of frequency * harmonicNumber**2 */
private var sumFh2 = 0f
/** Sum of harmonicNumber**2 */
private var sumH2 = 0f
/** Sum of harmonicNumber**3 */
private var sumH3 = 0f
/** Sum of harmonicNumber**4 */
private var sumH4 = 0f
/** Alpha factor of modelling function */
private var alpha = 0f
/** Beta factor of modelling function */
private var beta = 0f
/** Base frequency of modelling function */
private var f1 = 0f

/** Reset predictor. */
fun clear() {
sumFh = 0f
sumFh2 = 0f
sumH2 = 0f
sumH3 = 0f
sumH4 = 0f
alpha = 0f
beta = 0f
f1 = 0f
}
/** Add new harmonic to the modelling function.
* @param harmonicNumber Harmonic number.
* @param frequency Frequency of harmonic.
*/
fun add(harmonicNumber: Int, frequency: Float) {
val hSqr = harmonicNumber * harmonicNumber
val hCub = harmonicNumber * hSqr
val hQuad = hSqr * hSqr

sumFh += frequency * harmonicNumber
sumFh2 += frequency * hSqr
sumH2 += hSqr
sumH3 += hCub
sumH4 += hQuad
if (f1 == 0f) {
f1 = frequency / harmonicNumber
} else {
alpha = (sumFh2 * sumH2 - sumFh * sumH3) / (sumH4 * sumH2 - sumH3 * sumH3)
f1 = (sumFh - alpha * sumH3) / sumH2
beta = alpha / f1
}
}

/** Predict frequency of a given harmonic.
* @param harmonicNumber harmonic number.
* @return Predicted frequency.
*/
fun predict(harmonicNumber: Int): Float {
return f1 * harmonicNumber * (1 + beta * harmonicNumber)
}
}
Loading

0 comments on commit 01accd5

Please # to comment.