Skip to main content

audd-cpp

Open .md

Recognize music in audio clips, long broadcasts, and live streams from C++.

include(FetchContent)
FetchContent_Declare(audd
GIT_REPOSITORY https://github.com/AudDMusic/audd-cpp.git
GIT_TAG v1.5.7
)
FetchContent_MakeAvailable(audd)
target_link_libraries(your_app PRIVATE audd::audd)
#include <audd/audd.hpp>
#include <iostream>

int main() {
// 10 reqs/day; get a real token at https://dashboard.audd.io
audd::AudD client("test");

auto song = client.recognize("https://audd.tech/example.mp3");
if (song) std::cout << song->artist << " — " << song->title << "\n";
}

C++17 by default; pass -DAUDD_CXX20=ON to compile under C++20. RAII — no explicit free; audd::AudD owns internal state and releases it when it goes out of scope.

Authentication

Get your API token at dashboard.audd.io.

audd::AudD(token) resolves the API token in this order:

  1. The token constructor argument, when non-empty.
  2. The AUDD_API_TOKEN environment variable.
  3. Otherwise the client is still constructed; the first network call throws audd::AudDApiError with error_code = 901.
audd::AudD client("your-api-token");  // explicit
audd::AudD client_env(""); // reads AUDD_API_TOKEN

To fail fast at construction time, call audd::AudD::strict(...) — it throws audd::AudDMissingApiTokenError when neither the argument nor the env var resolves to a non-empty token, and returns a movable std::unique_ptr<AudD>:

auto client = audd::AudD::strict("");  // throws if AUDD_API_TOKEN is unset

The public "test" token is capped at 10 requests/day on the standard endpoint only — fine for hello-worlds, not enough for production.

For long-running services that pull tokens from a secret manager, rotate without rebuilding the client:

client.set_api_token("new-token");

The swap is atomic with respect to in-flight requests: in-flight calls finish under the previous token; subsequent calls use the new value. client.api_token() returns the in-effect token (empty when none set).

Recognize a clip

recognize takes a 5–25-second clip — URL, file path, or raw bytes — and returns std::optional<RecognitionResult>. std::nullopt means the clip processed but matched nothing; an exception means something went wrong.

auto song = client.recognize("https://audd.tech/example.mp3");
// or: client.recognize("/path/to/clip.mp3");

if (song) {
std::cout << song->artist << " — " << song->title
<< " @ " << song->timecode << "\n";
std::cout << "song page: " << song->song_link << "\n";
std::cout << "cover art: " << song->thumbnail_url() << "\n";
}

A bare std::string is auto-classified: an http:// / https:// prefix is sent as a URL, anything else is opened as a file path. To force the classification (or for in-memory bytes), use the typed Source forms:

client.recognize(audd::SourceUrl{"https://audd.tech/example.mp3"});
client.recognize(audd::SourceFilePath{"/clip.mp3"});

std::vector<std::uint8_t> bytes = read_buffer();
client.recognize(audd::SourceBytes{
.bytes = std::move(bytes),
.name = "clip.mp3",
.mime_type = "audio/mpeg",
});

For non-blocking call sites, every blocking method has an _async twin returning std::future<T>:

auto fut = client.recognize_async("https://audd.tech/example.mp3");
auto song = fut.get();

A RecognitionResult carries artist, title, album, release_date, label, timecode, song_link, isrc / upc (enterprise plans), plus helpers — thumbnail_url(), streaming_url(provider), streaming_urls(), preview_url(). Server fields not yet typed surface via result->extras (an std::map<std::string, nlohmann::json>). Full reference: github.com/AudDMusic/audd-cpp#what-you-get-back.

Custom-catalog matches populate audio_id instead of artist / title — use is_custom_match() / is_public_match() to discriminate.

Process a long audio file

For broadcasts, podcasts, and full sets longer than 25 seconds, use recognize_enterprise. AudD chunks the file server-side and returns every match.

audd::EnterpriseOptions opts;
opts.limit = 10; // ALWAYS set this in development

auto matches = client.recognize_enterprise("/path/to/broadcast.mp3", opts);
for (const auto& m : matches) {
std::cout << m.timecode << " "
<< m.artist << " — " << m.title
<< " (score=" << m.score << ")\n";
}

