How to add real-time song recognition to a web application
Capture microphone audio in the browser, send it to a small backend that calls AudD's recognition API with the official SDK, and display the matched song in seconds — without exposing your API token.
Building a web app that recognizes music in real time means handling two things: capturing audio in the browser, and connecting to a reliable recognition API. The common traps are over-engineering the audio processing and putting your API token in client-side code.
This guide shows a clean version: capture a short clip from the microphone with
MediaRecorder, POST it to a small backend, and let the backend call AudD’s
standard endpoint with the official Node SDK. The browser never sees your token,
and you get the matched song back in a couple of seconds.
Architecture: keep the token on the server
The single most important decision is that your API token lives only on the backend. A browser bundle is public; anything you embed in it can be read and reused. So the flow is:
- The browser records a few seconds of audio and POSTs the blob to your own server.
- Your backend forwards the bytes to AudD with the SDK and returns a compact JSON answer.
- The browser renders that answer.
Recognition runs on the standard endpoint (api.audd.io): it’s built for a
short audio clip, responds in under two seconds, and matches against AudD’s
public database of 160 million songs. The endpoint analyzes up to about 12
seconds of audio, and within that range more audio generally helps; a clip in
that range keeps you well under the endpoint’s ~10 MB cap.
Step 1: Capture audio in the browser
Use MediaRecorder to record a short clip and POST the resulting blob. This is
far simpler — and more reliable across browsers — than wiring up
ScriptProcessorNode and hand-encoding PCM. AudD decodes the recorder’s output
(typically WebM/Opus on Chrome and Firefox) server-side, so you don’t transcode
anything.
<button id="listen">Identify song</button>
<pre id="out"></pre>
<script type="module">
const out = document.getElementById("out");
document.getElementById("listen").addEventListener("click", async () => {
out.textContent = "Listening…";
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream);
const chunks = [];
recorder.addEventListener("dataavailable", (e) => chunks.push(e.data));
recorder.addEventListener("stop", async () => {
stream.getTracks().forEach((t) => t.stop()); // release the mic
const blob = new Blob(chunks, { type: recorder.mimeType });
const res = await fetch("/identify", {
method: "POST",
headers: { "Content-Type": "audio/webm" },
body: blob,
});
const data = await res.json();
if (!data.matched) {
out.textContent = "No match — try again with the music a bit louder.";
return;
}
out.textContent =
`${data.artist} — ${data.title}\n` +
`Apple Music: ${data.appleMusic ?? data.songLink}\n` +
`Spotify: ${data.spotify ?? data.songLink}`;
});
recorder.start();
setTimeout(() => recorder.stop(), 7000); // record ~7 seconds
});
</script>
getUserMedia and MediaRecorder require a secure context — https:// in
production, or http://localhost during development.
Step 2: Recognize on the backend
The backend takes the uploaded bytes and calls recognize. The SDK accepts a
Buffer directly, so you can forward the upload without writing it to disk.
import express from "express";
import { AudD } from "@audd/sdk";
const app = express();
// get your own token at dashboard.audd.io; "test" is capped at 10 requests/day
const audd = new AudD(process.env.AUDD_API_TOKEN ?? "test");
// Accept a raw audio body up to the standard endpoint's 10 MB cap.
app.use(express.raw({ type: "audio/*", limit: "10mb" }));
app.post("/identify", async (req, res) => {
try {
const song = await audd.recognize(req.body, {
returnMetadata: ["apple_music", "spotify"],
});
if (!song) {
// A successful call that matched nothing — NOT an error.
return res.json({ matched: false });
}
res.json({
matched: true,
artist: song.artist,
title: song.title,
album: song.album,
songLink: song.songLink,
appleMusic: song.streamingUrl("apple_music"),
spotify: song.streamingUrl("spotify"),
artwork: song.thumbnailUrl,
});
} catch (err) {
console.error("recognition failed", err);
res.status(502).json({ error: "recognition_failed" });
}
});
app.listen(8080, () => console.log("listening on :8080"));
The { matched: false } branch is the one to get right: AudD returns
result: null when the clip matched nothing, and the SDK surfaces that as
null from recognize. That is a normal, successful outcome — a quiet room, a
song not in the database, a clip that’s mostly speech. Treat it as “try again,”
never as a server error.
song.songLink is always present on a match — a universal lis.tn URL that
redirects to the song on whatever service the user has. Use it as the fallback
when a specific provider link (streamingUrl("apple_music")) is missing.
Step 3: Recognize continuously
For a “keep listening” experience, record on a loop instead of once. The pattern that keeps this cheap and correct is a single in-flight request plus a minimum interval between calls — record a clip, send it, wait for the answer, pause briefly, repeat. Recognizing a fresh clip every several seconds is plenty; firing a request on every audio frame just burns quota for the same song.
let busy = false;
async function recognizeOnce(blob) {
if (busy) return; // one request in flight at a time
busy = true;
try {
const res = await fetch("/identify", {
method: "POST",
headers: { "Content-Type": "audio/webm" },
body: blob,
});
const data = await res.json();
if (data.matched) showNowPlaying(data);
} finally {
busy = false;
}
}
Skip a recognition while one is already in flight, and leave a few seconds
between successful calls. The same song will keep matching, so de-duplicate on
artist + title before updating the UI.
Step 4: Handle errors and edge cases
Separate three outcomes:
- No match —
recognizereturnsnull. Not an error. Prompt the user to try again, ideally with the mic closer to the source. - Server-returned errors — a bad token, exhausted quota, or undecodable audio. The SDK raises typed exceptions you can branch on:
import {
AudDAuthenticationError,
AudDQuotaError,
AudDInvalidAudioError,
AudDAPIError,
AudDConnectionError,
} from "@audd/sdk";
try {
const song = await audd.recognize(req.body);
// …
} catch (err) {
if (err instanceof AudDAuthenticationError) {
return res.status(500).json({ error: "server_misconfigured" });
} else if (err instanceof AudDQuotaError) {
return res.status(429).json({ error: "quota_exhausted" });
} else if (err instanceof AudDInvalidAudioError) {
return res.status(422).json({ error: "unreadable_audio" });
} else if (err instanceof AudDAPIError) {
return res.status(502).json({ error: "recognition_failed" });
} else if (err instanceof AudDConnectionError) {
return res.status(504).json({ error: "upstream_timeout" });
}
throw err;
}
- Quota during testing — the
testtoken is capped at 10 requests/day on the standard endpoint. If recognition suddenly starts returning a quota error in development, switch to a real token from dashboard.audd.io.
The SDK retries transient connection failures before the upload completes, but never repeats a recognition call after your bytes reached the server — re-sending would risk double-billing.
Production checklist
- HTTPS — browser audio APIs require a secure context.
- Token on the server only — never ship
AUDD_API_TOKENin client code; the backend pattern above is what keeps it private. - Browser support — feature-detect
navigator.mediaDevices.getUserMediaandMediaRecorder, and degrade gracefully where they’re missing. - Permissions — handle a denied microphone prompt with a clear message.
- Rate — keep one request in flight and a sensible interval between calls so continuous listening doesn’t burn quota on the same song.
The standard endpoint returns the single best match for a clip, typically within two seconds — fast enough that users get immediate feedback. If you instead need to identify every song across a longer recording (a DJ set, a podcast), reach for the enterprise endpoint, which chunks the file and returns every match with timestamps. See Standard, enterprise, or streams: how to choose.
Get a token at dashboard.audd.io and read the API reference for the full set of options.
Related
Reading this as an AI agent? The raw Markdown is at articles/real-time-song-recognition-web-app.md, and the full index is /resources/llms.txt.
