tono_core/dsl/tracks.rs
1//! Mixer-track types: one channel of a [`Node::Tracks`] root plus its
2//! automation lanes.
3
4use super::{Node, default_gain};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8/// One mixer channel in a [`Node::Tracks`] root.
9#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
10pub struct Track {
11 /// Stable layer id — a short slug like `"kick"` or `"tail"`, unique within
12 /// the document. This is how edits address the track by id, so it never
13 /// shifts when sibling layers are added or
14 /// removed (unlike an array index). Omitted ids are backfilled
15 /// deterministically (`layer_<position>`) on the next build.
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub id: Option<String>,
18 /// The track's signal graph (usually a `seq` or a `chain`).
19 pub node: Node,
20 /// Stereo position, −1 (hard left) .. 1 (hard right). Equal-power law.
21 #[serde(default)]
22 pub pan: f32,
23 /// Channel fader, 0..2 (1 = unity).
24 #[serde(default = "default_gain")]
25 pub gain: f32,
26 /// Start offset in seconds: the rendered layer is shifted this far right
27 /// on the bus (the transient + body + tail recipe). The render keeps its
28 /// full length and the shifted tail is truncated at the document edge.
29 #[serde(default)]
30 pub at: f32,
31 /// Muted layers stay in the document but are left off the bus. This is
32 /// rendered state, not a monitoring convenience — exports ship without
33 /// muted layers.
34 #[serde(default)]
35 pub mute: bool,
36 /// Song-time automation lanes for this track's `gain` / `pan` (volume rides,
37 /// pan moves across sections). Empty ⇒ the static `gain`/`pan` apply and the
38 /// render is byte-identical to a document without this field. A lane's value
39 /// overrides the static one over time; per-node modulators still cover the
40 /// node level (this is the track/song level).
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
42 pub automation: Vec<AutoLane>,
43 /// Sidechain ducking: this track's level dips whenever the `source`
44 /// track's signal is loud — the classic kick→bass pump, at mixer level.
45 /// The source track renders exactly as it does today; only this (the
46 /// follower) track is gain-reduced. None ⇒ the render is byte-identical
47 /// to a document without this field. A sidechained mix streams natively
48 /// (the duck envelope advances per sample, so a schema-v2 `tracks` root
49 /// — sidechains, buses, and all — streams byte-identically to the
50 /// offline bounce).
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub sidechain: Option<Sidechain>,
53 /// The mix bus this track's main output routes to (e.g. `"drums"`,
54 /// `"reverb"`). None ⇒ the master bus, the only behavior documents had
55 /// before this field existed, so they render byte-identically.
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub bus: Option<String>,
58 /// Post-fader sends: copies of this track's positioned stereo signal
59 /// (post fader/pan/duck), each scaled by its amount, into mix buses.
60 /// Empty ⇒ the render is byte-identical to a document without this field.
61 #[serde(default, skip_serializing_if = "Vec::is_empty")]
62 pub sends: Vec<Send>,
63}
64
65/// A mix bus in a [`Node::Tracks`] root: a named submix with its own insert
66/// chain, returned onto the master bus. Tracks route to it with their `bus`
67/// field and feed it with `sends`; the rendered mix without it is
68/// byte-identical, so buses are purely additive.
69#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
70pub struct Bus {
71 /// Stable bus id — a short slug like `"drums"` or `"reverb"`, unique
72 /// within the document (and never colliding with a track id).
73 pub id: String,
74 /// The bus return fader, 0..2 (1 = unity).
75 #[serde(default = "default_gain")]
76 pub gain: f32,
77 /// Insert chain on the bus (stereo processors, applied like the master
78 /// chain — a reverb gets the decorrelated-tails treatment).
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
80 pub effects: Vec<Node>,
81}
82
83/// A post-fader send into a mix bus.
84#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
85pub struct Send {
86 /// The target bus's id.
87 pub bus: String,
88 /// Send level, 0..1 (0 = silent, 1 = the full post-fader signal).
89 #[serde(default = "default_send_amount")]
90 pub amount: f32,
91}
92
93fn default_send_amount() -> f32 {
94 0.5
95}
96
97/// A tracks-level sidechain link: the follower's post-fader signal is
98/// multiplied by a gain envelope driven by the `source` track's signal, with
99/// the same attack/release follower the `duck` node uses (so the pump
100/// character matches). A source must be a plain track — follower-of-follower
101/// chains are rejected by validation (duck directly to the source's source).
102#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
103pub struct Sidechain {
104 /// The id of the track whose signal drives the ducking (e.g. `"kick"`).
105 pub source: String,
106 /// Duck depth, 0..1 (1 = fully silent at the source's peak).
107 #[serde(default = "default_sidechain_amount")]
108 pub amount: f32,
109 /// Gain-reduction attack in seconds.
110 #[serde(default = "default_sidechain_attack")]
111 pub attack: f32,
112 /// Recovery time in seconds (the "pump" length).
113 #[serde(default = "default_sidechain_release")]
114 pub release: f32,
115}
116
117// The defaults mirror the `duck` node's, so moving a pump from inside a node
118// tree to the mixer keeps the same feel.
119fn default_sidechain_amount() -> f32 {
120 0.8
121}
122fn default_sidechain_attack() -> f32 {
123 0.005
124}
125fn default_sidechain_release() -> f32 {
126 0.25
127}
128
129/// What a track automation lane controls.
130#[non_exhaustive]
131#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
132#[serde(rename_all = "lowercase")]
133pub enum AutoTarget {
134 /// The track's channel fader (0..2).
135 Gain,
136 /// The track's stereo position (−1..1).
137 Pan,
138}
139
140/// How an automation lane interpolates between its breakpoints.
141#[non_exhaustive]
142#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
143#[serde(rename_all = "lowercase")]
144pub enum AutoCurve {
145 /// Straight-line segments (default — the only behavior documents had
146 /// before this field existed, so they render byte-identically).
147 #[default]
148 Linear,
149 /// Hold the previous breakpoint's value until the next one lands (a
150 /// stepped ride — fader moves without ramps).
151 Step,
152 /// Exponential approach per segment: `v0 · (v1/v0)^u` while both
153 /// endpoints are positive (natural-feeling swells and fades); any other
154 /// segment falls back to linear — deterministic, and documented.
155 Exp,
156}
157
158/// One breakpoint in an automation lane: value `v` at song time `t` seconds.
159/// Between breakpoints the value follows the lane's `curve`; before the
160/// first / after the last it holds flat.
161#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
162pub struct AutoPoint {
163 /// Song time in seconds.
164 pub t: f32,
165 /// Target value at this time.
166 pub v: f32,
167}
168
169/// A track automation lane: a `target` driven by a list of breakpoints.
170#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
171pub struct AutoLane {
172 /// What this lane controls.
173 pub target: AutoTarget,
174 /// The interpolation between breakpoints (default linear).
175 #[serde(default)]
176 pub curve: AutoCurve,
177 /// Breakpoints over song time.
178 pub points: Vec<AutoPoint>,
179}