P0: Rust engine, wire daemon and audio-reactive descent surface

This commit is contained in:
Emil
2026-11-01 00:36:04 +03:00
parent cd967cced4
commit db4368efae
68 changed files with 19507 additions and 3 deletions
+340
View File
@@ -0,0 +1,340 @@
//! ZHILA wire protocol, version 1.
//!
//! Text frames carry JSON control messages. Audio meters travel in a compact
//! binary frame (see [`MeterFrame`]) at ~30 Hz, never inside JSON.
use serde::{Deserialize, Serialize};
pub const PROTOCOL_VERSION: u32 = 1;
pub const VEIN_COUNT: usize = 5;
pub const LOOP_STEPS: usize = 16;
pub const STEPS_PER_BAR: usize = 8;
pub const LOOP_BARS: f32 = 2.0;
pub const MIN_BPM: f32 = 40.0;
pub const MAX_BPM: f32 = 220.0;
pub const MIN_GAIN: f32 = 0.0;
pub const MAX_GAIN: f32 = 1.2;
pub const METER_FRAME_KIND: u8 = 1;
/// `kind (1) | count (2) | bar (4) | master (4) | levels (5 * 4)`
pub const METER_FRAME_LEN: usize = 3 + 4 * (2 + VEIN_COUNT);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VeinId {
Bass,
Crack,
Voice,
Noise,
Silt,
}
impl VeinId {
pub const ALL: [VeinId; VEIN_COUNT] = [
VeinId::Bass,
VeinId::Crack,
VeinId::Voice,
VeinId::Noise,
VeinId::Silt,
];
pub const fn index(self) -> usize {
match self {
VeinId::Bass => 0,
VeinId::Crack => 1,
VeinId::Voice => 2,
VeinId::Noise => 3,
VeinId::Silt => 4,
}
}
pub const fn name(self) -> &'static str {
match self {
VeinId::Bass => "bass",
VeinId::Crack => "crack",
VeinId::Voice => "voice",
VeinId::Noise => "noise",
VeinId::Silt => "silt",
}
}
pub const fn from_index(index: usize) -> Option<Self> {
match index {
0 => Some(VeinId::Bass),
1 => Some(VeinId::Crack),
2 => Some(VeinId::Voice),
3 => Some(VeinId::Noise),
4 => Some(VeinId::Silt),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Actor {
Hand,
Api,
Mcp,
System,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage {
Hello {
protocol: u32,
token: String,
client: String,
},
Command {
id: u64,
command: Command,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerMessage {
Hello {
protocol: u32,
session: String,
engine: AudioInfo,
},
Snapshot {
revision: u64,
state: State,
},
Event {
revision: u64,
event: Event,
},
Result {
id: u64,
result: CommandResult,
},
Error {
id: Option<u64>,
code: ErrorCode,
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Command {
GetSnapshot,
Play,
Stop,
Strike { vein: VeinId, velocity: f32 },
SetTempo { bpm: f32 },
SetVeinGain { vein: VeinId, gain: f32 },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CommandResult {
Snapshot { state: State },
State { state: State },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Event {
Transport { playing: bool },
Tempo { bpm: f32 },
VeinGain { vein: VeinId, gain: f32 },
Struck { vein: VeinId, velocity: f32 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
BadRequest,
Unauthorized,
Protocol,
Audio,
Internal,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AudioInfo {
pub running: bool,
pub device: String,
pub sample_rate: u32,
pub buffer_frames: u32,
pub channels: u16,
}
impl AudioInfo {
pub fn offline() -> Self {
Self {
running: false,
device: "none".into(),
sample_rate: 48_000,
buffer_frames: 0,
channels: 2,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct State {
pub revision: u64,
pub transport: TransportState,
pub veins: Vec<VeinState>,
pub pattern: Pattern,
pub audio: AudioInfo,
pub journal_len: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct TransportState {
pub playing: bool,
pub bpm: f32,
pub bar: f32,
pub loop_bars: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VeinState {
pub id: VeinId,
pub name: String,
pub gain: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Pattern {
pub steps: u16,
pub steps_per_bar: u16,
pub rows: Vec<PatternRow>,
}
impl Pattern {
pub fn row(&self, vein: VeinId) -> Option<&PatternRow> {
self.rows.iter().find(|row| row.vein == vein)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PatternRow {
pub vein: VeinId,
/// One velocity per step, 0..=127. Zero means the step is empty.
pub velocities: Vec<u8>,
}
/// A single binary meter frame. Text frames never carry audio-rate data.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MeterFrame {
pub bar: f32,
pub master: f32,
pub veins: [f32; VEIN_COUNT],
}
impl MeterFrame {
pub fn silent() -> Self {
Self {
bar: 1.0,
master: 0.0,
veins: [0.0; VEIN_COUNT],
}
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(METER_FRAME_LEN);
out.push(METER_FRAME_KIND);
out.extend_from_slice(&(VEIN_COUNT as u16).to_le_bytes());
out.extend_from_slice(&self.bar.to_le_bytes());
out.extend_from_slice(&self.master.to_le_bytes());
for level in self.veins {
out.extend_from_slice(&level.to_le_bytes());
}
debug_assert_eq!(out.len(), METER_FRAME_LEN);
out
}
pub fn decode(data: &[u8]) -> Option<Self> {
if data.len() != METER_FRAME_LEN || data[0] != METER_FRAME_KIND {
return None;
}
let count = u16::from_le_bytes([data[1], data[2]]) as usize;
if count != VEIN_COUNT {
return None;
}
let bar = read_f32(data, 3)?;
let master = read_f32(data, 7)?;
let mut veins = [0.0; VEIN_COUNT];
for (index, level) in veins.iter_mut().enumerate() {
*level = read_f32(data, 11 + index * 4)?;
}
Some(Self { bar, master, veins })
}
}
fn read_f32(data: &[u8], offset: usize) -> Option<f32> {
let bytes: [u8; 4] = data.get(offset..offset + 4)?.try_into().ok()?;
Some(f32::from_le_bytes(bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hello_roundtrip() {
let message = ClientMessage::Hello {
protocol: PROTOCOL_VERSION,
token: "dev".into(),
client: "ui".into(),
};
let json = serde_json::to_string(&message).unwrap();
assert!(json.contains("\"type\":\"hello\""));
let back: ClientMessage = serde_json::from_str(&json).unwrap();
assert_eq!(back, message);
}
#[test]
fn command_roundtrip() {
let message = ClientMessage::Command {
id: 7,
command: Command::Strike {
vein: VeinId::Bass,
velocity: 0.8,
},
};
let json = serde_json::to_string(&message).unwrap();
let back: ClientMessage = serde_json::from_str(&json).unwrap();
assert_eq!(back, message);
}
#[test]
fn meter_roundtrip_is_exact() {
let frame = MeterFrame {
bar: 2.75,
master: 0.42,
veins: [0.1, 0.2, 0.3, 0.4, 0.5],
};
let bytes = frame.encode();
assert_eq!(bytes.len(), METER_FRAME_LEN);
let back = MeterFrame::decode(&bytes).expect("valid frame");
assert_eq!(back, frame);
}
#[test]
fn meter_rejects_garbage() {
assert!(MeterFrame::decode(&[]).is_none());
assert!(MeterFrame::decode(&[METER_FRAME_KIND; 4]).is_none());
let mut bytes = MeterFrame::silent().encode();
bytes[0] = 9;
assert!(MeterFrame::decode(&bytes).is_none());
}
#[test]
fn vein_indices_are_stable() {
for (index, vein) in VeinId::ALL.iter().enumerate() {
assert_eq!(vein.index(), index);
assert_eq!(VeinId::from_index(index), Some(*vein));
}
assert_eq!(VeinId::from_index(VEIN_COUNT), None);
}
}