UNPKG

bluesharp-pitch-detection

Version:

High-accuracy pitch detection algorithms for musical applications

282 lines (250 loc) 13.2 kB
/* * Copyright (c) 2023 Christian Kierdorf * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * */ import NoteUtils from './NoteUtils.js'; /** * Implementation of the YIN algorithm for pitch detection. * * This class provides a clean, efficient implementation of the YIN algorithm * for detecting the fundamental frequency (pitch) of an audio signal. * The implementation allows configuring the frequency range and provides * both the detected pitch and a confidence value. * * The YIN algorithm works by: * 1. Computing the difference function to measure signal similarity at various lags * 2. Normalizing the difference function using the CMNDF algorithm * 3. Finding the first minimum in the CMNDF below a threshold * 4. Refining the estimate using parabolic interpolation * 5. Calculating the pitch and confidence from the refined estimate */ class YINPitchDetector { // Constants static NO_DETECTED_PITCH = -1; // Indicates no pitch detected static DEFAULT_MIN_FREQUENCY = 80.0; // Default minimum frequency in Hz static DEFAULT_MAX_FREQUENCY = 4835.0; // Default maximum frequency in Hz static YIN_MINIMUM_THRESHOLD = 0.4; // YIN's minimum threshold for periodicity static RMS_SCALING_FACTOR = 0.3; // Scaling factor for RMS calculation // Configurable properties static minFrequency = YINPitchDetector.DEFAULT_MIN_FREQUENCY; static maxFrequency = YINPitchDetector.DEFAULT_MAX_FREQUENCY; /** * Sets the minimum frequency that can be detected (in Hz). * @param {number} frequency - The minimum frequency in Hz */ static setMinFrequency(frequency) { YINPitchDetector.minFrequency = frequency; } /** * Gets the minimum frequency that can be detected (in Hz). * @returns {number} The minimum frequency in Hz */ static getMinFrequency() { return YINPitchDetector.minFrequency; } /** * Sets the maximum frequency that can be detected (in Hz). * @param {number} frequency - The maximum frequency in Hz */ static setMaxFrequency(frequency) { YINPitchDetector.maxFrequency = frequency; } /** * Gets the maximum frequency that can be detected (in Hz). * @returns {number} The maximum frequency in Hz */ static getMaxFrequency() { return YINPitchDetector.maxFrequency; } /** * Detects the pitch of an audio signal using the YIN algorithm. * * This implementation calculates the pitch (fundamental frequency) of an audio signal * by analyzing periodic patterns in the signal's waveform. The algorithm evaluates the * signal's periodicity with the help of the cumulative mean normalized difference function (CMNDF), * and dynamically determines thresholds for improved accuracy. * If a pitch is successfully detected, the method refines the results and provides a * confidence value to indicate the reliability of the estimate. * * @param {Array<number>} audioData - An array of values representing the audio signal to analyze. * @param {number} sampleRate - The sample rate of the audio signal in Hz. * @returns {Object} An object containing: * - pitch (number): The detected pitch in Hz, or NO_DETECTED_PITCH if none detected. * - confidence (number): A value between 0.0 and 1.0 indicating detection reliability. */ static detectPitch(audioData, sampleRate) { // Determine the size of the input buffer (length of the audio sample data) const bufferSize = audioData.length; // Calculate tau limits based on the frequency range const maxTau = Math.floor(sampleRate / NoteUtils.addCentsToFrequency(-25, YINPitchDetector.minFrequency)); const minTau = Math.floor(sampleRate / NoteUtils.addCentsToFrequency(25, YINPitchDetector.maxFrequency)); // Step 1: Compute the difference function to measure signal similarity at various lags const difference = YINPitchDetector.computeDifferenceFunction(audioData, bufferSize); // Step 2: Normalize the difference function using the CMNDF algorithm const cmndf = YINPitchDetector.computeCMNDFInRange(difference, minTau, maxTau); // Step 3: Compute the Root Mean Square (RMS) to assess the signal's energy level const rms = YINPitchDetector.calcRMS(audioData); // Step 4: Adapt the YIN threshold based on RMS to improve pitch detection reliability const dynamicThreshold = Math.min(0.5, YINPitchDetector.YIN_MINIMUM_THRESHOLD * (1 + YINPitchDetector.RMS_SCALING_FACTOR / (rms + 0.01))); // Step 5: Find the first minimum in the CMNDF below the threshold; this corresponds to the lag (tau) const tauEstimate = YINPitchDetector.findFirstMinimum(cmndf, dynamicThreshold, minTau, maxTau); // Step 6: If a valid lag is found, refine it using parabolic interpolation for accuracy if (tauEstimate !== -1) { const refinedTau = YINPitchDetector.parabolicInterpolation(cmndf, tauEstimate); // Ensure the refined lag is valid before proceeding if (refinedTau > 0) { // Calculate the confidence by evaluating how close the CMNDF value is to the threshold const confidence = 1 - Math.pow((cmndf[tauEstimate] / dynamicThreshold), 2); // Derive the pitch (fundamental frequency) from the sample rate and tau const pitch = sampleRate / refinedTau; // Return the detected pitch and confidence in a result object return { pitch, confidence }; } } // Step 7: If no pitch is detected, return no pitch with confidence set to 0.0 return { pitch: YINPitchDetector.NO_DETECTED_PITCH, confidence: 0.0 }; } /** * Refines the estimate of the lag value `tau` using parabolic interpolation * for improved accuracy in analyzing periodic signals. * * @param {Array<number>} cmndf - An array representing the cumulative mean normalized difference function (CMNDF) * @param {number} tau - An integer representing the initial lag value * @returns {number} The refined lag value as a double, obtained through parabolic interpolation */ static parabolicInterpolation(cmndf, tau) { if (tau <= 0 || tau >= cmndf.length - 1) { return tau; } const x0 = cmndf[tau - 1]; const x1 = cmndf[tau]; const x2 = cmndf[tau + 1]; return tau + (x0 - x2) / (2 * (x0 - 2 * x1 + x2)); // Parabolic refinement } /** * Determines whether a specific element in an array is a local minimum. * A local minimum is defined as an element that is smaller than its * immediate neighbors. * * @param {Array<number>} array - The array of values to evaluate * @param {number} index - The index of the element to check for being a local minimum * @returns {boolean} True if the element at the specified index is a local minimum; * false otherwise */ static isLocalMinimum(array, index) { if (index <= 0 || index >= array.length - 1) { return false; } return array[index] < array[index - 1] && array[index] < array[index + 1]; } /** * Finds the first index in the Cumulative Mean Normalized Difference Function (CMNDF) * array where the value is below a specified threshold and is a local minimum. * * @param {Array<number>} cmndf - An array representing the cumulative mean normalized * difference function (CMNDF). Each element corresponds to the * periodicity measure for a specific lag value. * @param {number} threshold - A value representing the threshold for identifying valid * CMNDF values. Only elements below this value will be considered. * @param {number} minTau - An integer specifying the minimum lag value to start the search from. * @param {number} maxTau - An integer specifying the maximum lag value up to which the search * should be conducted. * @returns {number} The index of the first local minimum that satisfies the threshold condition; * returns -1 if no valid index is found within the given range. */ static findFirstMinimum(cmndf, threshold, minTau, maxTau) { for (let tau = minTau; tau < maxTau; tau++) { if (cmndf[tau] < threshold && YINPitchDetector.isLocalMinimum(cmndf, tau)) { return tau; } } return -1; } /** * Computes the Cumulative Mean Normalized Difference Function (CMNDF) for the given range of τ values. * This method calculates a normalized measure within the relevant τ range, specified by minTau and maxTau, * while ignoring values outside that range. * * @param {Array<number>} difference - An array of difference values to compute the CMNDF from * @param {number} minTau - The minimum index in the τ range to be considered for calculation * @param {number} maxTau - The maximum index in the τ range to be considered for calculation * @returns {Array<number>} An array representing the CMNDF values, where values outside the specified range are set to 1 */ static computeCMNDFInRange(difference, minTau, maxTau) { const cmndf = new Array(difference.length).fill(0); cmndf[0] = 1; let cumulativeSum = 0; for (let tau = 1; tau < difference.length; tau++) { cumulativeSum += difference[tau]; // Only calculate in the relevant range if (tau >= minTau && tau <= maxTau) { cmndf[tau] = difference[tau] / ((cumulativeSum / tau) + 1e-10); } else { cmndf[tau] = 1; // Ignore values outside the range } } return cmndf; } /** * Computes the difference function for an audio signal, used as an intermediate * step in signal processing algorithms like pitch detection. The difference function * evaluates the dissimilarity between overlapping segments of the audio data at * various time lags to assess periodicity. * * @param {Array<number>} audioData - An array of values representing the original audio signal * to be analyzed. Each value corresponds to the amplitude of the * signal at a specific point in time. * @param {number} bufferSize - The size of the buffer to process in the audio data. This determines * the range of time lags to evaluate in the difference function. * @returns {Array<number>} An array of values representing the computed difference function. * Each value corresponds to the dissimilarity measure for a specific time lag. */ static computeDifferenceFunction(audioData, bufferSize) { const audioSquared = audioData.map(sample => sample * sample); const difference = new Array(Math.floor(bufferSize / 2)).fill(0); for (let tau = 0; tau < difference.length; tau++) { let sum = 0; for (let i = 0; i < Math.floor(bufferSize / 2); i++) { sum += audioSquared[i] + audioSquared[i + tau] - 2 * audioData[i] * audioData[i + tau]; } difference[tau] = sum; } return difference; } /** * Calculates the Root Mean Square (RMS) of an audio signal. * * RMS is a measure of the signal's power and helps to determine * whether it's strong enough for pitch detection. * * @param {Array<number>} audioData - The array of sample amplitudes of the audio signal. * @returns {number} - The RMS value of the audio data. */ static calcRMS(audioData) { if (!Array.isArray(audioData) || audioData.length === 0) { return 0; } const sumOfSquares = audioData.reduce((sum, sample) => sum + sample * sample, 0); return Math.sqrt(sumOfSquares / audioData.length); } } export default YINPitchDetector;