Other knobs on EnterpriseOptions: skip, every, skip_first_seconds, use_timecode, accurate_offsets, return_metadata. All numeric fields are std::optional<int> — leave them unset to use the server defaults. The endpoint accepts the same Source forms as recognize.

Enterprise calls bill per 12 seconds of audio processed

Always set opts.limit during development. The unbounded default can ingest many hours of audio on a single call.

Each EnterpriseMatch carries score, timecode, artist, title, album, release_date, label, song_link, isrc / upc (enterprise plans), start_offset, end_offset, plus thumbnail_url(), streaming_url(provider), streaming_urls(). The _async twin (recognize_enterprise_async) returns a std::future<std::vector<EnterpriseMatch>>.

Get streaming-service metadata

Set RecognizeOptions::return_metadata to populate provider sub-objects on the result. Without it, those std::optional blocks are empty.

audd::RecognizeOptions opts;
opts.return_metadata = {"apple_music", "spotify"};

auto song = client.recognize("https://audd.tech/example.mp3", opts);

if (song) {
if (song->apple_music)
std::cout << "Apple Music: " << song->apple_music->url << "\n";
if (song->spotify)
std::cout << "Spotify URI: " << song->spotify->uri << "\n";

// Or resolve any provider — direct URL when the metadata block is
// populated, else the lis.tn redirect when song_link is on lis.tn.
std::cout << song->streaming_url(audd::StreamingProvider::Spotify) << "\n";
std::cout << song->preview_url() << "\n";
}

Valid providers (the strings in return_metadata): apple_music, spotify, deezer, napster, musicbrainz. Each provider adds latency. The audd::StreamingProvider enum (used by streaming_url) covers Spotify, AppleMusic, Deezer, Napster, YouTube.

The market option (ISO 3166 region code) controls the Apple Music storefront:

audd::RecognizeOptions opts;
opts.return_metadata = {"apple_music"};
opts.market = "GB";

For each provider sub-object's full field shape, see the upstream API reference and the GitHub README.

Monitor a live audio stream

A stream in AudD is a long-running audio source — typically a radio URL, HLS, Icecast, or SHOUTcast endpoint — that AudD ingests continuously and recognizes against. Each stream is identified by a radio_id you pick.

There are two ways to consume recognition events:

Both modes need a callback URL configured on the account; otherwise events are never delivered. If your consumers will only longpoll — e.g., browser widgets, mobile apps, or scripts that can't expose a public URL — set the account's callback URL to https://audd.tech/empty/ in advance; it's a placeholder URL that discards POSTs. Consumers themselves don't need to configure anything.

Receive callbacks from a stream

Setup is three steps: configure the callback URL, register the stream, and parse incoming POSTs.

1. Configure the account callback URL

audd::SetCallbackUrlOptions copts;
copts.return_metadata = {"apple_music", "spotify"};

client.streams().set_callback_url(
"https://your-app.example.com/audd-callback", copts);

return_metadata is serialized into the URL as ?return=apple_music,spotify (the server reads it from the URL). If you've already baked ?return= into the URL string and also pass return_metadata, the SDK throws rather than silently overwriting.

2. Register the stream

int radio_id = 1;             // any integer you choose — your handle for this stream

audd::AddStreamRequest req;
req.url = "https://radio.example.com/stream.mp3";
req.radio_id = radio_id;
// req.callbacks = "before"; // fire callbacks at song start (default: end)

client.streams().add(req);

url accepts direct stream URLs (DASH, Icecast, HLS, m3u/m3u8) and shortcuts: twitch:<channel>, youtube:<video_id>, youtube-ch:<channel_id>.

Other stream-management methods:

int radio_id = 1; // your stream's handle from when you called streams().add(...)

client.streams().set_url(radio_id, "https://radio.example.com/new.mp3");
client.streams().del(radio_id);

for (const auto& s : client.streams().list()) {
std::cout << s.radio_id << " " << s.url
<< " running=" << s.stream_running << "\n";
}

3. Parse incoming POST bodies

audd::handle_callback(body) and audd::parse_callback(body) are transport-agnostic: feed them the raw POST body from any HTTP framework. Both return a CallbackEvent — a std::variant<StreamCallbackMatch, StreamCallbackNotification> — and throw audd::AudDSerializationError on malformed input.

