82 lines
2.2 KiB
TypeScript
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);
|
|
}
|