Mixing Scene JSON
A scene is the pure-data description of a whole mixer.
It is the format Mixer.fromSceneJson(...) reads and toSceneJson() writes. In Python, the names are from_scene_json(...) and to_scene_json().
The format is identical across WASM, Python, Node, the C ABI, and C++. Because it is plain JSON, you can store it with a project, diff it in git, hand-edit it, and reload it later. A JSON Schema for it, mixer-scene.schema.json, ships inside the npm and Python packages so a hand-written or generated scene can be checked before it ever reaches a mixer — see Validating a scene against the schema.
If you have not met strips, sends, and buses yet, read Mixing Basics and the Mixing Engine guide first. This page is the field-by-field reference.
The demo below shows the same routing ideas without JSON: lanes feed a small mixer, sends and levels change the output, and the meters respond immediately. Use it first if the scene fields feel abstract, then come back to the schema with the signal flow in mind.
What You Will Learn
By the end of this page you should be able to:
- recognize the top-level scene shape and the role of strips, buses, VCA groups, and connections;
- edit or generate a scene without confusing strip controls, inserts, sends, and routing edges;
- understand which fields have defaults and which identifiers must match across the graph;
- validate a scene against the shipped JSON Schema, and know which mistakes only the loader can catch;
- use a built-in preset as the safest starting point for custom scene JSON.
Learn the format by example
The fastest way to understand the format is to print a built-in preset and read it: mixingScenePresetJson('vocalReverbSend'). Every field below appears in that output, so you can match each key to a real value. The annotated preset at the end of this page does exactly that.
Top-level shape
{
"version": 1,
"strips": [],
"buses": [],
"vcaGroups": [],
"connections": []
}| Field | Type | Meaning |
|---|---|---|
version | integer | Format version. Currently must be 1; other values are rejected. The schema requires the key; the loader treats a missing version as 1. |
strips | array | The track lanes (see Strip). |
buses | array | Shared destinations, including the master (see Bus). |
vcaGroups | array | Level groups that trim several strips at once (see VCA group). |
connections | array | The routing graph edges (see Connection). |
The loader accepts unknown keys, and reports them
The parser still loads a scene that carries fields it does not recognize, so a forward-compatible producer can add metadata without breaking older readers. A key that starts with $ or x- is reserved for that purpose and is never reported. Any other key the reader does not consume is a likely typo — processorName (wrong) vs processor (right), or faderDB for faderDb — and it takes no effect, so the field keeps its default.
Every such key is reported as a non-fatal warning with its path from the document root, e.g. unknown scene key 'strips[0].faderDB'. The scene still loads. Read the warnings right after loading instead of hunting for a knob that "does nothing":
Mixer.sceneWarnings()(Pythonscene_warnings()) afterMixer.fromSceneJson(...);- the string list that
Project.setMixerSceneJson(...)returns (Pythonset_mixer_scene_json), empty when the scene is clean; - the
unknown_scene_keydiagnostic thatProject.fromJsonWithDiagnostics(...)returns in itsdiagnosticstext for the scene embedded in a project document, with a path such asscene.strips[2].faderDB(Project.fromJsonloads the same document and drops the diagnostics). The rest of the project document is not checked for unknown keys.
Insert params keys are covered by the same list: a param key that no processor consumed is reported too. Enumerate the keys an insert actually reads with masteringInsertParamNames(name) (Python mastering_insert_param_names(name)).
The shipped schema catches the same mistakes before any load. Every object uses additionalProperties: false with patternProperties allowing $ and x- annotations, so misspellings such as faderDB fail validation while reserved metadata keys validate.
The live-engine setters are stricter than a whole-scene load. setTrackStripJson, setBusStripJson, setMasterStripJson (and the Node, WASM, and Python setters over them) and the strip EQ band setters (setTrackStripEqBand, setTrackStripEqBandJson, and the bus and master counterparts) refuse a key they do not consume with InvalidParameter, and the message names it: unknown strip key 'strips[0].faderDB', unknown band key 'gainDB'. The legacy snake_case spellings and the $ / x- keys are accepted.
Strip
Each strip object describes one channel lane. All numeric fields have sensible defaults, so a minimal strip is just { "id": "vocal" }.
| Field | Type | Default | Meaning |
|---|---|---|---|
id | string | — (required) | Unique strip identifier used by connections, sends, and VCA members |
inputTrimDb | number | 0 | Gain before any processing (the first stage in the strip signal flow) |
faderDb | number | 0 | Main fader level |
vcaOffsetDb | number | 0 | Per-strip VCA trim summed into the fader stage (the live setVcaOffsetDb(...) value; separate from any VCA group gainDb, which is applied as a delta on top and is not stored in this field) |
pan | number | 0 | Pan position, -1 (left) … +1 (right). The loader clamps to that range; the schema rejects values outside it |
width | number | 1 | Stereo width / side multiplier (0 = mono, 1 = unchanged, 2 = doubled). Clamped to 0 … 2 on load; the schema rejects values outside it |
muted | boolean | false | Silences the strip |
soloed | boolean | false | Implies-mutes other (non-solo-safe) strips |
soloSafe | boolean | false | Never implied-muted by another strip's solo |
panMode | integer | 0 | 0 = balance, 1 = stereo pan, 2 = dual pan. Anything else is rejected at load |
dualPanLeft | number | -1 | Left position in dual-pan mode (default is identity hard-left, preserving the stereo image); clamped to -1 … +1 |
dualPanRight | number | 1 | Right position in dual-pan mode (default is identity hard-right); clamped to -1 … +1 |
sourceLayout | string | "stereo" | Channel layout of the source feeding the strip: "mono", "stereo", "5.1", or "7.1". The writer omits it at the stereo default; any other string is rejected at load |
surroundPan | object | identity | Surround-pan position for wider-than-stereo hosts. azimuth, divergence, and lfe affect the realtime engine's 5.1/7.1 group-bus panner; elevation and distance are reserved. Values are clamped on load and round-tripped through scene JSON; the writer omits the object at the centred default. The standalone offline Mixer remains stereo and therefore does not apply it |
metering | object | all on | Which meters the strip computes: enabled, lufs, truePeak (booleans, default true) and truePeakOversample (integer, default 4, must be 1 … 16; the meter resolves it to 2x for 2, 8x for 8–16, and 4x otherwise). The writer omits the object when every meter is at its default |
polarityInvertLeft | boolean | false | Inverts the left channel polarity |
polarityInvertRight | boolean | false | Inverts the right channel polarity |
panLaw | integer | 0 | 0 = const 3 dB, 1 = const 4.5 dB, 2 = const 6 dB, 3 = linear 0 dB. Anything else is rejected at load |
channelDelaySamples | integer | 0 | Per-strip delay, 0 … 192000; delays only this strip and is excluded from PDC. Out of range is rejected at load |
eq | object | identity | The strip's built-in equalizer, which runs pre-fader: enabled (default true) and bands, at most 24 EQ bands. Omitted by the writer while it is enabled and every band is at its default. Scene JSON is the only way to configure it from WASM, which has no EQ setters on Mixer |
inserts | array | [] | In-series processors (see Insert) |
sends | array | [] | Parallel sends to buses (see Send) |
Enums are integers in the file, strings in the API
The scene file stores panMode and panLaw as integers, but insert slot and send timing are stored as the short string tokens "pre" / "post". The JavaScript runtime methods accept friendly strings — setPanLaw(strip, 'const3dB'), addSend(..., 'postFader'). Python accepts the same send/tap names, but pan-law strings use normalized names such as 'const-3db', 'const-4.5db', 'const-6db', or 'linear-0db' (or the enum/int value). Both map to the same underlying value; the difference is just file format vs. ergonomic API.
Serializing a scene after runtime pan edits preserves the strip's current panMode. Use Mixer.toSceneJson() / Mixer.to_scene_json() instead of rebuilding the pan fields by hand.
Insert slot and send timing must be strings. A non-string value — e.g. a numeric "timing": 1 — is rejected at load time, with the reason send timing must be a string ("pre" or "post"). Always write "pre" or "post".
How a rejected scene reaches you
Everything that stops a scene from building (malformed JSON, an unsupported version, an out-of-range field, a non-string timing) is reported with the C ABI's own code and the specific reason in the message.
| Runtime | What you catch | Code |
|---|---|---|
| WASM | SonareError, message failed to build mixer from scene JSON: <reason> | InvalidFormat for a malformed document, a fractional integer, or an out-of-range panMode, panLaw, channelDelaySamples, or metering.truePeakOversample; InvalidParameter for an unsupported version, a non-string slot or timing, an unknown layout, or a graph the build refuses |
| Node native | SonareError with the same message and codes | Same |
| Python | SonareError with the same message and codes | Same |
| C ABI | nullptr from sonare_mixer_from_scene_json | sonare_last_error_code(); the reason is in sonare_last_error_message() |
Branch on code; log the message to see which field was wrong. Validating a scene before you hand it to a mixer still gives the earliest, path-naming message.
Field terms: dual pan, polarity invert, pan law, PDC
- Dual pan (
panMode: 2): pans the left and right channels to independent positions instead of moving the whole signal together. Useful for narrowing or re-placing an already-stereo source. - Polarity invert: multiplies a channel by −1, flipping the waveform. Used to fix a track recorded out of phase with another; it changes phase relationships, not perceived loudness on its own.
- Pan law: how much the center is attenuated relative to hard-left/right so loudness stays even as you pan.
const 3 dBis constant power,const 6 dBis constant amplitude (it keeps the mono sum steady),const 4.5 dBsits between the two, andlinear 0 dBapplies no center compensation. The center dip reaches the output only in stereo-pan mode (panMode: 1); in the default balance mode a centered strip stays at 0 dB under every law. See Mixing Engine. - PDC (plugin-delay compensation): when one path is delayed by a lookahead processor, the engine delays the shorter paths to match so everything lines up at the master.
channelDelaySamplesis excluded from that calculation.
Insert
An insert is a named processor running in series inside the strip (or a bus).
| Field | Type | Meaning |
|---|---|---|
slot | "pre" | "post" | Runs before or after the fader; defaults to "pre" when omitted. Note the short tokens — not preFader/postFader. |
processor | string | Required. Processor id, e.g. eq.parametric, dynamics.compressor, effects.reverb.plate. See Mastering Processors for solo processors and Effects Inserts for the creative-FX catalog. An id the registry does not know is refused when the mixer is built, not by the schema. |
params | object | The processor's parameters, keyed by parameter name, e.g. { "thresholdDb": -18, "ratio": 2.5 }. Values are numbers or booleans, with two processor-specific exceptions: a string for a named rig or an embedded impulse response, and a per-band array for the acoustic room morph. |
sidechainKey | string | Optional. Strip id whose signal feeds this insert's external sidechain (e.g. ducking). Omitted when empty. |
Sidechain and ducking
Normally a processor reacts to the audio passing through it. A sidechain makes it react to a different track instead: sidechainKey names that other strip. The classic use is ducking: a compressor on the music that turns the music down whenever the named voice strip is loud, so speech stays clear over a music bed.
On the live realtime engine, sidechain keys are bound with setLaneSidechain, setBusSidechain, and setMasterSidechain, which refuse a binding they cannot honor with InvalidParameter. canSetLaneSidechain(trackId, insertIndex, sourceTrackId), canSetBusSidechain, and canSetMasterSidechain ask the same question without changing anything and answer { ok, reason }, so a UI can gray out an impossible choice before the user makes it. reason is null exactly when ok is true; otherwise it is the first check that failed:
reason | The binding is refused because |
|---|---|
invalidTarget | the track id is 0, or the bus is not configured |
insertOutOfRange | the strip has no insert at that index |
undeclaredSource | the source track or bus is not declared |
invalidSourceKind | the source kind is neither a track nor a bus |
selfKey | a strip would key itself |
cycle | the key would close a loop through outputs, sends, or other keys |
tableFull | the sidechain binding table is full |
planRefused | no delay-compensation plan exists for the alignment |
The calls and their setup are in Realtime Engine.
params is an object; the escaped-string form is legacy
toSceneJson() writes params as a nested object ("params": { "ratio": 2.5 }), and that is the only shape the schema admits. The loader still accepts the older form, an escaped JSON string ("params": "{\"ratio\":2.5}"), so a scene saved by an earlier release loads unchanged; a validator rejects it as is not object. When you write a scene by hand, use the object form. Anything that is neither an object nor a string is refused at load with insert params must be a JSON object (or the legacy JSON string).
Send
A send routes a copy of the strip's signal to a destination bus. The timing field chooses whether the copy is tapped before the fader ("pre") or after it ("post") — see the channel-strip signal flow.
| Field | Type | Meaning |
|---|---|---|
id | string | Send identifier, unique within the strip |
destinationBusId | string | Required. The id of the target bus. An id that no bus declares creates an implicit aux bus that routes to the master, so a misspelled destination is not rejected: it makes sound, and it raises a scene warning naming the strip and the bus (send 'a' targets undeclared bus 'nope'; created as implicit aux). Declare the bus you mean |
sendDb | number | Send level in dB, default 0 |
timing | "pre" | "post" | Tapped before or after the fader (again, short tokens); defaults to "post" |
Bus
| Field | Type | Meaning |
|---|---|---|
id | string | Required. Bus identifier (one bus is conventionally "master") |
role | string | "master", "aux" (the default), or a group bus such as "submix" |
layout | string | Channel layout of the bus: "mono", "stereo" (default), "5.1", or "7.1". The master bus carries the project output layout. Omitted by the writer at the stereo default |
inputTrimDb | number | Gain at the bus input, default 0 |
width | number | Stereo width of the bus, default 1, clamped to 0 … 2 like a strip's. Refused when layout is wider than stereo, since a surround bed has no stereo image to narrow or widen |
polarityInvertLeft / polarityInvertRight | boolean | Per-channel polarity flip on the bus, default false |
pan, panMode, dualPanLeft, dualPanRight, panLaw | as on a strip | Same meaning and ranges as the strip fields. Anything other than the defaults is refused when layout is wider than stereo |
eq | object | The bus's built-in equalizer; the same shape as the strip's eq |
inserts | array | Processors on the bus itself (same Insert shape as a strip) |
The writer omits layout, inputTrimDb, width, the pan fields, eq, and both polarity flags whenever they sit at their defaults, so a plain stereo bus serializes as just id, role, and inserts.
EQ bands
A strip's or bus's eq object is { "enabled": true, "bands": [...] }, with at most 24 bands. Each band has the same fields as the realtime engine's EqBand:
| Field | Meaning |
|---|---|
enabled | Defaults to false: a band does nothing until it is enabled |
type | Peak (default), LowShelf, HighShelf, LowPass, HighPass, BandPass, Notch, TiltShelf, FlatTilt, or AllPass |
frequencyHz, gainDb, q | Center or corner frequency (default 1000), gain (default 0), and resonance or slope |
slopeDbOct, coeffMode, placement, phase, soloed, bypassed | Filter slope, coefficient mode, stereo placement, phase mode, and per-band solo and bypass |
dynamic, thresholdDb, ratio, rangeDb, attackMs, releaseMs, … | The per-band dynamic-EQ fields; see eqBand in the schema for the full list |
A band left out of the list stays at the identity, and the writer drops the whole object while every band is untouched.
Only master is a special role token
master is the only role the engine treats specially, and aux is just the default a bus gets when it has no role. Any other role string is a generic non-master bus, so a "drum bus" works the same whether its role is submix, subgroup, or group: the token is a label, not a behavior switch. The built-in drumBusSubgroup preset uses subgroup, so if you print it (mixingScenePresetJson('drumBusSubgroup')) you will see "role": "subgroup", not "submix". A misspelt "mastre" is not an error either. That bus becomes an ordinary one, and the master is the bus with id master or an implicit one. The scene still sounds, but not through the bus you configured.
VCA group
A VCA (voltage-controlled amplifier) group is one fader that trims several strips at once without re-routing their audio.
| Field | Type | Meaning |
|---|---|---|
id | string | Required. Group identifier |
gainDb | number | Offset summed into each member's fader, default 0 |
members | string[] | Strip ids governed by the group |
Connection
| Field | Type | Meaning |
|---|---|---|
source | string | Required. Strip or bus id the signal leaves |
destination | string | Required. Strip or bus id the signal enters |
Connections are the graph edges. A strip routed to master is { "source": "vocal", "destination": "master" }. A send's bus reaches the master through a connection from the bus (or its return strip) to master. Both ends must name a strip or bus in the document; the mixer build refuses connection references unknown node otherwise.
A strip with no outgoing connection is routed to the master bus automatically when the graph is built. An explicitly declared bus is not: an aux or group bus with no connection out stays unpatched, which is how a scene keeps a bus silent on purpose.
Validating a scene against the schema
The scene format is described by a JSON Schema (draft 2020-12) titled MixSceneDocument. It covers the document Mixer.fromSceneJson reads, toSceneJson writes, suggestMixScene returns, and sonare mix --scene loads. The engine's own tests hold the schema to the writer's published field list and validate every built-in preset against it, so a key the writer emits cannot be missing from it.
| Where | Path |
|---|---|
| Engine source tree | schemas/mixer-scene.schema.json |
npm @libraz/libsonare | exports subpath @libraz/libsonare/schemas/mixer-scene.schema.json |
| Python wheel | libsonare/schemas/mixer-scene.schema.json inside the installed package |
| Python sdist | schemas/mixer-scene.schema.json |
The $id (https://libraz.net/schemas/libsonare/mixer-scene.schema.json) is an identifier, not a download location — point your validator at the installed copy. The @libraz/libsonare-native package and the CMake install do not carry the file; take it from one of the packages above.
Associate a schema with a scene
A scene may carry a "$schema" key for editor discovery. Keys beginning with $ or x- are accepted on every object by the schema and ignored without warnings by the loader. You can also associate the schema through an editor file-match rule or pass it directly to a validator, as below.
import Ajv from 'ajv/dist/2020';
import schema from '@libraz/libsonare/schemas/mixer-scene.schema.json' with { type: 'json' };
const validate = new Ajv().compile(schema);
const scene = JSON.parse(sceneText);
if (!validate(scene)) {
throw new Error(JSON.stringify(validate.errors, null, 2));
}
const mixer = Mixer.fromSceneJson(sceneText, 48000, 512);# npm: ajv-cli, with the draft the schema declares
npx ajv-cli validate --spec=draft2020 \
-s node_modules/@libraz/libsonare/schemas/mixer-scene.schema.json \
-d my-scene.json
# Python: check-jsonschema, reading the copy inside the installed wheel
check-jsonschema \
--schemafile "$(python -c 'from importlib.resources import files; print(files("libsonare") / "schemas/mixer-scene.schema.json")')" \
my-scene.json{
"json.schemas": [
{
"fileMatch": ["*.scene.json"],
"url": "./node_modules/@libraz/libsonare/schemas/mixer-scene.schema.json"
}
]
}Where the schema and the loader disagree
The schema is written to be stricter than the loader wherever the loader would otherwise accept a mistake silently. In the other direction, the loader enforces a few bounds the schema leaves open, and the mixer build checks references no schema can express. Validate first, then load, then read sceneWarnings() (or the list Project.setMixerSceneJson returns) — each layer catches what the previous one cannot.
| Input | Schema | Loader |
|---|---|---|
Unknown or misspelled key (faderDB) | Rejected (additionalProperties: false) | Loaded without effect and reported in sceneWarnings(); the field keeps its default. The live strip and EQ band setters refuse it |
Legacy snake_case key (fader_db, params_json, processor_name, destination_bus_id, vca_groups, …) | Rejected | Accepted; the writer never emits this spelling |
params as an escaped JSON string | Rejected (type: object) | Accepted as the legacy form |
Missing version | Rejected (required) | Assumed 1 |
Wrong-typed scalar ("faderDb": "loud", "muted": 1) | Rejected | Ignored silently; the field keeps its default |
pan, dualPanLeft, dualPanRight outside −1 … 1, width outside 0 … 2 | Rejected (minimum/maximum) | Clamped into range |
Missing id, processor, destinationBusId, source, destination | Rejected (required, minLength: 1) | Read as empty and refused later by the mixer build |
Non-object entry in strips/buses/vcaGroups/connections/inserts/sends, non-string members entry | Rejected | Skipped silently |
Number outside 32-bit float range (1e40) | Accepted | Rejected: floating-point field is non-finite or out of float range |
Unknown processor id, connection or sidechainKey naming an unknown node, duplicate ids | Accepted (not expressible) | Refused by the mixer build |
destinationBusId naming no declared bus | Accepted (not expressible) | Accepted: the send feeds an implicit aux bus routed to the master, which busCount() does not count |
Where both agree, the validator's message simply arrives earlier and names the path. They agree that version must be 1 when present, that slot and timing are "pre"/"post" strings, that sourceLayout and layout are one of the four layout tokens, and that integer fields must be integral. They also share the ranges panMode 0–2, panLaw 0–3, channelDelaySamples 0–192000, and metering.truePeakOversample 1–16.
Built-In Presets
| Preset | Intent |
|---|---|
vocalReverbSend | Vocal strip (EQ + compressor inserts) with a post-fader aux send into a plate-reverb return |
drumBusSubgroup | Kick/snare/overheads into a group bus (role subgroup), made cohesive with parallel compression and tape, ridden by a "drums" VCA |
commentaryDucking | Host/guest speech (de-ess + compress) with a music bed ducked via dynamics.sidechainRouter keyed off the host |
Discover them at runtime with mixingScenePresetNames() and fetch one with mixingScenePresetJson(name).
A complete, annotated scene
This is the actual output of mixingScenePresetJson('vocalReverbSend') (defaults trimmed for readability, so the shortened form still validates). dryWet: 1 keeps the return fully wet; the plate's own default is 0.35, so leaving it out would let dry signal into the return. It shows every relationship: a vocal strip with two pre-fader inserts and a post-fader send, a reverb return strip, two buses, and the connections that wire them to the master.
{
"version": 1,
"strips": [
{
"id": "vocal",
"faderDb": -3,
"inserts": [
{ "slot": "pre", "processor": "eq.parametric",
"params": { "band0.type": 4, "band0.frequencyHz": 80, "band1.frequencyHz": 4000, "band1.gainDb": 2 } },
{ "slot": "pre", "processor": "dynamics.compressor", "params": { "thresholdDb": -18, "ratio": 2.5 } }
],
"sends": [
{ "id": "vocal-to-verb", "destinationBusId": "vocal-verb", "sendDb": -14, "timing": "post" }
]
},
{
"id": "vocal-verb-return",
"faderDb": -10,
"width": 1.25,
"inserts": [
{ "slot": "post", "processor": "effects.reverb.plate", "params": { "decaySec": 1.8, "preDelayMs": 25, "dryWet": 1 } }
]
}
],
"buses": [
{ "id": "master", "role": "master" },
{ "id": "vocal-verb", "role": "aux" }
],
"vcaGroups": [],
"connections": [
{ "source": "vocal", "destination": "master" },
{ "source": "vocal-verb", "destination": "vocal-verb-return" },
{ "source": "vocal-verb-return", "destination": "master" }
]
}Trace the reverb below: one reverb instance, with the dry vocal and the wet return kept as separate paths to master.
The eq.parametric insert uses band-indexed keys
The eq.parametric insert reads band-indexed keys — band{N}.type, band{N}.frequencyHz, band{N}.gainDb, band{N}.q, and the per-band dynamic-EQ fields. In this preset, band0 is an 80 Hz high-pass ("band0.type": 4 is HighPass in the EQ band-type enum) and band1 is a +2 dB presence bell at 4 kHz. This insert is separate from the strip's built-in eq field.
List the full key set with masteringInsertParamNames('eq.parametric'). Keys outside that list (say, a flat highPassHz) load fine but take no effect, and Mixer.sceneWarnings() reports them after the scene loads.
For one-knob tonal moves, simpler inserts remain: eq.tilt (tiltDb, pivotHz) for a broad bright/dark tilt, and spectral.airBand (amount, shelfFrequencyHz) for a high-shelf "air" lift.
Editing and re-saving
const json = mixingScenePresetJson('vocalReverbSend');
const mixer = Mixer.fromSceneJson(json, 48000, 512);
mixer.sceneWarnings(); // [] unread keys (typos) would be listed here, non-fatally
mixer.addSend(0, 'more-verb', 'vocal-verb', -18, 'postFader'); // topology change
mixer.compile(); // rebuild before timing-critical work
const saved = mixer.toSceneJson(); // round-trips back to the same formatimport libsonare as sonare
scene_json = sonare.mixing_scene_preset_json('vocalReverbSend')
with sonare.Mixer.from_scene_json(scene_json, sample_rate=48000, block_size=512) as mixer:
mixer.scene_warnings() # [] unread keys (typos) would be listed here, non-fatally
mixer.add_send(0, 'more-verb', 'vocal-verb', -18, 'post_fader') # topology change
mixer.compile() # rebuild before timing-critical work
saved = mixer.to_scene_json() # round-trips back to the same format# Export a built-in scene, edit the JSON file, then render it.
sonare mixing-preset --preset vocalReverbSend > my-scene.json
sonare mix --scene my-scene.json --input vocal=vocal.wav -o master.wavmix --scene with per-strip inputs is Python-CLI only
Rendering a whole scene from a JSON file with one --input per strip is implemented by the Python CLI. The native CLI has no mix command at all: its strip command answers to sonare-cli mix-strip and nothing else, so a script that still calls sonare-cli mix fails as an unknown command. mix-strip itself is on both CLIs, but it is a single-strip, single-input processor with no --scene — use it for quick per-strip checks, not full scene renders. See the channel strip.
When to recompile
Structural edits (adding or removing buses, sends, or connections, or re-routing with setOutputBus) mark the graph dirty and need compile() before the next timing-critical block. Parameter moves (setSendDb / Python set_send_db, setPanLaw), VCA group changes (add/remove/gain — applied live as control-only gain offsets on member strips), and scheduled automation do not need a recompile.
Related
- Mixing Demo Project JSON — the
/mixingdemo's UI project file, a per-track arrangement format that is not a mixer scene - Mixing Engine — the API guide and signal flow
- Mixing Assistant —
suggestMixScenewrites a scene in this format from an analysis - Mixing Basics — the vocabulary
- Mastering Processors — valid
processorids for the mastering registry - Effects Inserts — the extra creative-FX mixer insert names
- Binding Parity — per-runtime differences