atagen

headroom

public
Star0
Watch0
da174e47c3dfe446a07b3dd77fdfcd414277a44e
1
ssh://brewery@pls.lobotomise.me:33/atagen/headroom.git

Blame: crates/headroom-ipc/src/proto.rs

dd6b8e786e1fatagen2026-05-27 15:49 UTC1
//! protocol message types. spec: `IPC.md`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC2
dd6b8e786e1fatagen2026-05-27 15:49 UTC3
use std::collections::BTreeMap;
dd6b8e786e1fatagen2026-05-27 15:49 UTC4
dd6b8e786e1fatagen2026-05-27 15:49 UTC5
use serde::{Deserialize, Serialize};
dd6b8e786e1fatagen2026-05-27 15:49 UTC6
use serde_json::Value;
dd6b8e786e1fatagen2026-05-27 15:49 UTC7
dd6b8e786e1fatagen2026-05-27 15:49 UTC8
use crate::error::ProtoError;
dd6b8e786e1fatagen2026-05-27 15:49 UTC9
dd6b8e786e1fatagen2026-05-27 15:49 UTC10
/// subscription topic.
dd6b8e786e1fatagen2026-05-27 15:49 UTC11
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC12
#[serde(rename_all = "kebab-case")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC13
#[non_exhaustive]
dd6b8e786e1fatagen2026-05-27 15:49 UTC14
pub enum Topic {
dd6b8e786e1fatagen2026-05-27 15:49 UTC15
    /// loudness / peak / GR telemetry.
dd6b8e786e1fatagen2026-05-27 15:49 UTC16
    Meters,
dd6b8e786e1fatagen2026-05-27 15:49 UTC17
    Profile,
dd6b8e786e1fatagen2026-05-27 15:49 UTC18
    Routing,
dd6b8e786e1fatagen2026-05-27 15:49 UTC19
    Daemon,
dd6b8e786e1fatagen2026-05-27 15:49 UTC20
    /// synthetic, for `hello`; clients never subscribe.
dd6b8e786e1fatagen2026-05-27 15:49 UTC21
    Control,
dd6b8e786e1fatagen2026-05-27 15:49 UTC22
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC23
dd6b8e786e1fatagen2026-05-27 15:49 UTC24
impl Topic {
dd6b8e786e1fatagen2026-05-27 15:49 UTC25
    #[must_use]
dd6b8e786e1fatagen2026-05-27 15:49 UTC26
    pub fn as_str(self) -> &'static str {
dd6b8e786e1fatagen2026-05-27 15:49 UTC27
        match self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC28
            Topic::Meters => "meters",
dd6b8e786e1fatagen2026-05-27 15:49 UTC29
            Topic::Profile => "profile",
dd6b8e786e1fatagen2026-05-27 15:49 UTC30
            Topic::Routing => "routing",
dd6b8e786e1fatagen2026-05-27 15:49 UTC31
            Topic::Daemon => "daemon",
dd6b8e786e1fatagen2026-05-27 15:49 UTC32
            Topic::Control => "control",
dd6b8e786e1fatagen2026-05-27 15:49 UTC33
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC34
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC35
dd6b8e786e1fatagen2026-05-27 15:49 UTC36
    /// topics clients may subscribe to (excludes `Control`).
dd6b8e786e1fatagen2026-05-27 15:49 UTC37
    #[must_use]
dd6b8e786e1fatagen2026-05-27 15:49 UTC38
    pub const fn subscribable() -> &'static [Topic] {
dd6b8e786e1fatagen2026-05-27 15:49 UTC39
        &[Topic::Meters, Topic::Profile, Topic::Routing, Topic::Daemon]
dd6b8e786e1fatagen2026-05-27 15:49 UTC40
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC41
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC42
dd6b8e786e1fatagen2026-05-27 15:49 UTC43
impl std::fmt::Display for Topic {
dd6b8e786e1fatagen2026-05-27 15:49 UTC44
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
dd6b8e786e1fatagen2026-05-27 15:49 UTC45
        f.write_str(self.as_str())
dd6b8e786e1fatagen2026-05-27 15:49 UTC46
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC47
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC48
dd6b8e786e1fatagen2026-05-27 15:49 UTC49
/// routing decision for one app or stream.
dd6b8e786e1fatagen2026-05-27 15:49 UTC50
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC51
#[serde(rename_all = "lowercase")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC52
pub enum Route {
dd6b8e786e1fatagen2026-05-27 15:49 UTC53
    /// through the processed (filtered) sink.
dd6b8e786e1fatagen2026-05-27 15:49 UTC54
    Processed,
dd6b8e786e1fatagen2026-05-27 15:49 UTC55
    /// straight to the real hardware sink; no processing, no extra graph hop.
dd6b8e786e1fatagen2026-05-27 15:49 UTC56
    Bypass,
dd6b8e786e1fatagen2026-05-27 15:49 UTC57
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC58
dd6b8e786e1fatagen2026-05-27 15:49 UTC59
impl Route {
dd6b8e786e1fatagen2026-05-27 15:49 UTC60
    #[must_use]
dd6b8e786e1fatagen2026-05-27 15:49 UTC61
    pub fn as_str(self) -> &'static str {
dd6b8e786e1fatagen2026-05-27 15:49 UTC62
        match self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC63
            Route::Processed => "processed",
dd6b8e786e1fatagen2026-05-27 15:49 UTC64
            Route::Bypass => "bypass",
dd6b8e786e1fatagen2026-05-27 15:49 UTC65
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC66
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC67
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC68
dd6b8e786e1fatagen2026-05-27 15:49 UTC69
impl std::fmt::Display for Route {
dd6b8e786e1fatagen2026-05-27 15:49 UTC70
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
dd6b8e786e1fatagen2026-05-27 15:49 UTC71
        f.write_str(self.as_str())
dd6b8e786e1fatagen2026-05-27 15:49 UTC72
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC73
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC74
dd6b8e786e1fatagen2026-05-27 15:49 UTC75
/// all operations. wire form `{ "op": "<name>", "args": <args> }`; arg-less ops omit `args`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC76
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC77
#[serde(tag = "op", content = "args")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC78
#[non_exhaustive]
dd6b8e786e1fatagen2026-05-27 15:49 UTC79
pub enum Op {
dd6b8e786e1fatagen2026-05-27 15:49 UTC80
    #[serde(rename = "status")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC81
    Status,
dd6b8e786e1fatagen2026-05-27 15:49 UTC82
dd6b8e786e1fatagen2026-05-27 15:49 UTC83
    #[serde(rename = "profile.list")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC84
    ProfileList,
dd6b8e786e1fatagen2026-05-27 15:49 UTC85
dd6b8e786e1fatagen2026-05-27 15:49 UTC86
    #[serde(rename = "profile.use")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC87
    ProfileUse { name: String },
dd6b8e786e1fatagen2026-05-27 15:49 UTC88
dd6b8e786e1fatagen2026-05-27 15:49 UTC89
    /// `name` omitted ⇒ active profile.
dd6b8e786e1fatagen2026-05-27 15:49 UTC90
    #[serde(rename = "profile.show")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC91
    ProfileShow {
dd6b8e786e1fatagen2026-05-27 15:49 UTC92
        #[serde(default, skip_serializing_if = "Option::is_none")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC93
        name: Option<String>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC94
    },
dd6b8e786e1fatagen2026-05-27 15:49 UTC95
dd6b8e786e1fatagen2026-05-27 15:49 UTC96
    /// reload all profile files from disk.
dd6b8e786e1fatagen2026-05-27 15:49 UTC97
    #[serde(rename = "profile.reload")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC98
    ProfileReload,
dd6b8e786e1fatagen2026-05-27 15:49 UTC99
dd6b8e786e1fatagen2026-05-27 15:49 UTC100
    #[serde(rename = "route.list")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC101
    RouteList,
dd6b8e786e1fatagen2026-05-27 15:49 UTC102
dd6b8e786e1fatagen2026-05-27 15:49 UTC103
    /// add/replace a persistent routing rule.
dd6b8e786e1fatagen2026-05-27 15:49 UTC104
    #[serde(rename = "route.set")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC105
    RouteSet {
dd6b8e786e1fatagen2026-05-27 15:49 UTC106
        /// typically `application.process.binary`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC107
        app: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC108
        to: Route,
dd6b8e786e1fatagen2026-05-27 15:49 UTC109
    },
dd6b8e786e1fatagen2026-05-27 15:49 UTC110
dd6b8e786e1fatagen2026-05-27 15:49 UTC111
    #[serde(rename = "route.unset")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC112
    RouteUnset { app: String },
dd6b8e786e1fatagen2026-05-27 15:49 UTC113
dd6b8e786e1fatagen2026-05-27 15:49 UTC114
    /// one-shot reroute of a live stream.
dd6b8e786e1fatagen2026-05-27 15:49 UTC115
    #[serde(rename = "route.stream")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC116
    RouteStream { node_id: u32, to: Route },
dd6b8e786e1fatagen2026-05-27 15:49 UTC117
dd6b8e786e1fatagen2026-05-27 15:49 UTC118
    #[serde(rename = "setting.get")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC119
    SettingGet {
dd6b8e786e1fatagen2026-05-27 15:49 UTC120
        /// dotted key, e.g. `compressor.threshold_db`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC121
        key: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC122
    },
dd6b8e786e1fatagen2026-05-27 15:49 UTC123
dd6b8e786e1fatagen2026-05-27 15:49 UTC124
    #[serde(rename = "setting.set")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC125
    SettingSet { key: String, value: Value },
dd6b8e786e1fatagen2026-05-27 15:49 UTC126
dd6b8e786e1fatagen2026-05-27 15:49 UTC127
    #[serde(rename = "setting.list")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC128
    SettingList,
dd6b8e786e1fatagen2026-05-27 15:49 UTC129
dd6b8e786e1fatagen2026-05-27 15:49 UTC130
    /// revert one key to the active profile's value.
dd6b8e786e1fatagen2026-05-27 15:49 UTC131
    #[serde(rename = "setting.clear")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC132
    SettingClear { key: String },
dd6b8e786e1fatagen2026-05-27 15:49 UTC133
dd6b8e786e1fatagen2026-05-27 15:49 UTC134
    /// revert all settings to the active profile; leaves route/per-app/bypass overrides intact.
dd6b8e786e1fatagen2026-05-27 15:49 UTC135
    #[serde(rename = "setting.reset")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC136
    SettingReset,
dd6b8e786e1fatagen2026-05-27 15:49 UTC137
dd6b8e786e1fatagen2026-05-27 15:49 UTC138
    /// global bypass kill switch.
dd6b8e786e1fatagen2026-05-27 15:49 UTC139
    #[serde(rename = "bypass.set")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC140
    BypassSet { enabled: bool },
dd6b8e786e1fatagen2026-05-27 15:49 UTC141
dd6b8e786e1fatagen2026-05-27 15:49 UTC142
    /// per-app (Layer A) controller state for managed streams.
dd6b8e786e1fatagen2026-05-27 15:49 UTC143
    #[serde(rename = "per-app.list")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC144
    LayerAList,
dd6b8e786e1fatagen2026-05-27 15:49 UTC145
dd6b8e786e1fatagen2026-05-27 15:49 UTC146
    /// Layer A enable for one app (persistent overlay override).
dd6b8e786e1fatagen2026-05-27 15:49 UTC147
    #[serde(rename = "per-app.set")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC148
    PerAppSet {
dd6b8e786e1fatagen2026-05-27 15:49 UTC149
        /// process_binary or application_name.
dd6b8e786e1fatagen2026-05-27 15:49 UTC150
        app: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC151
        enabled: bool,
dd6b8e786e1fatagen2026-05-27 15:49 UTC152
    },
dd6b8e786e1fatagen2026-05-27 15:49 UTC153
dd6b8e786e1fatagen2026-05-27 15:49 UTC154
    /// Layer A master switch (persistent overlay override).
dd6b8e786e1fatagen2026-05-27 15:49 UTC155
    #[serde(rename = "per-app.master")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC156
    PerAppMaster { enabled: bool },
dd6b8e786e1fatagen2026-05-27 15:49 UTC157
dd6b8e786e1fatagen2026-05-27 15:49 UTC158
    /// clear a stream's deference lock (user-ceiling / strict-mode) so the controller resumes.
dd6b8e786e1fatagen2026-05-27 15:49 UTC159
    #[serde(rename = "per-app.reset")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC160
    LayerAReset { node_id: u32 },
dd6b8e786e1fatagen2026-05-27 15:49 UTC161
dd6b8e786e1fatagen2026-05-27 15:49 UTC162
    #[serde(rename = "subscribe")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC163
    Subscribe { topics: Vec<Topic> },
dd6b8e786e1fatagen2026-05-27 15:49 UTC164
dd6b8e786e1fatagen2026-05-27 15:49 UTC165
    #[serde(rename = "unsubscribe")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC166
    Unsubscribe { topics: Vec<Topic> },
dd6b8e786e1fatagen2026-05-27 15:49 UTC167
dd6b8e786e1fatagen2026-05-27 15:49 UTC168
    /// optional client protocol handshake
dd6b8e786e1fatagen2026-05-27 15:49 UTC169
    #[serde(rename = "hello")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC170
    Hello { protocol: u32 },
dd6b8e786e1fatagen2026-05-27 15:49 UTC171
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC172
dd6b8e786e1fatagen2026-05-27 15:49 UTC173
impl Op {
dd6b8e786e1fatagen2026-05-27 15:49 UTC174
    /// canonical wire name.
dd6b8e786e1fatagen2026-05-27 15:49 UTC175
    #[must_use]
dd6b8e786e1fatagen2026-05-27 15:49 UTC176
    pub fn name(&self) -> &'static str {
dd6b8e786e1fatagen2026-05-27 15:49 UTC177
        match self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC178
            Op::Status => "status",
dd6b8e786e1fatagen2026-05-27 15:49 UTC179
            Op::ProfileList => "profile.list",
dd6b8e786e1fatagen2026-05-27 15:49 UTC180
            Op::ProfileUse { .. } => "profile.use",
dd6b8e786e1fatagen2026-05-27 15:49 UTC181
            Op::ProfileShow { .. } => "profile.show",
dd6b8e786e1fatagen2026-05-27 15:49 UTC182
            Op::ProfileReload => "profile.reload",
dd6b8e786e1fatagen2026-05-27 15:49 UTC183
            Op::RouteList => "route.list",
dd6b8e786e1fatagen2026-05-27 15:49 UTC184
            Op::RouteSet { .. } => "route.set",
dd6b8e786e1fatagen2026-05-27 15:49 UTC185
            Op::RouteUnset { .. } => "route.unset",
dd6b8e786e1fatagen2026-05-27 15:49 UTC186
            Op::RouteStream { .. } => "route.stream",
dd6b8e786e1fatagen2026-05-27 15:49 UTC187
            Op::SettingGet { .. } => "setting.get",
dd6b8e786e1fatagen2026-05-27 15:49 UTC188
            Op::SettingSet { .. } => "setting.set",
dd6b8e786e1fatagen2026-05-27 15:49 UTC189
            Op::SettingList => "setting.list",
dd6b8e786e1fatagen2026-05-27 15:49 UTC190
            Op::SettingClear { .. } => "setting.clear",
dd6b8e786e1fatagen2026-05-27 15:49 UTC191
            Op::SettingReset => "setting.reset",
dd6b8e786e1fatagen2026-05-27 15:49 UTC192
            Op::BypassSet { .. } => "bypass.set",
dd6b8e786e1fatagen2026-05-27 15:49 UTC193
            Op::LayerAList => "per-app.list",
dd6b8e786e1fatagen2026-05-27 15:49 UTC194
            Op::PerAppSet { .. } => "per-app.set",
dd6b8e786e1fatagen2026-05-27 15:49 UTC195
            Op::PerAppMaster { .. } => "per-app.master",
dd6b8e786e1fatagen2026-05-27 15:49 UTC196
            Op::LayerAReset { .. } => "per-app.reset",
dd6b8e786e1fatagen2026-05-27 15:49 UTC197
            Op::Subscribe { .. } => "subscribe",
dd6b8e786e1fatagen2026-05-27 15:49 UTC198
            Op::Unsubscribe { .. } => "unsubscribe",
dd6b8e786e1fatagen2026-05-27 15:49 UTC199
            Op::Hello { .. } => "hello",
dd6b8e786e1fatagen2026-05-27 15:49 UTC200
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC201
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC202
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC203
dd6b8e786e1fatagen2026-05-27 15:49 UTC204
/// client-to-server request.
dd6b8e786e1fatagen2026-05-27 15:49 UTC205
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC206
pub struct Request {
dd6b8e786e1fatagen2026-05-27 15:49 UTC207
    /// client-chosen; echoed in the paired response.
dd6b8e786e1fatagen2026-05-27 15:49 UTC208
    pub id: u64,
dd6b8e786e1fatagen2026-05-27 15:49 UTC209
    /// flattened: contributes `op` and optional `args` alongside `id`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC210
    #[serde(flatten)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC211
    pub op: Op,
dd6b8e786e1fatagen2026-05-27 15:49 UTC212
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC213
dd6b8e786e1fatagen2026-05-27 15:49 UTC214
impl Request {
dd6b8e786e1fatagen2026-05-27 15:49 UTC215
    #[must_use]
dd6b8e786e1fatagen2026-05-27 15:49 UTC216
    pub fn new(id: u64, op: Op) -> Self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC217
        Self { id, op }
dd6b8e786e1fatagen2026-05-27 15:49 UTC218
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC219
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC220
dd6b8e786e1fatagen2026-05-27 15:49 UTC221
/// server-to-client response, paired to a request by `id`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC222
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC223
pub struct Response {
dd6b8e786e1fatagen2026-05-27 15:49 UTC224
    pub id: u64,
dd6b8e786e1fatagen2026-05-27 15:49 UTC225
    #[serde(flatten)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC226
    pub payload: ResponsePayload,
dd6b8e786e1fatagen2026-05-27 15:49 UTC227
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC228
dd6b8e786e1fatagen2026-05-27 15:49 UTC229
impl Response {
dd6b8e786e1fatagen2026-05-27 15:49 UTC230
    /// # Errors
dd6b8e786e1fatagen2026-05-27 15:49 UTC231
    /// serde error if `value` fails to serialize.
dd6b8e786e1fatagen2026-05-27 15:49 UTC232
    pub fn ok<T: Serialize>(id: u64, value: &T) -> Result<Self, serde_json::Error> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC233
        Ok(Self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC234
            id,
dd6b8e786e1fatagen2026-05-27 15:49 UTC235
            payload: ResponsePayload::Ok {
dd6b8e786e1fatagen2026-05-27 15:49 UTC236
                result: serde_json::to_value(value)?,
dd6b8e786e1fatagen2026-05-27 15:49 UTC237
            },
dd6b8e786e1fatagen2026-05-27 15:49 UTC238
        })
dd6b8e786e1fatagen2026-05-27 15:49 UTC239
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC240
dd6b8e786e1fatagen2026-05-27 15:49 UTC241
    #[must_use]
dd6b8e786e1fatagen2026-05-27 15:49 UTC242
    pub fn err(id: u64, error: ProtoError) -> Self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC243
        Self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC244
            id,
dd6b8e786e1fatagen2026-05-27 15:49 UTC245
            payload: ResponsePayload::Err { error },
dd6b8e786e1fatagen2026-05-27 15:49 UTC246
        }
dd6b8e786e1fatagen2026-05-27 15:49 UTC247
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC248
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC249
dd6b8e786e1fatagen2026-05-27 15:49 UTC250
/// body of a `Response`. on the wire: a single `result` or `error` field inlined alongside `id`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC251
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC252
#[serde(untagged)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC253
pub enum ResponsePayload {
dd6b8e786e1fatagen2026-05-27 15:49 UTC254
    Ok { result: Value },
dd6b8e786e1fatagen2026-05-27 15:49 UTC255
    Err { error: ProtoError },
dd6b8e786e1fatagen2026-05-27 15:49 UTC256
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC257
dd6b8e786e1fatagen2026-05-27 15:49 UTC258
/// server-to-client event on a subscribed topic.
dd6b8e786e1fatagen2026-05-27 15:49 UTC259
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC260
pub struct Event {
dd6b8e786e1fatagen2026-05-27 15:49 UTC261
    /// e.g. `tick`, `changed`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC262
    pub event: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC263
    pub topic: Topic,
dd6b8e786e1fatagen2026-05-27 15:49 UTC264
    /// shape depends on `topic` and `event`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC265
    pub data: Value,
dd6b8e786e1fatagen2026-05-27 15:49 UTC266
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC267
dd6b8e786e1fatagen2026-05-27 15:49 UTC268
impl Event {
dd6b8e786e1fatagen2026-05-27 15:49 UTC269
    /// # Errors
dd6b8e786e1fatagen2026-05-27 15:49 UTC270
    /// serde error if `data` fails to serialize.
dd6b8e786e1fatagen2026-05-27 15:49 UTC271
    pub fn new<T: Serialize>(
dd6b8e786e1fatagen2026-05-27 15:49 UTC272
        topic: Topic,
dd6b8e786e1fatagen2026-05-27 15:49 UTC273
        event: impl Into<String>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC274
        data: &T,
dd6b8e786e1fatagen2026-05-27 15:49 UTC275
    ) -> Result<Self, serde_json::Error> {
dd6b8e786e1fatagen2026-05-27 15:49 UTC276
        Ok(Self {
dd6b8e786e1fatagen2026-05-27 15:49 UTC277
            event: event.into(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC278
            topic,
dd6b8e786e1fatagen2026-05-27 15:49 UTC279
            data: serde_json::to_value(data)?,
dd6b8e786e1fatagen2026-05-27 15:49 UTC280
        })
dd6b8e786e1fatagen2026-05-27 15:49 UTC281
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC282
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC283
dd6b8e786e1fatagen2026-05-27 15:49 UTC284
/// one frame as received from the server: paired response or fan-out event.
dd6b8e786e1fatagen2026-05-27 15:49 UTC285
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC286
#[serde(untagged)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC287
pub enum ServerFrame {
dd6b8e786e1fatagen2026-05-27 15:49 UTC288
    Response(Response),
dd6b8e786e1fatagen2026-05-27 15:49 UTC289
    Event(Event),
dd6b8e786e1fatagen2026-05-27 15:49 UTC290
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC291
dd6b8e786e1fatagen2026-05-27 15:49 UTC292
/// result body of `status`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC293
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC294
pub struct Status {
dd6b8e786e1fatagen2026-05-27 15:49 UTC295
    /// semver.
dd6b8e786e1fatagen2026-05-27 15:49 UTC296
    pub version: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC297
    pub protocol: u32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC298
    pub uptime_s: u64,
dd6b8e786e1fatagen2026-05-27 15:49 UTC299
    pub profile: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC300
    pub bypass: bool,
dd6b8e786e1fatagen2026-05-27 15:49 UTC301
    /// Layer A master switch (per-app level control, global).
dd6b8e786e1fatagen2026-05-27 15:49 UTC302
    #[serde(default)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC303
    pub per_app: bool,
dd6b8e786e1fatagen2026-05-27 15:49 UTC304
    pub sinks: Sinks,
dd6b8e786e1fatagen2026-05-27 15:49 UTC305
    pub streams: Vec<StreamRoute>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC306
    /// per-app (Layer A) controller state; empty when not managing anything.
dd6b8e786e1fatagen2026-05-27 15:49 UTC307
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC308
    pub layer_a: Vec<LayerASnapshot>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC309
    /// non-fatal warnings (typically profile-load: TOML parse errors, active profile missing on disk).
dd6b8e786e1fatagen2026-05-27 15:49 UTC310
    /// reflects the last successful load/reload; empty when healthy.
dd6b8e786e1fatagen2026-05-27 15:49 UTC311
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC312
    pub warnings: Vec<String>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC313
    /// user-overlay setting overrides (dotted key → value). these shadow the active profile and
dd6b8e786e1fatagen2026-05-27 15:49 UTC314
    /// persist across `profile.use`, so a stale `headroom set` can silently override it
dd6b8e786e1fatagen2026-05-27 15:49 UTC315
    /// (e.g. `agc.enabled=false`). surfaced so clients can flag it.
dd6b8e786e1fatagen2026-05-27 15:49 UTC316
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC317
    pub setting_overrides: BTreeMap<String, Value>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC318
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC319
dd6b8e786e1fatagen2026-05-27 15:49 UTC320
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC321
pub struct Sinks {
dd6b8e786e1fatagen2026-05-27 15:49 UTC322
    /// the processed virtual sink; the only sink headroom creates.
dd6b8e786e1fatagen2026-05-27 15:49 UTC323
    pub processed: SinkInfo,
dd6b8e786e1fatagen2026-05-27 15:49 UTC324
    /// hardware sink headroom forwards to, and where bypassed streams go directly.
dd6b8e786e1fatagen2026-05-27 15:49 UTC325
    pub real: SinkInfo,
dd6b8e786e1fatagen2026-05-27 15:49 UTC326
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC327
dd6b8e786e1fatagen2026-05-27 15:49 UTC328
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC329
pub struct SinkInfo {
dd6b8e786e1fatagen2026-05-27 15:49 UTC330
    #[serde(default, skip_serializing_if = "Option::is_none")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC331
    pub node_id: Option<u32>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC332
    #[serde(default, skip_serializing_if = "Option::is_none")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC333
    pub name: Option<String>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC334
    /// linked and accepting audio.
dd6b8e786e1fatagen2026-05-27 15:49 UTC335
    #[serde(default)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC336
    pub ready: bool,
dd6b8e786e1fatagen2026-05-27 15:49 UTC337
    /// native rate (Hz). filter matches the *real* sink's rate to skip the output-edge resample;
dd6b8e786e1fatagen2026-05-27 15:49 UTC338
    /// the processed sink advertises whatever rate the filter currently runs at.
dd6b8e786e1fatagen2026-05-27 15:49 UTC339
    #[serde(default, skip_serializing_if = "Option::is_none")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC340
    pub sample_rate: Option<u32>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC341
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC342
dd6b8e786e1fatagen2026-05-27 15:49 UTC343
/// one playback stream and its route.
dd6b8e786e1fatagen2026-05-27 15:49 UTC344
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC345
pub struct StreamRoute {
dd6b8e786e1fatagen2026-05-27 15:49 UTC346
    pub node_id: u32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC347
    /// typically `application.process.binary`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC348
    pub app: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC349
    pub route: Route,
dd6b8e786e1fatagen2026-05-27 15:49 UTC350
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC351
dd6b8e786e1fatagen2026-05-27 15:49 UTC352
/// per-app (Layer A) controller state for one managed stream; on `status` and `per-app.list`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC353
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC354
pub struct LayerASnapshot {
dd6b8e786e1fatagen2026-05-27 15:49 UTC355
    pub node_id: u32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC356
    pub app: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC357
    /// true while a tap + controller is actively managing the stream.
dd6b8e786e1fatagen2026-05-27 15:49 UTC358
    pub managed: bool,
dd6b8e786e1fatagen2026-05-27 15:49 UTC359
    /// last linear volume written (1.0 = unity).
dd6b8e786e1fatagen2026-05-27 15:49 UTC360
    pub volume_lin: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC361
    /// asserted gain reduction in dB (`>= 0`; `0` = no cut).
dd6b8e786e1fatagen2026-05-27 15:49 UTC362
    pub reduction_db: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC363
    /// user-set ceiling (linear) when ceiling-mode deference is active.
dd6b8e786e1fatagen2026-05-27 15:49 UTC364
    #[serde(default, skip_serializing_if = "Option::is_none")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC365
    pub user_ceiling_lin: Option<f32>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC366
    /// strict-mode deference has locked the controller until `per-app.reset`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC367
    pub deferred: bool,
dd6b8e786e1fatagen2026-05-27 15:49 UTC368
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC369
dd6b8e786e1fatagen2026-05-27 15:49 UTC370
/// entry in `profile.list`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC371
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC372
pub struct ProfileInfo {
dd6b8e786e1fatagen2026-05-27 15:49 UTC373
    pub name: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC374
    pub active: bool,
dd6b8e786e1fatagen2026-05-27 15:49 UTC375
    pub description: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC376
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC377
dd6b8e786e1fatagen2026-05-27 15:49 UTC378
/// result body of `route.list`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC379
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC380
pub struct RouteList {
dd6b8e786e1fatagen2026-05-27 15:49 UTC381
    /// in evaluation order.
dd6b8e786e1fatagen2026-05-27 15:49 UTC382
    pub rules: Vec<RouteRule>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC383
    pub current: Vec<StreamRoute>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC384
    /// fallback when no rule matches.
dd6b8e786e1fatagen2026-05-27 15:49 UTC385
    pub default_route: Route,
dd6b8e786e1fatagen2026-05-27 15:49 UTC386
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC387
dd6b8e786e1fatagen2026-05-27 15:49 UTC388
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC389
pub struct RouteRule {
dd6b8e786e1fatagen2026-05-27 15:49 UTC390
    #[serde(rename = "match")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC391
    pub match_: RouteRuleMatch,
dd6b8e786e1fatagen2026-05-27 15:49 UTC392
    pub route: Route,
dd6b8e786e1fatagen2026-05-27 15:49 UTC393
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC394
dd6b8e786e1fatagen2026-05-27 15:49 UTC395
/// present fields are AND'd; values within a field are OR'd; empty match is true.
dd6b8e786e1fatagen2026-05-27 15:49 UTC396
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC397
pub struct RouteRuleMatch {
dd6b8e786e1fatagen2026-05-27 15:49 UTC398
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC399
    pub process_binary: Vec<String>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC400
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC401
    pub application_name: Vec<String>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC402
    /// `pipewire.access.portal.app_id` (Flatpak).
dd6b8e786e1fatagen2026-05-27 15:49 UTC403
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC404
    pub portal_app_id: Vec<String>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC405
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC406
    pub media_role: Vec<String>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC407
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC408
dd6b8e786e1fatagen2026-05-27 15:49 UTC409
/// `hello` payload (sent on connect).
dd6b8e786e1fatagen2026-05-27 15:49 UTC410
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC411
pub struct HelloData {
dd6b8e786e1fatagen2026-05-27 15:49 UTC412
    /// always `"headroom"`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC413
    pub daemon: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC414
    /// semver.
dd6b8e786e1fatagen2026-05-27 15:49 UTC415
    pub version: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC416
    pub protocol: u32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC417
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC418
dd6b8e786e1fatagen2026-05-27 15:49 UTC419
/// `meters` tick payload.
dd6b8e786e1fatagen2026-05-27 15:49 UTC420
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC421
pub struct MeterTick {
dd6b8e786e1fatagen2026-05-27 15:49 UTC422
    /// BS.1770 M, 400 ms, LUFS.
dd6b8e786e1fatagen2026-05-27 15:49 UTC423
    pub momentary_lufs: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC424
    /// BS.1770 S, 3 s, LUFS.
dd6b8e786e1fatagen2026-05-27 15:49 UTC425
    pub shortterm_lufs: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC426
    /// BS.1770 I, gated, LUFS.
dd6b8e786e1fatagen2026-05-27 15:49 UTC427
    pub integrated_lufs: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC428
    /// max true peak across channels, dBTP.
dd6b8e786e1fatagen2026-05-27 15:49 UTC429
    pub true_peak_dbtp: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC430
    /// compressor + limiter, dB.
dd6b8e786e1fatagen2026-05-27 15:49 UTC431
    pub gain_reduction_db: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC432
    pub compressor_gr_db: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC433
    pub limiter_gr_db: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC434
    /// AGC contribution (positive = boost), dB.
dd6b8e786e1fatagen2026-05-27 15:49 UTC435
    pub agc_gain_db: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC436
    /// when `false`, AGC is bypassed and `agc_gain_db` is inert (0) — render as "disabled",
dd6b8e786e1fatagen2026-05-27 15:49 UTC437
    /// not an active 0 dB. absent ⇒ `true`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC438
    #[serde(default = "default_true")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC439
    pub agc_enabled: bool,
dd6b8e786e1fatagen2026-05-27 15:49 UTC440
    /// same as [`Self::agc_enabled`] for `compressor_gr_db`. (limiter has no flag — always-on backstop.)
dd6b8e786e1fatagen2026-05-27 15:49 UTC441
    #[serde(default = "default_true")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC442
    pub compressor_enabled: bool,
dd6b8e786e1fatagen2026-05-27 15:49 UTC443
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC444
dd6b8e786e1fatagen2026-05-27 15:49 UTC445
/// absent ⇒ enabled, matching the always-on assumption before these fields existed.
dd6b8e786e1fatagen2026-05-27 15:49 UTC446
fn default_true() -> bool {
dd6b8e786e1fatagen2026-05-27 15:49 UTC447
    true
dd6b8e786e1fatagen2026-05-27 15:49 UTC448
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC449
dd6b8e786e1fatagen2026-05-27 15:49 UTC450
/// `profile` topic events.
dd6b8e786e1fatagen2026-05-27 15:49 UTC451
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC452
#[serde(tag = "event", rename_all = "snake_case")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC453
#[non_exhaustive]
dd6b8e786e1fatagen2026-05-27 15:49 UTC454
pub enum ProfileEvent {
dd6b8e786e1fatagen2026-05-27 15:49 UTC455
    Changed { name: String, previous: String },
dd6b8e786e1fatagen2026-05-27 15:49 UTC456
    /// `changed`: profiles whose definitions changed on disk.
dd6b8e786e1fatagen2026-05-27 15:49 UTC457
    Reloaded { changed: Vec<String> },
dd6b8e786e1fatagen2026-05-27 15:49 UTC458
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC459
dd6b8e786e1fatagen2026-05-27 15:49 UTC460
/// `routing` topic events.
dd6b8e786e1fatagen2026-05-27 15:49 UTC461
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC462
#[serde(tag = "event", rename_all = "snake_case")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC463
#[non_exhaustive]
dd6b8e786e1fatagen2026-05-27 15:49 UTC464
pub enum RoutingEvent {
dd6b8e786e1fatagen2026-05-27 15:49 UTC465
    StreamRouted {
dd6b8e786e1fatagen2026-05-27 15:49 UTC466
        node_id: u32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC467
        app: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC468
        to: Route,
dd6b8e786e1fatagen2026-05-27 15:49 UTC469
    },
dd6b8e786e1fatagen2026-05-27 15:49 UTC470
    /// stream's PipeWire node disappeared; drop state keyed by `node_id`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC471
    StreamRemoved { node_id: u32 },
dd6b8e786e1fatagen2026-05-27 15:49 UTC472
    /// Layer A tap attached; daemon now manages `Props.channelVolumes` and publishes `meters/layer_a_level`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC473
    LayerAAttached { node_id: u32, app: String },
dd6b8e786e1fatagen2026-05-27 15:49 UTC474
    /// Layer A tap torn down (usually stream gone); drop Layer A state for `node_id`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC475
    LayerADetached { node_id: u32 },
dd6b8e786e1fatagen2026-05-27 15:49 UTC476
    RuleChanged,
dd6b8e786e1fatagen2026-05-27 15:49 UTC477
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC478
dd6b8e786e1fatagen2026-05-27 15:49 UTC479
/// `meters/layer_a_level` payload — emitted when the Layer A controller writes a new `channelVolumes`.
dd6b8e786e1fatagen2026-05-27 15:49 UTC480
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC481
pub struct LayerALevel {
dd6b8e786e1fatagen2026-05-27 15:49 UTC482
    pub node_id: u32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC483
    pub app: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC484
    /// linear volume written (1.0 = unity).
dd6b8e786e1fatagen2026-05-27 15:49 UTC485
    pub volume_lin: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC486
    /// asserted gain reduction, dB; ≤ 0 when reducing.
dd6b8e786e1fatagen2026-05-27 15:49 UTC487
    pub reduction_db: f32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC488
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC489
dd6b8e786e1fatagen2026-05-27 15:49 UTC490
/// `daemon` topic events.
dd6b8e786e1fatagen2026-05-27 15:49 UTC491
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC492
#[serde(tag = "event", rename_all = "snake_case")]
dd6b8e786e1fatagen2026-05-27 15:49 UTC493
#[non_exhaustive]
dd6b8e786e1fatagen2026-05-27 15:49 UTC494
pub enum DaemonEvent {
dd6b8e786e1fatagen2026-05-27 15:49 UTC495
    Started { version: String },
dd6b8e786e1fatagen2026-05-27 15:49 UTC496
    Shutdown,
dd6b8e786e1fatagen2026-05-27 15:49 UTC497
    /// events dropped on this connection.
dd6b8e786e1fatagen2026-05-27 15:49 UTC498
    Overflow {
dd6b8e786e1fatagen2026-05-27 15:49 UTC499
        lost_topic: Topic,
dd6b8e786e1fatagen2026-05-27 15:49 UTC500
        /// lost in this batch.
dd6b8e786e1fatagen2026-05-27 15:49 UTC501
        lost: u32,
dd6b8e786e1fatagen2026-05-27 15:49 UTC502
        /// total lost on this connection.
dd6b8e786e1fatagen2026-05-27 15:49 UTC503
        total_lost: u64,
dd6b8e786e1fatagen2026-05-27 15:49 UTC504
    },
dd6b8e786e1fatagen2026-05-27 15:49 UTC505
    /// non-fatal error.
dd6b8e786e1fatagen2026-05-27 15:49 UTC506
    Error {
dd6b8e786e1fatagen2026-05-27 15:49 UTC507
        /// matches `ErrorCode` when applicable.
dd6b8e786e1fatagen2026-05-27 15:49 UTC508
        code: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC509
        message: String,
dd6b8e786e1fatagen2026-05-27 15:49 UTC510
    },
dd6b8e786e1fatagen2026-05-27 15:49 UTC511
}
dd6b8e786e1fatagen2026-05-27 15:49 UTC512
dd6b8e786e1fatagen2026-05-27 15:49 UTC513
#[cfg(test)]
dd6b8e786e1fatagen2026-05-27 15:49 UTC514
mod tests {
dd6b8e786e1fatagen2026-05-27 15:49 UTC515
    use super::*;
dd6b8e786e1fatagen2026-05-27 15:49 UTC516
dd6b8e786e1fatagen2026-05-27 15:49 UTC517
    fn roundtrip<T>(v: &T) -> T
dd6b8e786e1fatagen2026-05-27 15:49 UTC518
    where
dd6b8e786e1fatagen2026-05-27 15:49 UTC519
        T: Serialize + for<'de> Deserialize<'de>,
dd6b8e786e1fatagen2026-05-27 15:49 UTC520
    {
dd6b8e786e1fatagen2026-05-27 15:49 UTC521
        let s = serde_json::to_string(v).expect("serialize");
dd6b8e786e1fatagen2026-05-27 15:49 UTC522
        serde_json::from_str(&s).expect("deserialize")
dd6b8e786e1fatagen2026-05-27 15:49 UTC523
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC524
dd6b8e786e1fatagen2026-05-27 15:49 UTC525
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC526
    fn op_status_serializes_without_args() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC527
        let req = Request::new(1, Op::Status);
dd6b8e786e1fatagen2026-05-27 15:49 UTC528
        let s = serde_json::to_string(&req).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC529
        // Must be the flat form, no `kind` wrapper, no `args` field.
dd6b8e786e1fatagen2026-05-27 15:49 UTC530
        assert_eq!(s, r#"{"id":1,"op":"status"}"#);
dd6b8e786e1fatagen2026-05-27 15:49 UTC531
dd6b8e786e1fatagen2026-05-27 15:49 UTC532
        let back: Request = serde_json::from_str(&s).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC533
        assert_eq!(back, req);
dd6b8e786e1fatagen2026-05-27 15:49 UTC534
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC535
dd6b8e786e1fatagen2026-05-27 15:49 UTC536
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC537
    fn op_profile_use_round_trips_flat() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC538
        let req = Request::new(
dd6b8e786e1fatagen2026-05-27 15:49 UTC539
            7,
dd6b8e786e1fatagen2026-05-27 15:49 UTC540
            Op::ProfileUse {
dd6b8e786e1fatagen2026-05-27 15:49 UTC541
                name: "night".into(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC542
            },
dd6b8e786e1fatagen2026-05-27 15:49 UTC543
        );
dd6b8e786e1fatagen2026-05-27 15:49 UTC544
        let s = serde_json::to_string(&req).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC545
        assert_eq!(s, r#"{"id":7,"op":"profile.use","args":{"name":"night"}}"#);
dd6b8e786e1fatagen2026-05-27 15:49 UTC546
dd6b8e786e1fatagen2026-05-27 15:49 UTC547
        let back: Request = serde_json::from_str(&s).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC548
        assert_eq!(back, req);
dd6b8e786e1fatagen2026-05-27 15:49 UTC549
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC550
dd6b8e786e1fatagen2026-05-27 15:49 UTC551
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC552
    fn route_set_serializes_canonical() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC553
        let req = Request::new(
dd6b8e786e1fatagen2026-05-27 15:49 UTC554
            12,
dd6b8e786e1fatagen2026-05-27 15:49 UTC555
            Op::RouteSet {
dd6b8e786e1fatagen2026-05-27 15:49 UTC556
                app: "firefox".into(),
dd6b8e786e1fatagen2026-05-27 15:49 UTC557
                to: Route::Processed,
dd6b8e786e1fatagen2026-05-27 15:49 UTC558
            },
dd6b8e786e1fatagen2026-05-27 15:49 UTC559
        );
dd6b8e786e1fatagen2026-05-27 15:49 UTC560
        let s = serde_json::to_string(&req).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC561
        assert_eq!(
dd6b8e786e1fatagen2026-05-27 15:49 UTC562
            s,
dd6b8e786e1fatagen2026-05-27 15:49 UTC563
            r#"{"id":12,"op":"route.set","args":{"app":"firefox","to":"processed"}}"#
dd6b8e786e1fatagen2026-05-27 15:49 UTC564
        );
dd6b8e786e1fatagen2026-05-27 15:49 UTC565
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC566
dd6b8e786e1fatagen2026-05-27 15:49 UTC567
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC568
    fn response_ok_shape() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC569
        let resp = Response::ok(3, &serde_json::json!({ "name": "default" })).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC570
        let s = serde_json::to_string(&resp).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC571
        assert_eq!(s, r#"{"id":3,"result":{"name":"default"}}"#);
dd6b8e786e1fatagen2026-05-27 15:49 UTC572
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC573
dd6b8e786e1fatagen2026-05-27 15:49 UTC574
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC575
    fn response_err_shape() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC576
        let resp = Response::err(4, ProtoError::new(crate::ErrorCode::NotFound, "missing"));
dd6b8e786e1fatagen2026-05-27 15:49 UTC577
        let s = serde_json::to_string(&resp).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC578
        assert_eq!(
dd6b8e786e1fatagen2026-05-27 15:49 UTC579
            s,
dd6b8e786e1fatagen2026-05-27 15:49 UTC580
            r#"{"id":4,"error":{"code":"NOT_FOUND","message":"missing"}}"#
dd6b8e786e1fatagen2026-05-27 15:49 UTC581
        );
dd6b8e786e1fatagen2026-05-27 15:49 UTC582
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC583
dd6b8e786e1fatagen2026-05-27 15:49 UTC584
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC585
    fn server_frame_distinguishes_response_from_event() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC586
        let resp = Response::ok(1, &serde_json::json!(null)).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC587
        let s = serde_json::to_string(&resp).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC588
        let frame: ServerFrame = serde_json::from_str(&s).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC589
        assert!(matches!(frame, ServerFrame::Response(_)));
dd6b8e786e1fatagen2026-05-27 15:49 UTC590
dd6b8e786e1fatagen2026-05-27 15:49 UTC591
        let ev = Event::new(Topic::Meters, "tick", &serde_json::json!({})).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC592
        let s = serde_json::to_string(&ev).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC593
        let frame: ServerFrame = serde_json::from_str(&s).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC594
        assert!(matches!(frame, ServerFrame::Event(_)));
dd6b8e786e1fatagen2026-05-27 15:49 UTC595
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC596
dd6b8e786e1fatagen2026-05-27 15:49 UTC597
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC598
    fn meter_tick_roundtrip() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC599
        let m = MeterTick {
dd6b8e786e1fatagen2026-05-27 15:49 UTC600
            momentary_lufs: -19.3,
dd6b8e786e1fatagen2026-05-27 15:49 UTC601
            shortterm_lufs: -20.1,
dd6b8e786e1fatagen2026-05-27 15:49 UTC602
            integrated_lufs: -19.8,
dd6b8e786e1fatagen2026-05-27 15:49 UTC603
            true_peak_dbtp: -1.4,
dd6b8e786e1fatagen2026-05-27 15:49 UTC604
            gain_reduction_db: -2.1,
dd6b8e786e1fatagen2026-05-27 15:49 UTC605
            compressor_gr_db: -0.8,
dd6b8e786e1fatagen2026-05-27 15:49 UTC606
            limiter_gr_db: -1.3,
dd6b8e786e1fatagen2026-05-27 15:49 UTC607
            agc_gain_db: 0.5,
dd6b8e786e1fatagen2026-05-27 15:49 UTC608
            agc_enabled: true,
dd6b8e786e1fatagen2026-05-27 15:49 UTC609
            compressor_enabled: true,
dd6b8e786e1fatagen2026-05-27 15:49 UTC610
        };
dd6b8e786e1fatagen2026-05-27 15:49 UTC611
        let back = roundtrip(&m);
dd6b8e786e1fatagen2026-05-27 15:49 UTC612
        assert_eq!(back, m);
dd6b8e786e1fatagen2026-05-27 15:49 UTC613
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC614
dd6b8e786e1fatagen2026-05-27 15:49 UTC615
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC616
    fn topic_string_canonical() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC617
        assert_eq!(Topic::Meters.as_str(), "meters");
dd6b8e786e1fatagen2026-05-27 15:49 UTC618
        assert_eq!(serde_json::to_string(&Topic::Meters).unwrap(), "\"meters\"");
dd6b8e786e1fatagen2026-05-27 15:49 UTC619
        let t: Topic = serde_json::from_str("\"profile\"").unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC620
        assert_eq!(t, Topic::Profile);
dd6b8e786e1fatagen2026-05-27 15:49 UTC621
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC622
dd6b8e786e1fatagen2026-05-27 15:49 UTC623
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC624
    fn route_string_canonical() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC625
        let r: Route = serde_json::from_str("\"bypass\"").unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC626
        assert_eq!(r, Route::Bypass);
dd6b8e786e1fatagen2026-05-27 15:49 UTC627
        assert_eq!(serde_json::to_string(&r).unwrap(), "\"bypass\"");
dd6b8e786e1fatagen2026-05-27 15:49 UTC628
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC629
dd6b8e786e1fatagen2026-05-27 15:49 UTC630
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC631
    fn error_code_screaming_snake() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC632
        let s = serde_json::to_string(&crate::ErrorCode::InvalidFrame).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC633
        assert_eq!(s, "\"INVALID_FRAME\"");
dd6b8e786e1fatagen2026-05-27 15:49 UTC634
        let c: crate::ErrorCode = serde_json::from_str("\"UNKNOWN_OP\"").unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC635
        assert_eq!(c, crate::ErrorCode::UnknownOp);
dd6b8e786e1fatagen2026-05-27 15:49 UTC636
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC637
dd6b8e786e1fatagen2026-05-27 15:49 UTC638
    #[test]
dd6b8e786e1fatagen2026-05-27 15:49 UTC639
    fn subscribe_op_roundtrip() {
dd6b8e786e1fatagen2026-05-27 15:49 UTC640
        let req = Request::new(
dd6b8e786e1fatagen2026-05-27 15:49 UTC641
            5,
dd6b8e786e1fatagen2026-05-27 15:49 UTC642
            Op::Subscribe {
dd6b8e786e1fatagen2026-05-27 15:49 UTC643
                topics: vec![Topic::Meters, Topic::Profile],
dd6b8e786e1fatagen2026-05-27 15:49 UTC644
            },
dd6b8e786e1fatagen2026-05-27 15:49 UTC645
        );
dd6b8e786e1fatagen2026-05-27 15:49 UTC646
        let back: Request = serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap();
dd6b8e786e1fatagen2026-05-27 15:49 UTC647
        assert_eq!(back, req);
dd6b8e786e1fatagen2026-05-27 15:49 UTC648
    }
dd6b8e786e1fatagen2026-05-27 15:49 UTC649
}