Polyphonic pitch detection in the browser. No dependencies, no build step.
~16ms latency at 60fps. Detects multiple simultaneous pitches and chords in real-time.
npm install @markusstrasser/pitchpleaseOr just copy pitchplease.js to your project. That's it.
- HTTPS or localhost required - microphone access blocked on HTTP
- User gesture required -
start()must be called from click/tap handler - Chrome, Firefox, Safari, Edge all supported
- Mobile browsers work but may have higher latency
<script src="pitchplease.js"></script>
<script>
const detector = PitchPlease.create({
onUpdate: (data) => console.log(data.pitchClasses),
onChord: (chord) => console.log(chord.full), // "Cmaj7"
});
// Must be called from user gesture (click/tap)
button.onclick = () => detector.start();
</script><script>
import { onMount } from 'svelte';
let chord = '';
let detector;
onMount(() => {
const script = document.createElement('script');
script.src = '/pitchplease.js';
script.onload = () => {
detector = PitchPlease.create({
onChord: (c) => chord = c.full,
});
};
document.head.appendChild(script);
});
function start() {
detector?.start();
}
</script>
<button on:click={start}>Start</button>
<p>{chord}</p>import { useEffect, useRef, useState } from 'react';
export function PitchDetector() {
const [chord, setChord] = useState('');
const detector = useRef(null);
useEffect(() => {
const script = document.createElement('script');
script.src = '/pitchplease.js';
script.onload = () => {
detector.current = window.PitchPlease.create({
onChord: (c) => setChord(c.full),
});
};
document.head.appendChild(script);
}, []);
return (
<>
<button onClick={() => detector.current?.start()}>Start</button>
<p>{chord}</p>
</>
);
}const PitchPlease = require('@markusstrasser/pitchplease');
const chord = PitchPlease.matchChord([0, 4, 7]); // { full: 'C' }Creates a detector instance. Returns { start, stop, togglePause, paused }.
const detector = PitchPlease.create({
onUpdate: (data) => {}, // called every frame (~60fps, ~16ms)
onChord: (chord) => {}, // called when stable chord detected
onError: (err) => {}, // called on errors
fftSize: 16384, // FFT resolution (default: 16384)
stabilityFrames: 4, // frames before chord is stable (default: 4)
});
await detector.start(); // request mic, start detection
detector.stop(); // stop and release mic
detector.togglePause(); // pause/resume
detector.paused; // boolean{
spectrum, // Float32Array - raw FFT magnitudes
binMidi, // Float32Array - MIDI note for each FFT bin
fundMidis, // Float32Array - detected fundamental pitches
fundCount, // number - how many pitches detected
pitchClasses, // number[] - unique pitch classes [0-11]
stable, // boolean - has detection stabilized?
chord, // object | null - detected chord
maxEnergy, // number - loudest FFT bin this frame
}{
root: 'C', // note name
rootPc: 0, // pitch class (0-11)
name: 'Major', // full chord name
abbrev: '', // suffix (m, maj7, dim, etc)
full: 'C', // root + abbrev ("Cmaj7", "F#m", etc)
}PitchPlease.midiToNote(60) // 'C'
PitchPlease.midiToNote(60, true) // 'C4'
PitchPlease.matchChord([0, 4, 7]) // { full: 'C', ... }
PitchPlease.pitchClassToColor(0) // 'hsla(60,80%,50%,1)'
PitchPlease.NOTE_NAMES // ['C','C#','D',...]Triads: Major, Minor, Dim, Aug, Sus4, Sus2, 5 Sevenths: Maj7, 7, m7, m(maj7), dim7, ø7, aug7 Sixths: 6, m6 Extensions: add9, madd9, 9, m9, maj9, 11, 13
- ~16ms per frame (60fps)
- FFT: 16384 bins for 3Hz resolution at 48kHz sample rate
- Processes MIDI range 24-96 (C1-C7)
- Typical CPU: <5% on modern hardware
bun dev-testing.js
# http://localhost:3000- FFT via Web Audio
AnalyserNode(16384 bins) - Adaptive noise floor estimation
- Peak detection with parabolic interpolation
- Harmonic sieve groups peaks into fundamentals
- Pitch class extraction and stability check
- Template matching against chord intervals
MIT