Article

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.

view .md auddreal-time recognitionweb audiomediarecorder

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:

  1. The browser records a few seconds of audio and POSTs the blob to your own server.
  2. Your backend forwards the bytes to AudD with the SDK and returns a compact JSON answer.
  3. 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 — recognize returns null. 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 test token 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_TOKEN in client code; the backend pattern above is what keeps it private.
  • Browser support — feature-detect navigator.mediaDevices.getUserMedia and MediaRecorder, 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.