Official Swift package for music recognition API: identify music from a short audio clip, a long audio file, or a live stream.
The API itself is so simple that it can easily be used even without an SDK: docs.audd.io.
Platforms: iOS 15+, macOS 12+, watchOS 8+, tvOS 15+, visionOS 1+, Linux. Swift 5.9+.
Package.swift:
.package(url: "https://github.com/AudDMusic/audd-swift", from: "1.5.9"),Get your API token at dashboard.audd.io.
Add "AudD" to your target's dependencies, then recognize from a URL:
import AudD
let audd = try AudD(apiToken: "your-api-token")
if let result = try await audd.recognize("https://audd.tech/example.mp3") {
print("\(result.artist ?? "?") — \(result.title ?? "?")")
}Recognize from a local file:
import AudD
import Foundation
let audd = try AudD(apiToken: "your-api-token")
let file = URL(fileURLWithPath: "/path/to/clip.mp3")
if let result = try await audd.recognize(.file(file)) {
print("\(result.artist ?? "?") — \(result.title ?? "?")")
}Source accepts .url(URL), .file(URL), .data(Data), or .stream(InputStream, name:).
recognize returns RecognitionResult? — nil when the clip isn't recognized
(distinct from a thrown error).
For longer audio files (full-length songs, short-form videos, podcasts, broadcasts, DJ sets), use
recognizeEnterprise(_:limit:...) — it returns [EnterpriseMatch], one per
song detected across the file.
Pass the token as a literal string:
let audd = try AudD(apiToken: "your-token")Or omit it (or pass nil / "") to read AUDD_API_TOKEN from the
environment. AudD.fromEnvironment() is a convenience factory for that path:
let audd = try AudD.fromEnvironment()For long-running services that rotate tokens (e.g., from a secret manager),
call await audd.setApiToken(newToken). In-flight requests finish on the
previous token; subsequent requests use the new one.
By default recognize returns the core tags plus AudD's universal song link — no metadata-block opt-in needed:
import AudD
let audd = try AudD(apiToken: "your-api-token")
guard let result = try await audd.recognize("https://audd.tech/example.mp3") else {
print("no match")
return
}
// Core tags
print(result.artist ?? "", "—", result.title ?? "")
print(result.album ?? "", result.releaseDate ?? "", result.label ?? "")
// AudD's universal song page — links into every provider
print(result.songLink ?? "")
// Helpers — driven off songLink, work without any `returnMetadata` opt-in
print(result.thumbnailURL ?? "") // cover-art URL, or nil
print(result.streamingUrl(.spotify) ?? "") // direct or lis.tn redirect, or nil
print(result.streamingUrls()) // [.spotify: "...", .deezer: "...", ...]If you need provider-specific metadata blocks, opt in per call. Request only what you need — each provider you ask for adds latency:
guard let result = try await audd.recognize(
"https://audd.tech/example.mp3",
returnMetadata: ["apple_music", "spotify"]
) else { return }
print(result.appleMusic?.url ?? "") // direct Apple Music link
print(result.spotify?.uri ?? "") // spotify:track:...
print(result.previewUrl() ?? "") // first preview across requested providers, nil if noneValid returnMetadata values: apple_music, spotify, deezer, napster,
musicbrainz. The corresponding properties (appleMusic, spotify, …) are
nil when not requested.
EnterpriseMatch (returned by recognizeEnterprise) carries the same core
tags plus score, isrc, upc, and startSeconds / endSeconds — where the
song plays in your file, in seconds. These are precise because
recognizeEnterprise requests accurate offsets by default (pass
accurateOffsets: false to opt out). The raw fragment-relative startOffset /
endOffset (milliseconds within the matched fragment) sit behind them. Access
to isrc, upc, and score requires a Startup plan or higher — contact
us for enterprise features.
The typed models cover what AudD's typed surface covers. To read additional
fields the server returns, go through extras (per-model) or rawResponse
(full payload):
// Top-level extras — anything outside the typed surface
let genre = result.extras["genre"]?.value as? String
// Nested extras inside a typed metadata block
let artwork = result.appleMusic?.extras["artwork"]?.value
// Or the full untyped payload
let raw = result.rawResponse.valueThis is the supported API for fields outside the typed surface. Beta features and per-account custom fields show up here.
For sending arbitrary form fields the typed parameters don't cover, pass an
extraParameters map:
let result = try await audd.recognize(
.file(URL(fileURLWithPath: "/tmp/snippet.wav")),
returnMetadata: ["apple_music"],
extraParameters: ["my_custom_flag": "1"]
)Typed parameters win on collision.
Every server-side error becomes a typed AudDError case. Pattern-match on the
case (and on detail.kind for API errors) to handle whole families:
import AudD
do {
_ = try await audd.recognize("https://example.mp3")
} catch let AudDError.api(detail) where detail.kind == .authentication {
// 900 / 901 / 903
print("check your token: [#\(detail.errorCode)] \(detail.message)")
} catch let AudDError.api(detail) where detail.kind == .invalidAudio {
// 300 / 400 / 500
print("audio rejected: \(detail.message)")
} catch let AudDError.api(detail) {
// Catch-all for anything the server reported
print("AudD #\(detail.errorCode): \(detail.message) (request_id=\(detail.requestID ?? "-"))")
} catch AudDError.connection(let message, _) {
// network / TLS / timeout — no HTTP response received
print("connection: \(message)")
} catch AudDError.serverError(let status, let message, _, _) {
// non-2xx with non-JSON body (gateway HTML, timeout text, etc.)
print("server error HTTP \(status): \(message)")
} catch AudDError.serializationError(let message, _) {
// 2xx with malformed JSON
print("decode: \(message)")
}The AudDErrorKind enum maps the AudD numeric error catalog to families:
.authentication, .quota, .subscription, .customCatalogAccess,
.invalidRequest, .invalidAudio, .rateLimit, .streamLimit,
.notReleased, .blocked, .needsUpdate, .server. Every
AudDAPIErrorDetail carries errorCode, message, httpStatus,
requestID, requestedParams, requestMethod, brandedMessage, and
rawResponse — enough to log a full incident or open a support ticket.
import AudD
import Foundation
let session = URLSession(configuration: .ephemeral)
let audd = try AudD(
apiToken: "your-token",
maxRetries: 3, // per-call retry budget
backoffFactor: 0.5, // initial backoff seconds (jittered)
urlSession: session, // inject your own URLSession
enterpriseURLSession: session, // separate session for the enterprise endpoint (optional)
onEvent: { event in // tracing / metrics hook
print(event.kind, event.method, event.requestId ?? "-")
}
)Custom URLSession. Inject your own URLSession to add a custom
configuration, proxy, certificate pinning, or shared connection pool. The SDK
adds its User-Agent header. The enterprise endpoint can optionally use a
separate session — useful when you want a longer resource timeout for
multi-hour file processing without affecting your standard-endpoint timeouts.
Retries. Calls are classified by cost and retried accordingly:
| Class | Endpoints | Retried on |
|---|---|---|
| Recognition | recognize, recognizeEnterprise, advanced.* |
network errors and 5xx before the upload reaches the server |
| Read | streams.list, streams.getCallbackURL, longpoll |
network errors and 5xx |
| Mutating | streams.setCallbackURL, streams.add, streams.delete, customCatalog.add |
network errors and 5xx (idempotent on the server) |
Recognition will not double-bill your account: once the server has accepted bytes, a 5xx after that is surfaced rather than retried.
Inspection. Pass onEvent: to receive an AudDEvent for every request /
response / exception — useful for metrics, tracing, or dropping a requestID
into your logs. Events never carry the api_token or request bytes; exceptions
raised from the hook are swallowed so observability can't break the request
path.
Concurrency. The client is an actor — share one instance across tasks
and structured-concurrency contexts; every method is async and serializes
through the actor. Token rotation via setApiToken(_:) takes effect on the
next outbound request; in-flight requests finish on the previous token. All
public model types and namespaces are Sendable.
Lifecycle. Call await audd.close() to release the underlying
URLSessions eagerly. deinit also closes; explicit close is only needed
when you want determinism (CLI tools, tests).
Real-time recognition off radio streams, broadcast feeds, and any other long-running URL. Configure once, then either receive callbacks on your server or poll for events.
try await audd.streams.setCallbackURL("https://your.server/audd-callback")
try await audd.streams.add(url: "https://your.stream.url/listen.m3u8", radioID: 42)
for stream in try await audd.streams.list() {
print(stream.radioID, stream.url, stream.streamRunning)
}The callback POSTs raw bytes; parse them into a typed match or notification:
// In your webhook handler, given the raw POST body as `Data`:
switch try audd.streams.parseCallback(bodyData) {
case .match(let match):
print("\(match.song.artist) — \(match.song.title) score=\(match.song.score)")
for alt in match.alternatives {
// alternatives may have a different artist/title — variant catalog releases
print(" alt: \(alt.artist) — \(alt.title)")
}
case .notification(let n):
print("notification:", n.notificationMessage)
}parseCallback(_:) is a pure function — no network. Use it inside whichever
HTTP framework receives your AudD callbacks (Vapor, Hummingbird, hand-rolled
URLSession listener, queue replay tool, etc.).
Vapor:
import Vapor
import AudD
func routes(_ app: Application) throws {
let audd = AudD(apiToken: Environment.get("AUDD_API_TOKEN")!)
app.post("audd", "callback") { req async throws -> HTTPStatus in
let body = try req.content.decode(Data.self)
let event = try audd.streams.parseCallback(body)
switch event {
case .match(let m): req.logger.info("\(m.song.artist) — \(m.song.title)")
case .notification(let n): req.logger.info("\(n.notificationMessage)")
}
return .ok
}
}Hummingbird:
import Hummingbird
import AudD
let audd = AudD(apiToken: ProcessInfo.processInfo.environment["AUDD_API_TOKEN"]!)
let router = Router()
router.post("/audd/callback") { req, ctx -> HTTPResponse.Status in
let body = try await Data(buffer: req.body.collect(upTo: 1 << 20))
let event = try audd.streams.parseCallback(body)
// handle event.match / event.notification
return .ok
}If you can't expose a public callback URL, longpoll instead. AudD still
requires a callback URL to be configured for the account
(https://audd.tech/empty/ works as a no-op receiver), and the SDK
preflights this for you — pass LongpollOptions(skipCallbackCheck: true) to
skip if you've already verified.
longpoll(...) returns a LongpollPoll with three typed AsyncStreams —
matches, notifications, and a single-shot errors stream. Iterate the ones you
care about (often concurrently in a withTaskGroup):
let streams = await audd.streams
let radioID = 1 // any integer you choose — your handle for this stream
let poll = try await streams.longpoll(radioID: radioID)
defer { Task { await poll.close() } }
for await match in poll.matches {
print("\(match.song.artist) — \(match.song.title)")
}A terminal failure (HTTP 5xx, malformed JSON, connection drop after retries)
fires once on poll.errors and then closes all three streams.
deriveLongpollCategory is a local computation: MD5(MD5(apiToken) + radioID)[..9].
The category alone is sufficient to subscribe — the api_token is never sent
over the wire for longpolls.
For browser widgets, embedded extensions, or any context where shipping the
api_token would leak it: derive the category server-side, ship only the
category to the consumer, and have the consumer use LongpollConsumer:
import AudD
// `category` was derived on your server and shared with this process.
let consumer = LongpollConsumer(category: "abc123def")
defer { consumer.close() }
let poll = consumer.iterate()
defer { Task { await poll.close() } }
for await match in poll.matches {
print("\(match.song.artist) — \(match.song.title)")
}LongpollConsumer carries no api_token. The category alone authorizes the
subscription. The returned LongpollPoll exposes the same
matches / notifications / errors streams as the authenticated path.
Full DocC reference ships with the SDK at
Sources/AudD/AudD.docc/ — articles for
recognition, streams, errors, and streaming-helper resolution rules, plus
symbol-level docs for every public type. Build it locally:
swift package generate-documentation --target AudDThe custom-catalog endpoint is NOT how you submit audio for music recognition. For recognition, use
recognize(orrecognizeEnterprisefor files longer than 25 seconds). The custom-catalog endpoint adds songs to your private fingerprint database so futurerecognizecalls on your account can identify your own tracks. Requires special access — contact api@audd.io.
try await audd.customCatalog.add(audioID: 42, source: .url(URL(string: "https://my.song.mp3")!))MIT — see LICENSE.
- Documentation: https://docs.audd.io
- Tokens: https://dashboard.audd.io
- Issues: https://github.com/AudDMusic/audd-swift/issues
- Email: api@audd.io
Runnable end-to-end examples live under Examples/ as a
sibling SwiftPM package — kept separate from the SDK proper so consumers
don't pull in CLI executables they'll never use:
cd Examples
swift run RecognizeUrl