cpp-httplib — canonical recipe

cpp-httplib ships vendored under vendor/cpp-httplib/ — link the headers in (no extra dependency) and write the receiver in a few lines:

#include <httplib.h>
#include <audd/audd.hpp>
#include <iostream>

int main() {
httplib::Server server;

server.Post("/audd-callback",
[](const httplib::Request& req, httplib::Response& res) {
try {
auto ev = audd::handle_callback(req.body);
std::visit([&](auto&& v) {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, audd::StreamCallbackMatch>) {
std::cout << "matched on radio_id=" << v.radio_id
<< ": " << v.song.artist
<< " — " << v.song.title << "\n";
} else if constexpr (std::is_same_v<T,
audd::StreamCallbackNotification>) {
std::cout << "notification #" << v.notification_code
<< ": " << v.notification_message << "\n";
}
}, ev);
res.status = 200;
} catch (const audd::AudDError& e) {
res.status = 400;
res.set_content(e.what(), "text/plain");
}
});

server.listen("0.0.0.0", 8080);
}

If you prefer not to use std::visit, the is_match / is_notification / match_or_null / notification_or_null helpers work just as well:

auto ev = audd::handle_callback(req.body);
if (auto* m = audd::match_or_null(ev)) {
std::cout << m->song.artist << " — " << m->song.title << "\n";
} else if (auto* n = audd::notification_or_null(ev)) {
std::cout << n->notification_message << "\n";
}

A StreamCallbackMatch carries radio_id, timestamp, play_length, song (the top match), alternatives (rare extra candidates — may have different artist/title from the top match), and raw_response. A StreamCallbackSong mirrors a recognition's metadata. A StreamCallbackNotification carries radio_id, stream_running, notification_code, notification_message, and time.

Other servers (Crow, Drogon, Pistache, Boost.Beast) — see the GitHub README.

Poll for stream events (longpoll)

For consumers that can't expose a public callback URL — desktop apps, scripts behind NAT, embedded clients — call client.streams().longpoll(radio_id) instead. The SDK opens the subscription, drives HTTP I/O on a background thread, and surfaces matches, notifications, and a single terminal error to your code.

Longpoll requires a configured callback URL

Unless a callback URL is configured for the account, longpoll requests just return keepalives — no events ever fire. The SDK preflights this on your first streams().longpoll(...) call and throws audd::AudDApiError (category InvalidRequest) with guidance if it's missing; set opts.skip_callback_check = true once you've configured one.

If you only want longpoll and have no real receiver, set https://audd.tech/empty/ — it's a placeholder URL that discards incoming POSTs:

client.streams().set_callback_url("https://audd.tech/empty/");

Three consumption patterns share the same LongpollPoll object. Pick whichever fits your event loop.

Callback-stylerun blocks until a terminal error fires:

int radio_id = 1; // any integer you choose — your handle for this stream

auto poll = client.streams().longpoll(radio_id);
poll.run(
[](audd::StreamCallbackMatch m) {
std::cout << m.song.artist << " — " << m.song.title << "\n";
},
[](audd::StreamCallbackNotification n) {
std::cout << "notification: " << n.notification_message << "\n";
},
[](std::exception_ptr ep) {
try { std::rethrow_exception(ep); }
catch (const std::exception& e) {
std::cerr << "terminal: " << e.what() << "\n";
}
});

Blocking pull — call next_match() / next_notification() / next_error() from your own loop. Each returns std::optional<T>; std::nullopt means the stream has terminated.

auto poll = client.streams().longpoll(radio_id);
while (auto m = poll.next_match()) {
std::cout << m->song.artist << " — " << m->song.title << "\n";
}

Future-basednext_match_async() / next_notification_async() / next_error_async() each return a std::future<std::optional<T>> that resolves on the worker thread.

LongpollOptions exposes since_time (Unix timestamp to resume from; 0 = "from now"), timeout_seconds (server-side wait; default 50), and skip_callback_check (bypass the preflight described above).

LongpollPoll is move-only. Letting it go out of scope cleanly joins the worker thread; poll.close() is idempotent and safe from any thread.

