<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Nabeel Builds]]></title><description><![CDATA[Nabeel Builds]]></description><link>https://nabeelbuilds.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 13:08:27 GMT</lastBuildDate><atom:link href="https://nabeelbuilds.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Build a pitch detector that runs entirely in the browser]]></title><description><![CDATA[I wanted a pitch detector that opens instantly, asks for nothing but the microphone, and never sends audio to a server. It turns out the browser can do all…]]></description><link>https://nabeelbuilds.hashnode.dev/build-a-pitch-detector-that-runs-entirely-in-the-browser</link><guid isPermaLink="true">https://nabeelbuilds.hashnode.dev/build-a-pitch-detector-that-runs-entirely-in-the-browser</guid><category><![CDATA[Web Audio API]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Nabeel]]></dc:creator><pubDate>Sat, 24 Oct 2026 15:24:51 GMT</pubDate><content:encoded><![CDATA[<p>I wanted a pitch detector that opens instantly, asks for nothing but the microphone, and never sends audio to a server. It turns out the browser can do all of it. Here is the core of the approach, small enough to read in one sitting.</p>
<h2>1. Get clean microphone input</h2>
<p>Browsers "help" voice calls by cleaning up the mic signal. For pitch detection that help gets in the way, so turn it off:</p>
<pre><code class="language-js">const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: false,
    noiseSuppression: false,
    autoGainControl: false,
  },
});
</code></pre>
<p>Ask for the mic only after the user taps a button. iOS Safari also needs the <code>AudioContext</code> to be created or resumed inside that tap.</p>
<h2>2. Feed it into an AnalyserNode</h2>
<pre><code class="language-js">const ctx = new AudioContext();
const source = ctx.createMediaStreamSource(stream);
const analyser = ctx.createAnalyser();
analyser.fftSize = 2048;
source.connect(analyser);
</code></pre>
<p>Nothing is connected to <code>ctx.destination</code>, so you don't hear yourself, and nothing leaves the page.</p>
<h2>3. Find the pitch</h2>
<p>A plain FFT peak is a poor pitch detector: for many voices and instruments the loudest peak is a harmonic, not the note you hear. Time-domain methods like YIN or the McLeod Pitch Method handle this much better. The small <a href="https://www.npmjs.com/package/pitchy">pitchy</a> package implements McLeod:</p>
<pre><code class="language-js">import { PitchDetector } from "pitchy";

const detector = PitchDetector.forFloat32Array(analyser.fftSize);
const input = new Float32Array(detector.inputLength);

function update() {
  analyser.getFloatTimeDomainData(input);
  const [pitch, clarity] = detector.findPitch(input, ctx.sampleRate);
  if (clarity &gt; 0.9) render(pitch); // ignore noisy, unclear frames
  requestAnimationFrame(update);
}
update();
</code></pre>
<p>The clarity threshold matters more than anything else for a steady display. Below about 0.9 you mostly get breath, room noise and the attack of a note.</p>
<h2>4. Turn Hz into a note and cents</h2>
<p>With A4 = 440 Hz as MIDI note 69, every semitone is a factor of 2^(1/12):</p>
<pre><code class="language-js">const NAMES = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"];

function toNote(freq, a4 = 440) {
  const n = 12 * Math.log2(freq / a4) + 69;
  const midi = Math.round(n);
  return {
    name: NAMES[midi % 12],
    octave: Math.floor(midi / 12) - 1,
    cents: Math.round((n - midi) * 100), // -50 to +50
  };
}
</code></pre>
<p><code>toNote(446)</code> gives A4 at +23 cents. Making <code>a4</code> a setting lets people tune to 432 or 442 Hz.</p>
<h2>5. Small things that make it feel good</h2>
<ul>
<li>Smooth the cents value over a few frames, or the needle jitters.</li>
<li>Hold the last note for half a second after the signal drops, so the reading doesn't vanish between notes.</li>
<li>Show a clear message when mic permission is denied, with how to re-enable it per browser.</li>
<li>Start the volume and sensitivity at sensible defaults; most people never open settings.</li>
</ul>
<p>That's the whole pipeline: mic, analyser, a time-domain pitch method, and some music math. You can try a finished version with a tuner-style needle and a vocal range mode on <a href="https://gizmetto.com/pitch-detector">Gizmetto's pitch detector</a>. It runs fully in the browser, so the audio never leaves your device.</p>
]]></content:encoded></item></channel></rss>