Files
k-tuner/src/domain/harmonic-analyzer.ts

82 lines
2.2 KiB
TypeScript

/**
* Pure domain logic for harmonic analysis.
* Helps identify fundamental frequency from harmonics.
*/
/**
* Check if two frequencies have a harmonic relationship.
* @param f1 - First frequency
* @param f2 - Second frequency
* @param tolerance - Tolerance in cents (default 50)
*/
export function isHarmonic(f1: number, f2: number, tolerance: number = 50): boolean {
const ratio = f2 / f1;
const roundedRatio = Math.round(ratio);
// Check if ratio is close to an integer (2x, 3x, 4x, etc.)
if (roundedRatio < 2 || roundedRatio > 6) {
return false;
}
// Calculate cents difference
const expectedFreq = f1 * roundedRatio;
const cents = 1200 * Math.log2(f2 / expectedFreq);
return Math.abs(cents) < tolerance;
}
/**
* Detect harmonic series from a list of frequency candidates.
* Returns the fundamental frequency if a harmonic series is detected.
*/
export function detectHarmonicSeries(frequencies: number[]): number | null {
if (frequencies.length < 2) {
return null;
}
// Sort frequencies
const sorted = [...frequencies].sort((a, b) => a - b);
// Check if we have a harmonic series starting from the lowest frequency
const fundamental = sorted[0];
let harmonicCount = 1; // fundamental counts as first harmonic
for (let i = 1; i < sorted.length; i++) {
if (isHarmonic(fundamental, sorted[i])) {
harmonicCount++;
}
}
// If we found at least 2 harmonics (fundamental + 1 more), it's likely a harmonic series
if (harmonicCount >= 2) {
return fundamental;
}
return null;
}
/**
* Find the fundamental frequency from a list of candidates.
* Uses harmonic analysis to filter out overtones.
*/
export function findFundamentalFromHarmonics(candidates: number[]): number {
if (candidates.length === 0) {
return 0;
}
if (candidates.length === 1) {
return candidates[0];
}
// Try to detect harmonic series
const fundamental = detectHarmonicSeries(candidates);
if (fundamental) {
return fundamental;
}
// If no harmonic series detected, return the lowest frequency
// (most conservative approach)
return Math.min(...candidates);
}