Tokenless consumers

When the consumer must not hold the api_token — a native desktop app, mobile build, or embedded device shipped to end users — keep the token on a server you control and ship the consumer an opaque per-stream identifier instead.

On the server, with the api_token in hand:

std::string subscription =
audd::derive_longpoll_category("your-api-token", radio_id);
// send `subscription` to the consumer over your own channel

On the consumer, with no api_token:

audd::AudD client("");  // no token needed for this overload
auto poll = client.streams().longpoll(subscription);
while (auto m = poll.next_match()) {
std::cout << m->song.artist << " — " << m->song.title << "\n";
}

The longpoll(std::string) overload accepts the identifier directly and reuses the same three consumption patterns above.

Add a song to your custom catalog

The custom-catalog endpoint adds songs to a private fingerprint database for your account. After upload, future recognize() calls on the same account can match against your tracks. It is not how you submit audio for music recognition — for that, use recognize() (or recognize_enterprise()).

danger

The custom-catalog endpoint requires special access. Contact api@audd.io to enable it. Calls without access throw audd::AudDCustomCatalogAccessError.

int audio_id = 1; // any integer you choose — your reference to the song

client.custom_catalog().add(
audio_id, audd::SourceUrl{"https://my.cdn/song.mp3"});

// Or from in-memory bytes:
audd::SourceBytes payload{ /* ... */ };
client.custom_catalog().add(audio_id, payload);

The SDK exposes only add — there is no public list or delete. Track audio_id ↔ song mappings yourself. Re-using an audio_id re-fingerprints that slot.

Handle errors

Every error raised by the SDK derives from audd::AudDError. Server- reported errors are audd::AudDApiError and carry error_code, message, http_status, request_id, request_method, branded_message, requested_params, and raw_response. Network failures throw audd::AudDConnectionError; malformed JSON throws audd::AudDSerializationError; missing token under strict() throws audd::AudDMissingApiTokenError; custom-catalog access is denied with audd::AudDCustomCatalogAccessError.

Idiomatic error handling

Discriminate either with dynamic_cast-friendly catch arms or with AudDError::category(). The most common shape is per-type catches for the failure modes you want to handle and a catch-all for the rest:

#include <audd/audd.hpp>
#include <iostream>

try {
auto song = client.recognize("/path/to/clip.mp3");
// ... use song
} catch (const audd::AudDMissingApiTokenError& e) {
std::cerr << "no token: " << e.what() << "\n";
return 1;
} catch (const audd::AudDCustomCatalogAccessError& e) {
std::cerr << "custom catalog access denied: " << e.what() << "\n";
} catch (const audd::AudDApiError& e) {
switch (e.category()) {
case audd::ErrorCategory::Authentication:
std::cerr << "check your token: [#" << e.error_code
<< "] " << e.message << "\n";
return 1;
case audd::ErrorCategory::Quota:
case audd::ErrorCategory::RateLimit:
std::cerr << "throttled: " << e.message << "\n";
break;
case audd::ErrorCategory::InvalidAudio:
std::cerr << "audio rejected: " << e.message << "\n";
break;
default:
std::cerr << "AudD #" << e.error_code << ": " << e.message
<< " (request_id=" << e.request_id << ")\n";
}
} catch (const audd::AudDConnectionError& e) {
std::cerr << "network: " << e.what() << "\n";
} catch (const audd::AudDSerializationError& e) {
std::cerr << "bad response body: " << e.what() << "\n";
}

For some codes — 19 (request_blocked, abuse, test_request_scope), 31337 (ip_ban), 903 (token_disabled) — the server populates branded human-readable text alongside the error. The SDK extracts that onto AudDApiError::branded_message rather than surfacing it as a fake recognition match.

When the server returns code 51 (deprecated parameter) with a usable result, the SDK invokes ClientConfig::on_deprecation (default: write to std::cerr) and returns the result as if the call had succeeded. Code 51 with no result throws AudDApiError in the InvalidRequest category.

Retry behavior

