Skip to main content

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}