Tutorial - Tonn API: Vocal + Beat Mix & Master (Stem Separation & Recombine)¶
The Tonn API's recombine endpoint mixes a vocal against a backing track — a beat, an instrumental, or the reconstructed backing from a stem separation job — using a fixed automatic mix recipe, then masters the result. The stem separation endpoint splits a finished stereo mix into stems using a GPU-accelerated source-separation model, feeding the same recombine engine. By default it returns four stems — vocals, drums, bass, and other; set separationMode: "vocals_instrumental" to instead return two stems — vocals and instrumental (no-vocals backing). Both modes accept an optional webhookURL for async status notifications. Together they cover two jobs: rebuilding controllable parts from an already-mixed track, and mixing a recorded vocal directly against a beat you already have.
Vocal + Beat Mix & Master API (Quickstart)¶
The most common use of /recombine doesn't touch stem separation at all: you have a recorded vocal (e.g. a rap take) and a licensed or purchased beat, and you want them mixed together and mastered without building a DSP/mixing chain yourself. This is a vocal mixing API / automatic vocal mixing over a beat workflow — point it at two URLs and get back a mastered stereo master.
import requests
API_KEY = "your-api-key"
BASE_URL = "https://tonn.roexaudio.com"
response = requests.post(
f"{BASE_URL}/recombine",
headers={"X-API-Key": API_KEY},
json={
"recombineData": {
"vocalStemURL": "https://your-storage.com/vocal_take.wav",
"backingStemURL": "https://your-storage.com/licensed_beat.wav",
"musicalStyle": "HIPHOP_GRIME",
"desiredLoudness": "MEDIUM",
"vocalGainDb": 0.0
}
},
timeout=30,
)
recombine_task_id = response.json()["recombineTaskId"]
Poll GET /recombinestatus/{id}, then call POST /retrieverecombinepreview for a free 30-second MP3 before spending any credits, and POST /retrieverecombine for the final master. This direct flow never calls /separate, so it only ever costs the 250-credit final-retrieval charge — the 75-credit separation fee simply doesn't apply.
vocalGainDb: 0(the default) trusts Tonn's automatic balance. Positive values bring the vocal forward in the mix; negative values tuck it further into the beat. Start at0, preview, then try small ±1–3 dB moves before reaching for the ±6 dB limits.- The vocal and beat must already start on the same timeline — silence before the vocal is preserved as-is. The endpoint does not tune vocals, edit timing, loop a short beat to match song length, or infer song structure; it mixes and masters what you give it.
- An unmastered WAV beat with headroom gives the mix engine the most to work with, but properly licensed marketplace WAV/MP3 beats work too.
- Rights: you must own or hold an appropriate licence for both the vocal and the beat you submit. Purchasing a marketplace beat tier does not automatically grant you every distribution or derivative-use right — check the licence terms for the release you're planning.
This same endpoint also covers mix vocals with beat, rap vocal mixing, and two-track beat mixing use cases — anywhere you have a vocal and an instrumental as two separate files and want one mastered stereo result. Think of it as a vocal and instrumental mix and master API: two URLs in, one mastered file out.
What You Can Build¶
- Vocal + beat mix and master — mix a recorded vocal (rapped or sung) against a purchased or licensed beat and get a mastered stereo file back, with no DSP or mastering chain of your own.
- Catalog remastering — revive older masters that have no surviving multitrack session by separating and remixing the existing stereo file.
- Vocal-up / vocal-down remixes — nudge the lead vocal forward or back relative to the instrumental with
vocalGainDb, without re-recording anything. - Karaoke and practice tools — isolate or mute the vocal stem for backing tracks and play-along apps.
- Remix and sample prep — hand off clean drum, bass, vocal, and "other" stems to a producer or DJ tool.
- Restoration workflows — recover usable stems for a track whose original session files are lost.
How It Works¶
POST /separate → 4 stems (vocals, drums, bass, other) or 2 stems (vocals + instrumental)
GET /separatestatus/{id} → poll until "complete"
POST /retrievestems → download the stem URLs (free)
POST /recombine → mix vocal + backing (fixed recipe + optional vocalGainDb), master to a loudness preset
GET /recombinestatus/{id} → poll until "complete"
POST /retrieverecombinepreview → 30s MP3 preview (free)
POST /retrieverecombine → full mastered WAV (charged once)
Separation and recombine are independent services connected by one field: pass the stemflowTaskId you get back from /separate as stemJobId to /recombine, and the vocal stem plus the complete summed backing (drums + bass + other) are resolved automatically. By default /separate returns four stems — vocals, drums, bass, and other. Set separationMode: "vocals_instrumental" to get just two stems instead: vocals and a single instrumental (no-vocals) stem. If you already have your own vocal and backing/beat files — the vocal + beat use case above — call /recombine directly with vocalStemURL and backingStemURL and skip separation entirely.
Pricing at a Glance¶
| Step | Endpoint | Cost |
|---|---|---|
| Submit separation | POST /separate |
Reserved at submission; 75 credits captured only if separation succeeds |
| Poll / retrieve stems | GET /separatestatus/{id}, POST /retrievestems |
Free |
| Submit recombine | POST /recombine |
Free |
| Poll / preview | GET /recombinestatus/{id}, POST /retrieverecombinepreview |
Free |
| Final master | POST /retrieverecombine |
250 credits, charged once on first successful retrieval |
A full separate-then-recombine pipeline costs 325 credits total ($3.25 on the Small plan, $2.60 on the Large plan). If you already have vocal and backing/beat files and use the direct vocalStemURL + backingStemURL flow, separation is skipped entirely and the pipeline costs only 250 credits. See Pricing for package rates. If separation fails, the 75-credit reservation is released automatically and you are not charged. Repeated calls to /retrieverecombine for the same task return the same master URL without charging again.
Prerequisites¶
- A valid API key (see Getting Started)
- An audio file uploaded via
/upload, or any publicly reachable WAV/FLAC/MP3 URL - Python with
requestsinstalled, or cURL
Base URL:
https://tonn.roexaudio.com
All requests require the X-API-Key header for authentication.
Step 1: Upload Your Audio¶
import requests
API_KEY = "your-api-key"
BASE_URL = "https://tonn.roexaudio.com"
def upload_track(filename, content_type="audio/wav"):
response = requests.post(
f"{BASE_URL}/upload",
headers={"X-API-Key": API_KEY},
json={"filename": filename, "contentType": content_type},
timeout=30,
)
response.raise_for_status()
result = response.json()
with open(filename, "rb") as f:
upload_response = requests.put(
result["signed_url"],
data=f,
headers={"Content-Type": content_type},
timeout=120,
)
upload_response.raise_for_status()
return result["readable_url"]
audio_url = upload_track("finished_mix.wav")
If you're doing the direct vocal + beat flow, upload both files the same way (or use any publicly reachable URL you already host) and skip to Step 5.
Step 2: Submit a Separation Job¶
response = requests.post(
f"{BASE_URL}/separate",
headers={"X-API-Key": API_KEY},
json={"stemflowData": {"audioFileLocation": audio_url}},
timeout=30,
)
response.raise_for_status()
stemflow_task_id = response.json()["stemflowTaskId"]
print(f"Separation task created: {stemflow_task_id}")
Response:
{
"stemflowTaskId": "sf_5c1e2b7a-...",
"error": false,
"message": "Successfully created stem separation task."
}
A 402 response means your account does not have 75 available credits and auto top-up either is not enabled or failed; a 400 means audioFileLocation is missing or malformed.
By default separation returns four stems. To get just a vocals and an instrumental (no-vocals) stem, pass separationMode: "vocals_instrumental"; to receive async status notifications (pending/started/completed/failed) instead of polling, pass webhookURL:
response = requests.post(
f"{BASE_URL}/separate",
headers={"X-API-Key": API_KEY},
json={
"stemflowData": {
"audioFileLocation": audio_url,
"separationMode": "vocals_instrumental",
"webhookURL": "https://your-server.com/webhooks/tonn",
}
},
timeout=30,
)
The webhook is delivered via a retried Cloud Tasks queue, so a transient failure at your endpoint is retried rather than lost. webhookURL is optional — polling with /separatestatus continues to work regardless.
Step 3: Poll Separation Status¶
import time
def wait_for_separation(stemflow_task_id, timeout_seconds=1200, poll_interval=15):
deadline = time.monotonic() + timeout_seconds
while time.monotonic() < deadline:
response = requests.get(
f"{BASE_URL}/separatestatus/{stemflow_task_id}",
headers={"X-API-Key": API_KEY},
timeout=30,
)
response.raise_for_status()
status = response.json()["status"]
if status == "complete":
return
if status == "failed":
raise RuntimeError(f"Separation failed: {response.json().get('info')}")
time.sleep(poll_interval)
raise TimeoutError("Separation did not complete in time")
wait_for_separation(stemflow_task_id)
Separation is GPU-bound; a typical track completes in well under a minute once a GPU instance is warm, but a cold start can occasionally take several minutes. Always poll with a bounded timeout — never loop forever on processing.
Step 4: Retrieve the Stems (Free)¶
response = requests.post(
f"{BASE_URL}/retrievestems",
headers={"X-API-Key": API_KEY},
json={"stemflowData": {"stemflowTaskId": stemflow_task_id}},
timeout=30,
)
response.raise_for_status()
stems = response.json()["stems"]
print(stems["vocals"], stems["drums"], stems["bass"], stems["other"])
For a vocals_instrumental job, the response contains vocals and instrumental (with bass, drums, other empty):
stems = response.json()["stems"]
print(stems["vocals"], stems["instrumental"])
Response:
{
"stemflowTaskId": "sf_5c1e2b7a-...",
"stems": {
"vocals": "https://storage.googleapis.com/.../vocals.wav?X-Goog-Signature=...",
"bass": "https://storage.googleapis.com/.../bass.wav?X-Goog-Signature=...",
"drums": "https://storage.googleapis.com/.../drums.wav?X-Goog-Signature=...",
"other": "https://storage.googleapis.com/.../other.wav?X-Goog-Signature=..."
},
"error": false,
"message": "Successfully retrieved separated stems."
}
Each stem is a 44.1kHz stereo WAV. Stem URLs are signed and expire after 24 hours — download or re-request before then.
Step 5: Submit a Recombine Job¶
Reference the separation job directly with stemJobId — the vocal stem and a complete summed backing (drums + bass + other) are resolved for you. musicalStyle is required; desiredLoudness and vocalGainDb are optional:
response = requests.post(
f"{BASE_URL}/recombine",
headers={"X-API-Key": API_KEY},
json={
"recombineData": {
"stemJobId": stemflow_task_id,
"musicalStyle": "POP",
"desiredLoudness": "MEDIUM"
}
},
timeout=30,
)
response.raise_for_status()
recombine_task_id = response.json()["recombineTaskId"]
Or use the direct vocal + beat flow and skip separation entirely — this is the path for mixing a recorded vocal against a licensed beat:
response = requests.post(
f"{BASE_URL}/recombine",
headers={"X-API-Key": API_KEY},
json={
"recombineData": {
"vocalStemURL": "https://your-storage.com/vocal.wav",
"backingStemURL": "https://your-storage.com/backing.wav",
"musicalStyle": "POP",
"desiredLoudness": "MEDIUM",
"vocalGainDb": 1.5
}
},
timeout=30,
)
Provide either stemJobId or vocalStemURL + backingStemURL — not both. musicalStyle is required in both cases (it materially affects the mix and mastering decisions). desiredLoudness defaults to MEDIUM and vocalGainDb defaults to 0.0 if omitted. An optional webhookURL can be added to receive async status notifications (pending/started/completed/failed) instead of polling recombinestatus.
musicalStyle (required): ROCK_INDIE, POP, ACOUSTIC, HIPHOP_GRIME, ELECTRONIC, REGGAE_DUB, ORCHESTRAL, METAL, OTHER, JAZZ, LO_FI, LATIN, TECHNO, HOUSE, TRAP, COUNTRY_ACOUSTIC, CINEMATIC, AFROBEAT, K_POP, REGGAETON
desiredLoudness (optional, default MEDIUM): LOW, MEDIUM, or HIGH. These are mastering presets, matching /masteringpreview — LOW is the quietest/most dynamic, MEDIUM is the balanced default, HIGH is the loudest/most competitive. They guide the mastering engine but are not a guarantee of an exact LUFS result; see the measured value in the response.
vocalGainDb (optional, default 0.0, range -6.0 to +6.0): a relative trim applied on top of Tonn's automatic vocal/backing balance, not an absolute fader position. 0.0 keeps the automatic mix untouched; positive values raise the vocal, negative values lower it. It does not control panning, reverb, EQ, compression, or the backing track's level — those come from the fixed automatic recipe (vocal: lead presence, low reverb; backing: normal presence, dry).
A 404 on stemJobId means the ID does not exist, does not belong to your account, or belongs to a different key on your account — this endpoint never reveals whether a task exists for someone else. A 400 means the referenced separation job has not finished yet, or a required field (like musicalStyle) is missing.
Step 6: Poll Recombine Status¶
def wait_for_recombine(recombine_task_id, timeout_seconds=1200, poll_interval=15):
deadline = time.monotonic() + timeout_seconds
while time.monotonic() < deadline:
response = requests.get(
f"{BASE_URL}/recombinestatus/{recombine_task_id}",
headers={"X-API-Key": API_KEY},
timeout=30,
)
response.raise_for_status()
status = response.json()["status"]
if status == "complete":
return
if status == "failed":
raise RuntimeError(f"Recombine failed: {response.json().get('info')}")
time.sleep(poll_interval)
raise TimeoutError("Recombine did not complete in time")
wait_for_recombine(recombine_task_id)
Step 7: Preview the Result (Free)¶
response = requests.post(
f"{BASE_URL}/retrieverecombinepreview",
headers={"X-API-Key": API_KEY},
json={"recombineData": {"recombineTaskId": recombine_task_id}},
timeout=30,
)
response.raise_for_status()
preview = response.json()["preview"]
print(preview["preview_url"], preview["measured_lufs_full"])
The preview is a ~30-second MP3 excerpt — the loudest section of the mastered track, not necessarily the beginning — free for accounts with purchased credits and safe to call as many times as you like. measured_lufs_full is the full mastered program's measured loudness (the excerpt itself isn't separately measured), and preview_start_seconds tells you where in the full track the excerpt begins.
Step 8: Retrieve the Final Master¶
This is the only endpoint in the whole workflow that charges credits (250, on the first successful call for a given task):
response = requests.post(
f"{BASE_URL}/retrieverecombine",
headers={"X-API-Key": API_KEY},
json={"recombineData": {"recombineTaskId": recombine_task_id}},
timeout=30,
)
if response.status_code == 402:
raise RuntimeError("Insufficient credits for the final master")
response.raise_for_status()
result = response.json()["result"]
print(result["master_url"], result["measured_lufs"], result["peak_level"])
Response:
{
"recombineTaskId": "rc_9f3d1a44-...",
"result": {
"master_url": "https://storage.googleapis.com/.../recombined_master.wav?X-Goog-Signature=...",
"measured_lufs": -13.4,
"peak_level": -1.2,
"track_length_seconds": 187.4,
"sample_rate": 44100,
"bit_depth": 16,
"channels": 2,
"processing": {
"musical_style": "POP",
"desired_loudness": "MEDIUM",
"vocal_gain_db": 0.0
}
},
"error": false,
"message": "Successfully retrieved recombined master."
}
measured_lufs and peak_level are the measured values of the full mastered program — desiredLoudness selects a preset the mastering engine targets, not a guaranteed exact LUFS output, so expect the measured value to land close to, but not always exactly on, the preset's usual range. processing echoes back the settings actually used, so you can confirm which preset and vocal trim produced this master. Calling /retrieverecombine again for the same recombineTaskId returns the same master_url without a second charge — safe to retry after a network error.
Full Pipeline (cURL)¶
export BASE="https://tonn.roexaudio.com"
export KEY="your-api-key"
# 1. Separate
SF_ID=$(curl -s -X POST "$BASE/separate" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"stemflowData": {"audioFileLocation": "https://your-storage.com/track.wav"}}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['stemflowTaskId'])")
# 2. Poll until complete
until [ "$(curl -s "$BASE/separatestatus/$SF_ID" -H "X-API-Key: $KEY" | python3 -c 'import sys,json; print(json.load(sys.stdin)["status"])')" = "complete" ]; do sleep 15; done
# 3. Get stems (free)
curl -s -X POST "$BASE/retrievestems" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"stemflowData\": {\"stemflowTaskId\": \"$SF_ID\"}}"
# 4. Recombine from the separation job
RC_ID=$(curl -s -X POST "$BASE/recombine" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"recombineData\": {\"stemJobId\": \"$SF_ID\", \"musicalStyle\": \"POP\", \"desiredLoudness\": \"MEDIUM\"}}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['recombineTaskId'])")
# 5. Poll until complete
until [ "$(curl -s "$BASE/recombinestatus/$RC_ID" -H "X-API-Key: $KEY" | python3 -c 'import sys,json; print(json.load(sys.stdin)["status"])')" = "complete" ]; do sleep 15; done
# 6. Preview (free)
curl -s -X POST "$BASE/retrieverecombinepreview" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"recombineData\": {\"recombineTaskId\": \"$RC_ID\"}}"
# 7. Final master (charges 250 credits once)
curl -s -X POST "$BASE/retrieverecombine" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"recombineData\": {\"recombineTaskId\": \"$RC_ID\"}}"
Or the direct vocal + beat flow, which never calls /separate and only costs 250 credits:
RC_ID=$(curl -s -X POST "$BASE/recombine" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"recombineData": {"vocalStemURL": "https://your-storage.com/vocal.wav", "backingStemURL": "https://your-storage.com/beat.wav", "musicalStyle": "HIPHOP_GRIME", "desiredLoudness": "MEDIUM", "vocalGainDb": 0}}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['recombineTaskId'])")
until [ "$(curl -s "$BASE/recombinestatus/$RC_ID" -H "X-API-Key: $KEY" | python3 -c 'import sys,json; print(json.load(sys.stdin)["status"])')" = "complete" ]; do sleep 15; done
curl -s -X POST "$BASE/retrieverecombinepreview" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"recombineData\": {\"recombineTaskId\": \"$RC_ID\"}}"
curl -s -X POST "$BASE/retrieverecombine" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"recombineData\": {\"recombineTaskId\": \"$RC_ID\"}}"
Response Codes¶
| Code | Meaning |
|---|---|
| 200 | Success — task created, or results ready (a failed job also returns 200 with "status": "failed") |
| 202 | Still processing — try again shortly |
| 400 | Bad request — missing/invalid field (e.g. musicalStyle), invalid combination of recombine inputs, or referenced separation job not yet complete |
| 401 | Invalid API key |
| 402 | Insufficient credits — separation reservation or final recombine charge could not be placed |
| 404 | Task or stemJobId not found, or not owned by your account |
| 500 | Pricing configuration missing, or the job could not be enqueued (any reserved credits are released automatically) |
Limitations¶
/recombineuses a fixed automatic mix recipe (vocal: lead presence, low reverb; backing: normal presence, dry) plus the optionalvocalGainDbtrim — it is not a fully custom rebalance, and does not expose control over panning, reverb, EQ, compression, or backing level.- The vocal and backing/beat must already start on the same timeline; the endpoint does not tune vocals, edit timing, loop a short beat to match song length, or infer song structure.
- Separation output is stereo only; sources with more than two channels are rejected.
- Near-silent input audio is rejected before it reaches the model.
- When using
stemJobId, all three backing stems (bass, drums, other) must be present from the referenced separation job. - All output audio (stems, preview, and master) is 44.1kHz; the final master is stereo 16-bit PCM WAV.
/recombinemixes exactly two parts — a vocal and a backing stem. For rebalancing three or more individually-mixed tracks, use the Multitrack Mixing endpoint instead.- All returned URLs are signed and expire after 24 hours.
- You must own or hold an appropriate licence for every vocal and beat you submit; the API does not verify rights and does not guarantee any particular vocal/beat combination is release-ready.
Next Steps¶
- See the API Reference for full endpoint schemas
- See the Pricing page for package rates
- See the FAQ for supported formats and general account questions
- See Mix Enhance if you want enhancement and optional stem processing applied to a stereo mix in a single call instead of separate/recombine