Pitch detection (F0 estimation). YIN, McLeod, pYIN, autocorrelation, AMDF, HPS, cepstrum, SWIPE.
|
YIN — cumulative mean normalized difference |
HPS — harmonic product spectrum |
Chroma, chord and key detection moved to @audio/mir (
mir-chroma,mir-chord,mir-key).
npm install @audio/pitch
import { yin, mcleod } from '@audio/pitch'
let fs = 44100
let frame = new Float32Array(2048) // fill from your audio source
let result = yin(frame, { fs })
// → { freq: 440.1, clarity: 0.97 } or nullWorks in Node.js and browser. No Web Audio API needed — operates on raw
Float32Arraysamples.
Sliding windows — call repeatedly as new samples arrive:
let hop = 512
for (let i = 0; i + 2048 <= samples.length; i += hop) {
let frame = samples.subarray(i, i + 2048)
let result = yin(frame, { fs })
if (result) console.log(i / fs, result.freq.toFixed(1))
}Harmony pipeline — chroma → chord → key lives in @audio/mir:
import { chroma, chord, smoothChords, key } from '@audio/mir'
let frames = []
for (let i = 0; i + 4096 <= samples.length; i += 2048) {
frames.push(chroma(samples.subarray(i, i + 4096), { fs, method: 'nnls' }))
}
let chords = smoothChords(frames, { selfProb: 0.5 })
let k = key(frames)All pitch algorithms return { freq, clarity } | null:
freq— fundamental frequency in Hzclarity— algorithm-specific confidence in[0, 1]null— no periodic structure found (silence, noise, polyphony)
Time-domain algorithms (YIN, McLeod, pYIN, autocorrelation, AMDF) accept any buffer length. Spectral algorithms (HPS, cepstrum, SWIPE) require power-of-2 length.
Each algorithm is also installable standalone: npm install @audio/pitch-yin etc.
de Cheveigné & Kawahara, 2002. The reference algorithm for monophonic pitch estimation. Most cited, most tested, most robust.
import yin from '@audio/pitch-yin'
let result = yin(samples, { fs: 44100 })| Param | Default | |
|---|---|---|
fs |
44100 |
Sample rate (Hz) |
threshold |
0.15 |
CMND threshold — lower = stricter, fewer detections |
minFreq |
— | Minimum frequency (Hz); also uses more of the frame for low pitches |
maxFreq |
— | Maximum frequency (Hz) |
Pass the expected range when known: yin(samples, { fs, minFreq: 60, maxFreq: 520 }). Besides rejecting out-of-range results, minFreq lets YIN use a longer comparison window and improves low-pitch detection near onsets.
Use when: General-purpose monophonic pitch tracking — speech, singing, solo instruments. The most reliable choice when in doubt.
Not for: Polyphonic audio (returns dominant or null), real-time with hard latency budgets (needs full window).
Ref: de Cheveigné & Kawahara, "YIN, a fundamental frequency estimator for speech and music", JASA 2002.
Complexity:
McLeod & Wyvill, 2005. Normalized square difference with smarter peak picking. Handles smaller windows — good for vibrato and fast pitch changes.
import mcleod from '@audio/pitch-mcleod'
let result = mcleod(samples, { fs: 44100 })| Param | Default | |
|---|---|---|
fs |
44100 |
Sample rate (Hz) |
threshold |
0.9 |
Peak selection threshold as fraction of global max |
Use when: Vibrato tracking, small hop sizes, singing voice where YIN occasionally double-triggers.
Not for: Highly noisy signals (NSDF is less thresholded than YIN's CMND).
Ref: McLeod & Wyvill, "A smarter way to find pitch", ICMC 2005.
Complexity:
Mauch & Dixon, 2014. Probabilistic YIN — runs YIN at multiple thresholds weighted by a Beta(2, 18) prior, producing a distribution over candidate pitches instead of a single hard pick. More robust than YIN on ambiguous frames.
import pyin from '@audio/pitch-pyin'
let result = pyin(samples, { fs: 44100 })
// → { freq: 440.1, clarity: 0.92, candidates: [{ freq: 440.1, prob: 0.85 }, ...] }| Param | Default | |
|---|---|---|
fs |
44100 |
Sample rate (Hz) |
minFreq |
50 |
Minimum detectable frequency (Hz) |
maxFreq |
2000 |
Maximum detectable frequency (Hz) |
Over a sequence of frames, track runs pYIN's second stage and notes segments its result into notes:
import { track, notes } from '@audio/pitch-pyin'
let { times, f0, voiced, prob } = track(samples, { fs: 44100 }) // frame i at i·hopSize/fs, f0 0 where unvoiced
let events = notes(samples, { fs: 44100 }) // → [{ time, duration, freq, midi, clarity }, ...]
let write = notes({ fs: 44100 }) // streaming, track too
write(block) // → the notes finished so far
write() // → the rest| Param | Default | |
|---|---|---|
frameSize |
2048 at 44.1 kHz |
Frame (samples): the shortest power of 2 whose YIN window holds a period of minFreq |
hopSize |
256 at 44.1 kHz |
Hop (samples), ≈ 5.8 ms |
minDuration |
0.1 |
notes: shortest note kept (s) |
candidates |
YIN's | Stage 1: another observation model for the HMM, such as @audio/neural-pitch's |
track Viterbi-decodes pYIN's pitch HMM. States are 0.1-semitone bins from minFreq to maxFreq, voiced or unvoiced; pitch moves at most 35.92 octaves per second and voicing switches with probability 0.01 (librosa.pyin's defaults). Voiced states take half their candidates' probability (the Vamp plugin's yinTrust), which keeps an unvoiced path open, so a leap between notes is not followed an octave off. A frame is decided once every Viterbi path through it agrees, so memory stays bounded at any length. Against librosa.pyin 1.0: the difference function is YIN's own (librosa zero-extends the frame, which pulls low pitches sharp), candidates in one bin add up, and f0 is the candidate inside the decoded bin, not the bin centre.
notes is Tony's note model, with the pYIN Vamp plugin's parameters and output: attack, stable and silent states for 3 pitches per semitone. Vibrato and a scoop into a note stay inside it; the same pitch played again starts a new note where the level rises 1/0.7-fold within two frames; a fast glide joins the notes at its ends, a slow one steps through the notes it passes. freq is the median pitch of the note's frames, clarity their mean voiced probability.
candidates replaces stage 1 and leaves the HMM and the note model as they are. It has candidates.js's signature: (frameSize, fs, minFreq, maxFreq) → run, where run(frame) returns the candidate count after filling run.freq, run.prob (each candidate's pitch probability, at most 1 in all) and run.rms (the level the note onsets read); run.frameSize and run.lead set how many samples a frame holds and how many of them precede its time. Without it the output is the same, bit for bit.
Use when: Ambiguous pitched content — breathy vocals, noisy recordings, or when you need a pitch posterior for downstream HMM tracking. Melody of a voice or solo instrument: smoothed f0 with voicing (track), note events (notes).
Not for: Polyphonic audio: one pitch per frame.
Ref: Mauch & Dixon, "pYIN: A Fundamental Frequency Estimator Using Probabilistic Threshold Distributions", ICASSP 2014. Mauch et al., "Computer-aided Melody Note Transcription Using the Tony Software: Accuracy and Efficiency", TENOR 2015.
Normalized autocorrelation — the simplest pitch estimator. Educational baseline.
import autocorrelation from '@audio/pitch-autocorrelation'
let result = autocorrelation(samples, { fs: 44100 })| Param | Default | |
|---|---|---|
fs |
44100 |
Sample rate (Hz) |
threshold |
0.5 |
Minimum normalized autocorrelation value to accept |
Use when: Learning, quick prototypes, signals with strong dominant periodicity and low noise.
Not for: Production — octave errors are common without additional heuristics.
Ref: Rabiner, "Use of autocorrelation analysis for pitch detection", IEEE TASSP 1977.
Complexity:
Ross et al., 1974. Average Magnitude Difference Function — the classical predecessor to YIN. Measures average absolute difference between a signal and its delayed copy; minima indicate periodicity.
import amdf from '@audio/pitch-amdf'
let result = amdf(samples, { fs: 44100 })| Param | Default | |
|---|---|---|
fs |
44100 |
Sample rate (Hz) |
minFreq |
50 |
Minimum detectable frequency (Hz) |
maxFreq |
2000 |
Maximum detectable frequency (Hz) |
threshold |
0.3 |
Normalized AMDF dip threshold |
Use when: Low-complexity environments, embedded systems. Simpler and cheaper than YIN (no squaring, no cumulative normalization).
Not for: Noisy signals — lacks YIN's cumulative normalization that suppresses octave errors.
Ref: Ross et al., "Average magnitude difference function pitch extractor", IEEE TASSP 1974.
Complexity:
Schroeder, 1968. Harmonic Product Spectrum — multiplies the spectrum by its downsampled copies so that harmonic peaks align at the fundamental. Robust to the missing-fundamental problem.
import hps from '@audio/pitch-hps'
let result = hps(samples, { fs: 44100 })| Param | Default | |
|---|---|---|
fs |
44100 |
Sample rate (Hz) |
harmonics |
5 |
Number of harmonic products |
minFreq |
50 |
Minimum detectable frequency (Hz) |
maxFreq |
4000 |
Maximum detectable frequency (Hz) |
cents |
10 |
Candidate spacing in cents |
threshold |
0.1 |
Minimum clarity to accept |
Use when: Harmonic-rich signals (guitar, piano, brass). Naturally handles missing fundamentals.
Not for: Pure sinusoids (only one harmonic), very noisy signals.
Ref: Schroeder, "Period histogram and product spectrum", JASA 1968.
Requires: Power-of-2 window length.
Noll, 1967. Real cepstrum —
import cepstrum from '@audio/pitch-cepstrum'
let result = cepstrum(samples, { fs: 44100 })| Param | Default | |
|---|---|---|
fs |
44100 |
Sample rate (Hz) |
minFreq |
50 |
Minimum detectable frequency (Hz) |
maxFreq |
2000 |
Maximum detectable frequency (Hz) |
threshold |
0.3 |
Minimum clarity to accept |
Use when: Harmonic signals where you want a clean spectral-domain method. Good pedagogical complement to time-domain algorithms.
Not for: Low-pitched signals (quefrency resolution is limited by window length).
Ref: Noll, "Cepstrum pitch determination", JASA 1967.
Requires: Power-of-2 window length.
Camacho & Harris, 2008. SWIPE' (Sawtooth Waveform Inspired Pitch Estimator, prime harmonics). Measures spectral similarity between the window and a sawtooth template whose lobes sit at prime harmonics. More accurate than HPS on clean instrumental signals; robust against octave errors because only prime harmonics contribute.
Simplified single-window form: uses one FFT instead of the multi-resolution loudness pyramid of the original paper — sufficient for stationary windows.
import swipe from '@audio/pitch-swipe'
let result = swipe(samples, { fs: 44100 })| Param | Default | |
|---|---|---|
fs |
44100 |
Sample rate (Hz) |
minFreq |
60 |
Minimum detectable frequency (Hz) |
maxFreq |
4000 |
Maximum detectable frequency (Hz) |
cents |
10 |
Candidate spacing in cents |
threshold |
0.15 |
Minimum clarity to accept |
Use when: Clean instrumental signals, studio recordings, where sub-Hz accuracy matters.
Not for: Very noisy or reverberant signals (single-window form lacks multi-resolution robustness of the full SWIPE').
Ref: Camacho & Harris, "A sawtooth waveform inspired pitch estimator for speech and music", JASA 2008.
Requires: Power-of-2 window length.
| YIN | McLeod | pYIN | AMDF | HPS | Cepstrum | SWIPE | |
|---|---|---|---|---|---|---|---|
| Domain | time | time | time | time | spectral | spectral | spectral |
| Accuracy | ★★★★★ | ★★★★ | ★★★★★ | ★★★ | ★★★★ | ★★★ | ★★★★★ |
| Noise robustness | ★★★★★ | ★★★★ | ★★★★★ | ★★★ | ★★★ | ★★★ | ★★★★ |
| Octave errors | rare | rare | rare | common | rare | occasional | rare |
| Missing fundamental | no | no | no | no | yes | yes | yes |
| Min window | ~4 periods | ~2 periods | ~4 periods | ~4 periods | power of 2 | power of 2 | power of 2 |
| Best for | general | vibrato | ambiguous | embedded | harmonic-rich | pedagogical | studio |
- fourier-transform — FFT used by spectral algorithms
- mir — chroma, chord, key, melody, structure, fingerprint
- beat — onset detection, tempo estimation, beat tracking
- digital-filter — filter design and processing
- stretch — time stretching and pitch shifting
- shift — pitch shifting algorithms