This is the tenth post in the LocalPapa Notes dev-log series. Earlier we covered the phone's "motion sensors"; this time we switch sensors — to the microphone. From one Web Audio pipeline we built four tools: a tuner, a decibel meter, a metronome and a voice changer.
They share one premise: the microphone is a sensor too, and the browser can read it directly — entirely locally, no recording, no upload.
One shared audio pipeline
All four tools start almost identically: getUserMedia({audio}) for the mic, connected to an AudioContext.
There's a counter-intuitive shared detail: we deliberately turn off echoCancellation, noiseSuppression and AGC (automatic gain control). Those are great for calls but disastrous for measurement — they "beautify" the signal, destroying the true pitch and energy. For a tuner and a decibel meter, you want the unprocessed raw waveform.
Tuner: hearing pitch via autocorrelation
The tuner answers: "the note you're playing — which note is it, and is it in tune?"
It detects the fundamental frequency with the ACF2+ autocorrelation algorithm: take the time-domain waveform, set a silence gate via RMS (too quiet → don't judge), trim leading/trailing noise to stabilise the signal, compute the autocorrelation function, find the periodic peak T0, then refine with parabolic interpolation: frequency = sample rate / T0. It's a standard, stable approach for monophonic detection.
With the frequency, note name and cents are just maths: midi = round(12·log2(freq/A4)+69), deviation cents = 1200·log2(freq/nearest-note freq). The needle is weighted-smoothed (smooth·0.7 + cents·0.3) to stop it jittering, and turns green within 5 cents. The A4 reference is adjustable (415–466Hz) for early music and different ensembles.
Decibel meter: honest about "you can't measure it precisely"
The decibel meter reuses the same mic pipeline, computes energy via RMS, dB = 20·log10(rms) + calibration, and draws min/avg/max plus a rolling history curve.
But there's an honesty problem: the browser can't get the phone microphone's hardware calibration. Which means we can't give a truly accurate absolute decibel reading.
Many apps pretend they're precise. We chose the opposite: the UI and FAQ clearly label this as an "uncalibrated relative estimate," with a calibration slider to align against a known reference. Do well what you can; be honest about what you can't — that's more responsible than a number that looks professional but misleads.
Metronome: why you can't use setInterval
The metronome looks simplest but hides the deepest lesson.
The instinct is setInterval to go "tick" at intervals. But that drifts — the moment the JavaScript main thread stutters or the tab gets throttled, timing slips. For a metronome, that's fatal.
The correct approach is Chris Wilson's "A Tale of Two Clocks": use setTimeout to wake a scheduler at a low frequency (every 25ms), and schedule every beat falling within a "now + 0.1s lookahead" window via osc.start(precise time) on the audio hardware clock. When a sound fires is decided by the audio hardware, completely immune to JS stutter. The visual beat lights align to that same audio clock (rather than timing themselves), so lights and sound stay forever in sync.
This is my favourite lesson in the whole series: hand timing to the right clock. What the audio hardware should own, don't give to JavaScript.
Voice changer: the "live pitch shift" the browser lacks
The voice changer is the most fun, because the browser has no native real-time pitch shift.
We used another Chris Wilson algorithm, Jungle: two DelayNodes, each modulated with a "sawtooth-varying delay time," cross-faded with "equal-power crossfade" to switch smoothly between the two — achieving live pitch up/down with no audio file and low latency.
The fun part is chaining effects into a composable graph: pitch shift → filter → ring modulation (robot voice) → echo → monitor + recording. The ring-modulation stage uses AudioParam's additive nature: pass-through normally, only truly multiplying by a carrier in robot mode. Preset effects apply all parameters in one click, switched smoothly via setTargetAtTime to avoid pops. Recording taps a separate output through MediaRecorder, playable and downloadable when done.
An easily-missed shared point: give the microphone back
All four tools use the mic, and all do the same easily-missed thing — teardown. On stop, and on page pagehide, they stop the MediaStream's audio tracks and close() the AudioContext.
Otherwise the mic stays occupied, that little red "recording" dot won't disappear, and users get spooked. Borrow the hardware, return it clean.
What we learned
- The microphone is an underrated sensor. Like motion sensors, it's a hardware entry point the browser can reach directly but an AI chat can't.
- Measurement tools must turn off "beautification." echoCancellation/AGC are good for calls, noise for measurement.
- Hand timing to the right clock. The metronome's stability comes from the audio hardware clock, not
setInterval.
- Honestly labelling precision is more professional than faking accuracy. The decibel meter's "uncalibrated relative estimate" is that principle.
- Return borrowed hardware clean. Releasing tracks and closing the context is basic courtesy for audio tools.
The same question, broadened
From motion sensors to the microphone, this line broadens the series' question: the hardware signals a browser can read directly — can they become useful, fun and honest tools, right there?
Tuning, noise metering, click tracks, voice changing — one microphone, four answers, all: yes.
In the next dev log, we step into a long-brewing thread — turning the whole site into a knowledge pack AI agents can read (OKF). Want to follow the series? Bookmark LocalPapa Notes.