audd-java
Recognize music in audio clips, long broadcasts, and live streams from Java.
Maven:
<dependency>
<groupId>io.audd</groupId>
<artifactId>audd</artifactId>
<version>1.5.7</version>
</dependency>
Gradle (Kotlin DSL):
implementation("io.audd:audd:1.5.7")
import io.audd.AudD;
import io.audd.models.RecognitionResult;
// Get a token at https://dashboard.audd.io — "test" is capped at 10 reqs/day.
try (AudD audd = new AudD("test")) {
RecognitionResult song = audd.recognize("https://audd.tech/example.mp3");
System.out.println(song.artist() + " — " + song.title());
}
The SDK ships two clients: AudD (sync, used above) and AsyncAudD (returns CompletableFuture from every method that hits the network). Both share the same AudD.Builder and configuration knobs — see Configuration.
Authentication
Get your API token at dashboard.audd.io.
Token resolution applied by both AudD and AsyncAudD:
- Explicit constructor / builder argument.
AUDD_API_TOKENenvironment variable.- Otherwise:
IllegalArgumentExceptionat build time, with a message pointing at dashboard.audd.io.
AudD a = new AudD("your-api-token"); // explicit
AudD b = AudD.builder().apiToken("your-api-token").build(); // builder
AudD c = AudD.fromEnvironment(); // env only
The public "test" token is capped at 10 requests/day for standard recognition only — no enterprise, no streams.
Token rotation
Long-running services that hot-rotate credentials can swap the token atomically. In-flight requests keep their original token; subsequent ones use the new one.
audd.setApiToken("new-token");
String current = audd.apiToken();
Recognize a clip
recognize(source) accepts a URL string, a file path string, a Path, a File, a byte[], or an InputStream:
RecognitionResult song = audd.recognize("https://audd.tech/example.mp3");
RecognitionResult song = audd.recognize(Path.of("clip.mp3"));
byte[] bytes = Files.readAllBytes(Path.of("clip.mp3"));
RecognitionResult song = audd.recognize(bytes);
try (InputStream in = micCaptureStream()) {
RecognitionResult song = audd.recognize(in);
}
recognize returns null when no music is detected in the clip (a successful response with result: null). When the server can't fingerprint the audio at all (file too small, non-audio bytes), it raises AudDInvalidAudioError instead — see Handle errors.
The RecognitionResult exposes getters for artist(), title(), album(), releaseDate(), label(), timecode(), songLink(), isrc() / upc() (enterprise tokens only), the per-provider metadata blocks, plus helpers thumbnailUrl() and streamingUrl(provider). See the GitHub README for the full field list.
RecognitionResult song = audd.recognize(source);
if (song != null) {
System.out.println(song.artist() + " — " + song.title());
System.out.println("album: " + song.album());
System.out.println("released: " + song.releaseDate());
System.out.println("label: " + song.label());
System.out.println("timecode: " + song.timecode());
System.out.println("link: " + song.songLink());
String thumb = song.thumbnailUrl(); // null for non-lis.tn links
if (thumb != null) System.out.println("art: " + thumb);
}
extras() (inherited from ForwardCompatible) gives you any field the server returned that the typed model doesn't expose yet — useful for new or undocumented metadata. rawResponse() returns the full Jackson JsonNode.
Process a long audio file
The enterprise endpoint accepts files longer than 25 seconds and bills per 12 seconds of audio processed. Use it whenever standard recognize would fail with code 400 ("audio over 10MB / 25s"):
import io.audd.EnterpriseOptions;
import io.audd.models.EnterpriseMatch;
List<EnterpriseMatch> matches = audd.recognizeEnterprise(
Path.of("broadcast.mp3"),
EnterpriseOptions.builder()
.limit(10) // cap response size
.every(2) // sample every Nth chunk
.skipFirstSeconds(60) // skip the intro
.build());
for (EnterpriseMatch m : matches) {
System.out.printf("%s - %s @ %s%n", m.artist(), m.title(), m.timecode());
}
Always set .limit(...) during development. Without a limit, a single hour-long file can produce hundreds of metered matches. Set a low cap (5-10) until you've sized your usage.
EnterpriseOptions also exposes skip, useTimecode, accurateOffsets, and timeoutMs. The async equivalent on AsyncAudD returns CompletableFuture<List<EnterpriseMatch>>.
Get streaming-service metadata
Pass returnMetadata to populate the per-provider blocks (apple_music, spotify, deezer, napster, musicbrainz). They are null unless requested:
import io.audd.RecognizeOptions;
RecognitionResult song = audd.recognize(source,
RecognizeOptions.builder()
.returnMetadata("apple_music", "spotify")
.build());
if (song.appleMusic() != null) {
System.out.println("apple: " + song.appleMusic().url);
}
if (song.spotify() != null) {
System.out.println("spotify uri: " + song.spotify().uri);
}
For a redirect URL that works regardless of which provider you requested, use streamingUrl(provider) — it returns the direct URL when the provider's metadata block is populated, otherwise falls back to the lis.tn ?<provider> redirect when songLink is a lis.tn URL:
import io.audd.models.StreamingProvider;
String spotify = song.streamingUrl(StreamingProvider.SPOTIFY);
String youtube = song.streamingUrl(StreamingProvider.YOUTUBE);
Map<StreamingProvider, String> all = song.streamingUrls();
The market option (ISO-3166-1 alpha-2) tweaks Apple Music storefront resolution: RecognizeOptions.builder().market("US"). Full provider ID list at docs.audd.io.
Monitor a live audio stream
An AudD stream is a long-running subscription where AudD continuously fingerprints a live audio source (Icecast, HLS, Twitch, YouTube channel, etc.) and notifies you when songs are recognized. Two consumption modes:
- Callbacks — AudD POSTs each event to a URL you control. Lowest-latency, requires a public HTTPS endpoint. See Receive callbacks from a stream.
- Longpoll — your client opens long-running GETs against
/longpoll/and AudD responds when events fire. Works behind NAT, in browsers, on mobile. See Poll for stream events (longpoll).
Both modes deliver the same CallbackEvent shape (a typed sum of match/notification). 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.
Stream subscriptions are paid; contact api@audd.io to enable them on your account.
Receive callbacks from a stream
Three steps: set the callback URL once, add a stream pointing at it, then parse incoming POST bodies in your web app.
import io.audd.streams.AddStreamRequest;
audd.streams().setCallbackUrl("https://example.com/audd-hook");
long radioId = 1L; // any integer you choose — your handle for this stream
audd.streams().add(new AddStreamRequest(
"https://stream.example.com/radio.mp3", // direct URL, or twitch:<channel>, youtube:<video_id>, youtube-ch:<channel_id>
radioId));
// Optional: callbacks at song *start* instead of end
audd.streams().add(new AddStreamRequest(streamUrl, radioId, "before"));
setCallbackUrl accepts a second argument that appends ?return=<metadata> to the URL so AudD includes streaming-service metadata in the callback payloads:
audd.streams().setCallbackUrl(
"https://example.com/audd-hook",
List.of("apple_music", "spotify"));
In your web app, parse incoming POST bodies with Streams.parseCallback. It accepts byte[], InputStream, or an already-parsed Jackson JsonNode and returns a CallbackEvent — exactly one of match() or notification() is present:
import io.audd.streams.Streams;
import io.audd.models.CallbackEvent;
CallbackEvent event = Streams.parseCallback(requestBody);
event.match().ifPresent(m -> {
System.out.printf("%d: %s — %s%n",
m.radioId(), m.song().artist(), m.song().title());
});
event.notification().ifPresent(n -> {
System.out.println("notif " + n.notificationCode() + ": " + n.notificationMessage());
});
Spring Boot
import io.audd.streams.Streams;
import io.audd.models.CallbackEvent;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/audd-hook")
public class AudDCallbackController {
@PostMapping
public ResponseEntity<Void> handle(@RequestBody byte[] body) {
CallbackEvent event = Streams.parseCallback(body);
event.match().ifPresent(m ->
metrics.record(m.radioId(), m.song().artist(), m.song().title()));
event.notification().ifPresent(n ->
log.warn("stream {} notification: {}", n.radioId(), n.notificationMessage()));
return ResponseEntity.ok().build();
}
}
@RequestBody byte[] consumes the raw bytes without intermediate deserialization — Streams.parseCallback(byte[]) does the JSON decoding. Other frameworks (Servlet API, Javalin, Micronaut) — see the GitHub README.
Manage streams
import io.audd.models.Stream;
long radioId = 1L; // your stream's handle from when you called add(...)
List<Stream> streams = audd.streams().list();
audd.streams().setUrl(radioId, "https://new-source.example.com/radio.mp3");
audd.streams().delete(radioId);
Poll for stream events (longpoll)
When you can't expose a public HTTPS endpoint to receive callbacks — desktop tools, scripts behind NAT, dev workstations — long-polling delivers the same match and notification events over a blocking GET that the SDK drives in a loop.
Pass the radio_id of a stream you've added, register three lambdas, then call run():
import io.audd.streams.LongpollPoll;
int radioId = 1; // the radio_id you used when calling streams.add(...)
try (LongpollPoll poll = audd.streams().longpoll(radioId)) {
poll.onMatch(m -> System.out.println(m.song().artist() + " — " + m.song().title()))
.onNotification(n -> System.out.println("notif: " + n.notificationMessage()))
.onError(err -> log.error("longpoll terminated", err));
poll.run(); // blocks until close() or terminal error
}
run() blocks the calling thread until close() is invoked or a terminal error fires. The error callback is single-shot — once it runs, the loop exits and no further match/notification callbacks are dispatched. close() is idempotent and safe to call from any thread, including from inside a callback. The "no events before timeout" envelope is absorbed transparently; the loop just re-polls.
LongpollOptions.builder().sinceTime(...).timeout(...).build() resumes from a server-supplied timestamp and tunes the long-poll wait (default 50 s, max 90 s).
Even when you only consume events via longpoll, the account must have a callback URL configured — otherwise the longpoll endpoint silently returns keepalives forever. The SDK preflights this on your first streams.longpoll(...) call and raises AudDInvalidRequestError with guidance if it's missing; pass LongpollOptions.builder().skipCallbackCheck(true).build() once you've set one.
If you only want longpoll and have no real receiver, set https://audd.tech/empty/ — a placeholder URL that discards incoming POSTs:
audd.streams().setCallbackUrl("https://audd.tech/empty/");
Async variant
AsyncAudD exposes the same surface, returning a CompletableFuture<LongpollPoll> so the preflight check is non-blocking. Drive the returned poll with runAsync(), which runs the dispatch loop on a daemon thread:
asyncAudd.streams().longpoll(radioId).thenAccept(poll -> {
poll.onMatch(m -> ...).onNotification(n -> ...).onError(err -> ...);
CompletableFuture<Void> done = poll.runAsync(); // completes on close() or error
// poll.close() from anywhere to stop the loop
});
Tokenless consumers
When the longpoll consumer is an Android app, a desktop client, or a browser widget that should never see your api_token, your server hands it an opaque per-stream identifier instead. The client subscribes with that identifier alone:
// Server (has the api_token):
String streamKey = audd.streams().deriveLongpollCategory(radioId);
// ship streamKey to the client over your own auth channel
// Client (no api_token; constructed with any non-empty placeholder):
try (LongpollPoll poll = client.streams().longpoll(streamKey)) {
poll.onMatch(m -> ...).onNotification(n -> ...).onError(err -> ...);
poll.run();
}
deriveLongpollCategory is also exposed as a static Streams.deriveLongpollCategory(apiToken, radioId) for callers that want the identifier without instantiating an AudD client.
Add a song to your custom catalog
Custom-catalog access lets AudD recognize your own tracks for your account only. It's gated — contact api@audd.io if you need it enabled on your token.
long audioId = 1L; // any integer you choose — your reference to the song
audd.customCatalog().add(audioId, Path.of("my-track.mp3"));
Recognition results from custom-catalog matches expose the integer audioId() you assigned. There is no public list/delete endpoint; track audioId ↔ song mappings on your side. Calling add with the same ID re-fingerprints that slot.
Without custom-catalog access on your token, add throws AudDCustomCatalogAccessError (a subclass of AudDSubscriptionError).
Handle errors
Idiomatic error handling
Every server-side status=error raises a typed exception (subclass of AudDApiError); all SDK exceptions extend AudDException (RuntimeException-based, so no throws clauses needed). Catch the leaves you care about:
import io.audd.errors.*;
try {
RecognitionResult song = audd.recognize(clip);
handle(song);
} catch (AudDAuthenticationError e) {
log.error("token problem (code {}): {}", e.errorCode(), e.serverMessage());
rotateToken();
} catch (AudDQuotaError e) {
log.warn("quota exceeded; backing off");
backOffUntilNextMonth();
} catch (AudDRateLimitError e) {
Thread.sleep(5_000);
} catch (AudDInvalidAudioError e) {
log.warn("not music or too long: code {} — {}", e.errorCode(), e.serverMessage());
} catch (AudDConnectionError e) {
log.warn("network: {}", e.getMessage(), e.getCause());
}
On Java 21+, pattern-match the closed hierarchy:
try {
audd.recognize(clip);
} catch (AudDException e) {
switch (e) {
case AudDQuotaError q -> backOffUntilNextMonth();
case AudDRateLimitError r -> sleepAndRetry();
case AudDInvalidAudioError ia -> log.warn("not music: {}", ia.serverMessage());
case AudDConnectionError ce -> log.warn("network: {}", ce.getMessage());
default -> throw e;
}
}
AudDApiError (and every subclass) exposes errorCode(), serverMessage(), httpStatus(), requestId() (quote in support tickets), requestedParams() (server's redacted echo, no token), requestMethod(), brandedMessage(), and rawResponse().
Retry behavior
The SDK ships three retry classes, applied per-method based on cost-awareness:
| Class | Methods | Retries on HTTP | Retries on exception |
|---|---|---|---|
READ | streams.list, streams.getCallbackUrl, longpoll loop | 408, 429, 5xx | Any IOException (idempotent reads) |
RECOGNITION | recognize, recognizeEnterprise, advanced.findLyrics, advanced.rawRequest | 5xx | Pre-upload connection failures only — ConnectException, UnknownHostException, SSLHandshakeException, connect-phase SocketTimeoutException. Read-timeout-after-upload is not retried (the server may have done metered work). |
MUTATING | streams.add / setUrl / delete / setCallbackUrl, customCatalog.add | None | Pre-upload connection failures only |
Defaults: maxRetries = 3 (1 initial + 2 retries), backoffFactorMs = 500. Backoff is exponential with full jitter, capped at 30 seconds. AudDApiError and subclasses are never retried — they're terminal client-visible failures.
AudD audd = AudD.builder()
.apiToken("your-api-token")
.maxRetries(5)
.backoffFactorMs(1000)
.build();
Configuration
Async client
AsyncAudD mirrors AudD — every method that hits the network returns a CompletableFuture. Same configuration knobs, same try-with-resources lifecycle.
import io.audd.AsyncAudD;
try (AsyncAudD audd = AudD.builder().apiToken("your-api-token").buildAsync()) {
audd.recognize("https://audd.tech/example.mp3")
.thenAccept(song -> {
if (song != null) System.out.println(song.artist() + " — " + song.title());
})
.join();
}
cancel(true) on a future interrupts the local CompletableFuture chain. If the upload has already reached the server, the request might still be metered — cancel before upload completes to avoid the metered work.
Timeouts
| Knob | Default | Applies to |
|---|---|---|
| connect timeout | 30 s | All requests (OkHttp default). |
standardTimeoutSeconds | 60 s | recognize, streams.*, customCatalog.add, advanced.*. |
enterpriseTimeoutSeconds | 3600 s | recognizeEnterprise only. |
AudD audd = AudD.builder()
.apiToken("your-api-token")
.standardTimeoutSeconds(120)
.enterpriseTimeoutSeconds(7200)
.build();
When you inject a custom OkHttpClient (below), the SDK does not override its timeouts — your client's settings win.
Custom HTTP client
Inject a fully-configured OkHttpClient for proxies, custom TLS, interceptors, or a shared connection pool:
import okhttp3.OkHttpClient;
OkHttpClient http = new OkHttpClient.Builder()
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.corp", 8080)))
.connectTimeout(Duration.ofSeconds(10))
.readTimeout(Duration.ofSeconds(60))
.build();
AudD audd = AudD.builder()
.apiToken("your-api-token")
.httpClient(http)
.build();
The default User-Agent on every request is audd-java/<sdk-version> jvm/<java-version> (<os.name>).
Observability
Register a Consumer<AudDEvent> to receive lifecycle events for every API call (REQUEST → RESPONSE or EXCEPTION):
import io.audd.AudDEvent;
AudD audd = AudD.builder()
.apiToken("your-api-token")
.onEvent(event -> {
switch (event.kind()) {
case REQUEST -> tracer.startSpan(event.method(), event.url());
case RESPONSE -> tracer.endSpan(event.requestId(), event.httpStatus(), event.elapsedMs());
case EXCEPTION -> tracer.endError(event.method(), event.elapsedMs(), event.extras());
}
})
.build();
AudDEvent exposes kind(), method(), url(), requestId(), httpStatus(), elapsedMs(), errorCode(), and extras(). The hook is invoked synchronously; hook exceptions are swallowed at FINE log level so observability never breaks the request path. The hook never receives api_token or request body bytes.
The SDK also uses java.util.logging.Logger.getLogger("io.audd") for code-51 deprecation warnings (when no onDeprecation hook is registered) and for hook-exception traces. Bridge to SLF4J / Logback / Log4j 2 via the standard JUL bridge for your stack.
Concurrency
AudD and AsyncAudD are both thread-safe — share a single instance across threads. AsyncAudD futures complete on OkHttp's dispatcher thread pool by default; chain .thenApplyAsync(..., executor) to switch to your own pool. setApiToken is atomic (AtomicReference-backed), safe to call concurrently with in-flight requests.
Calling undocumented endpoints
For AudD endpoints not yet wrapped by typed methods, audd.advanced().rawRequest(method, params) posts form-encoded params to /<method>/ and returns the raw Jackson JsonNode. The api_token is injected automatically; retries follow the recognition policy.
import com.fasterxml.jackson.databind.JsonNode;
JsonNode body = audd.advanced().rawRequest("someUndocumentedMethod",
Map.of("foo", "bar"));
status=error responses are still parsed into typed exceptions, so error handling matches the rest of the SDK.