audd-cpp
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:
- The
tokenconstructor argument, when non-empty. - The
AUDD_API_TOKENenvironment variable. - Otherwise the client is still constructed; the first network call throws
audd::AudDApiErrorwitherror_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.
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:
- Callbacks — AudD POSTs each match (and each lifecycle notification) to a URL you control. See Receive callbacks from a stream.
- Longpoll — your code polls AudD's
/longpoll/endpoint and receives matches synchronously over HTTP. See Poll for stream events (longpoll).
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.
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-style — run 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-based — next_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()).
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 anyAudDConnectionError, 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 Authentication —
client.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.