Each endpoint is classified into one of three retry classes:

  • Read (streams().list, streams().get_callback_url, longpoll HTTP) — retries on any AudDConnectionError, plus HTTP 408 / 429 / 5xx.
  • Recognition (recognize, recognize_enterprise, advanced().raw_request) — retries only on pre-upload network errors (DNS, connect, TLS) to avoid double-billing for in-progress uploads, plus 5xx.
  • Mutating (streams().add, streams().set_url, streams().del, streams().set_callback_url, custom_catalog().add) — retries only on pre-upload errors; 5xx is surfaced because the side effect may have already happened.

Defaults: max_attempts = 3, backoff_factor = 500ms (geometric, doubles each attempt). Override via ClientConfig:

audd::ClientConfig cfg;
cfg.max_attempts = 5;
cfg.backoff_factor = std::chrono::seconds(1);

audd::AudD client("your-api-token", cfg);

Set max_attempts = 1 to disable retries.

Configuration

Every knob lives on audd::ClientConfig, passed to the constructor:

audd::ClientConfig cfg;
// ... set fields ...
audd::AudD client("your-api-token", cfg);

Timeouts

Two timeouts live on ClientConfig: a short one for the standard endpoint (and stream / catalog / advanced calls), a long one for enterprise.

audd::ClientConfig cfg;
cfg.standard_timeout = std::chrono::seconds(30); // default 90 s
cfg.enterprise_timeout = std::chrono::hours(2); // default 1 h

The 1-hour default for enterprise is intentional: chunked server-side fingerprinting of multi-hour files takes that long.

Async and concurrency

A single audd::AudD instance is safe to share across threads — the internal mutex guards token rotation and sub-client lazy construction; the underlying libcurl handle is per-request.

AudD is non-copyable and non-movable (it owns the mutex). Hold it as an automatic local, in a std::unique_ptr (via AudD::strict(...)), or in a std::shared_ptr. Pass references where you'd otherwise pass values.

Every blocking method has an _async twin returning std::future<T>recognize_async, recognize_enterprise_async, streams().add_async, streams().list_async, custom_catalog().add_async, advanced().raw_request_async, etc. They run on a default thread pool; the future is satisfied when the call completes or throws.

Observability

For per-request observability, register an on_event hook:

audd::ClientConfig cfg;
cfg.on_event = [](const audd::AudDEvent& e) {
if (e.kind == audd::AudDEvent::Kind::Response) {
std::cerr << e.method << " -> " << e.http_status
<< " in " << e.elapsed.count() << "ms\n";
}
};

audd::AudD client("your-api-token", cfg);

AudDEvent is plain data: kind (Request / Response / Exception), method, url, request_id, http_status, elapsed, error_code. Events never carry the api_token or request body bytes. Hook exceptions are swallowed so observability cannot break the request path.

Deprecation hook

Server code 51 ("deprecated parameter") with a usable result triggers on_deprecation instead of throwing:

audd::ClientConfig cfg;
cfg.on_deprecation = [](const std::string& msg) {
my_logger.warn("audd deprecation", msg);
};

Default writes to std::cerr.

Token rotation

Covered under Authenticationclient.set_api_token("new-token") swaps atomically without aborting in-flight requests.

Retries

Covered under Retry behavior. max_attempts and backoff_factor on ClientConfig.

Calling undocumented endpoints

For AudD endpoints not yet wrapped by typed methods on this SDK, hit them by name through client.advanced().raw_request(method, params). It returns the parsed JSON body as nlohmann::json, runs through the same retry policy, and surfaces transport / parsing failures through the usual exception channel. This is the supported path for anything beta or one-off — typed wrappers ship as features stabilize.

auto body = client.advanced().raw_request(
"getLinks", {{"audd_id", "12345"}});
// body is nlohmann::json — parse with whatever you like.

Build options

CMake cache variables — AUDD_CXX20 (default OFF) toggles C++20; AUDD_BUILD_SHARED / AUDD_BUILD_STATIC (both default ON) pick the output library kind; AUDD_BUILD_EXAMPLES and AUDD_BUILD_TESTS (default OFF) build the runnable examples and the doctest suite; AUDD_INSTALL (default ON) generates install(...) rules so consumers can find_package(audd CONFIG REQUIRED). External requirement: libcurl (development headers). nlohmann/json and cpp-httplib ship vendored.


dashboard.audd.io · HTTP API reference · Other SDKs