Skip to content

markusstrasser/pitchplease

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PitchPlease

Polyphonic pitch detection in the browser. No dependencies, no build step.

~16ms latency at 60fps. Detects multiple simultaneous pitches and chords in real-time.

Try the live demo →

Install

npm install @markusstrasser/pitchplease

Or just copy pitchplease.js to your project. That's it.

Browser Requirements

  • 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

Usage

Plain HTML

<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>

Svelte

<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>

React

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>
    </>
  );
}

Node/CommonJS (for testing only - no audio in Node)

const PitchPlease = require('@markusstrasser/pitchplease');
const chord = PitchPlease.matchChord([0, 4, 7]); // { full: 'C' }

API

PitchPlease.create(options)

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

onUpdate data

{
  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
}

chord object

{
  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)
}

Utilities

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',...]

Chord Types

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

Performance

  • ~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

Dev

bun dev-testing.js
# http://localhost:3000

How it works

  1. FFT via Web Audio AnalyserNode (16384 bins)
  2. Adaptive noise floor estimation
  3. Peak detection with parabolic interpolation
  4. Harmonic sieve groups peaks into fundamentals
  5. Pitch class extraction and stability check
  6. Template matching against chord intervals

License

MIT

About

No buffers, no delay, no machine learning. Just instant polyphonic pitch detection

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published