bbx_net/
message.rs

1//! Network message types for audio control.
2//!
3//! Provides `NetMessage` for parameter changes and triggers, and `NetEvent`
4//! for sample-accurate timing within audio buffers.
5
6use crate::{address::NodeId, clock::SyncedTimestamp};
7
8/// Network message types for audio control.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10#[repr(C)]
11pub enum NetMessageType {
12    /// Parameter value change (float value).
13    ParameterChange = 0,
14    /// Trigger event (momentary, no value).
15    Trigger = 1,
16    /// Request current state (server responds with current values).
17    StateRequest = 2,
18    /// State response from server.
19    StateResponse = 3,
20    /// Ping for latency measurement.
21    Ping = 4,
22    /// Pong response with server timestamp.
23    Pong = 5,
24}
25
26/// Payload data for network messages.
27///
28/// Type-safe discriminated union that can carry different data types
29/// while maintaining `Copy` semantics for lock-free buffers.
30#[repr(C)]
31#[derive(Debug, Clone, Copy, Default)]
32pub enum NetPayload {
33    /// No payload data.
34    #[default]
35    None,
36    /// Single float value (for parameter changes).
37    Value(f32),
38    /// 2D coordinates (for spatial triggers).
39    Coordinates { x: f32, y: f32 },
40}
41
42impl NetPayload {
43    /// Extract coordinates if this payload contains them.
44    pub fn coordinates(&self) -> Option<(f32, f32)> {
45        match self {
46            NetPayload::Coordinates { x, y } => Some((*x, *y)),
47            _ => None,
48        }
49    }
50
51    /// Extract single value if this payload contains one.
52    pub fn value(&self) -> Option<f32> {
53        match self {
54            NetPayload::Value(v) => Some(*v),
55            _ => None,
56        }
57    }
58}
59
60/// A network message for audio control.
61///
62/// Uses `#[repr(C)]` for FFI compatibility.
63#[repr(C)]
64#[derive(Debug, Clone, Copy)]
65pub struct NetMessage {
66    /// The type of message.
67    pub message_type: NetMessageType,
68    /// Target parameter name hash (FNV-1a for fast lookup).
69    pub param_hash: u32,
70    /// Message payload data.
71    pub payload: NetPayload,
72    /// Source node identifier.
73    pub node_id: NodeId,
74    /// Scheduled timestamp (synced clock).
75    pub timestamp: SyncedTimestamp,
76}
77
78impl NetMessage {
79    /// Create a new parameter change message.
80    pub fn param_change(param_name: &str, value: f32, node_id: NodeId) -> Self {
81        Self {
82            message_type: NetMessageType::ParameterChange,
83            param_hash: hash_param_name(param_name),
84            payload: NetPayload::Value(value),
85            node_id,
86            timestamp: SyncedTimestamp::default(),
87        }
88    }
89
90    /// Create a new trigger message.
91    pub fn trigger(trigger_name: &str, node_id: NodeId) -> Self {
92        Self {
93            message_type: NetMessageType::Trigger,
94            param_hash: hash_param_name(trigger_name),
95            payload: NetPayload::None,
96            node_id,
97            timestamp: SyncedTimestamp::default(),
98        }
99    }
100
101    /// Create a trigger with 2D coordinates.
102    pub fn trigger_with_coordinates(trigger_name: &str, x: f32, y: f32, node_id: NodeId) -> Self {
103        Self {
104            message_type: NetMessageType::Trigger,
105            param_hash: hash_param_name(trigger_name),
106            payload: NetPayload::Coordinates { x, y },
107            node_id,
108            timestamp: SyncedTimestamp::default(),
109        }
110    }
111
112    /// Create a ping message for latency measurement.
113    pub fn ping(node_id: NodeId, timestamp: SyncedTimestamp) -> Self {
114        Self {
115            message_type: NetMessageType::Ping,
116            param_hash: 0,
117            payload: NetPayload::None,
118            node_id,
119            timestamp,
120        }
121    }
122
123    /// Create a pong response message.
124    pub fn pong(node_id: NodeId, timestamp: SyncedTimestamp) -> Self {
125        Self {
126            message_type: NetMessageType::Pong,
127            param_hash: 0,
128            payload: NetPayload::None,
129            node_id,
130            timestamp,
131        }
132    }
133
134    /// Set the timestamp for scheduled delivery.
135    pub fn with_timestamp(mut self, timestamp: SyncedTimestamp) -> Self {
136        self.timestamp = timestamp;
137        self
138    }
139}
140
141/// A network event with sample-accurate timing for audio buffer processing.
142///
143/// Combines a network message with a sample offset indicating when the event
144/// should be processed within the current audio buffer.
145#[repr(C)]
146#[derive(Debug, Clone, Copy)]
147pub struct NetEvent {
148    /// The network message data.
149    pub message: NetMessage,
150    /// Sample offset within the current buffer (0 to buffer_size - 1).
151    pub sample_offset: u32,
152}
153
154impl NetEvent {
155    /// Create a new network event with sample offset.
156    pub fn new(message: NetMessage, sample_offset: u32) -> Self {
157        Self { message, sample_offset }
158    }
159
160    /// Create an event to be processed at the start of the buffer.
161    pub fn immediate(message: NetMessage) -> Self {
162        Self {
163            message,
164            sample_offset: 0,
165        }
166    }
167}
168
169/// FNV-1a hash for fast parameter name lookup.
170///
171/// This hash function is fast and produces good distribution for short strings.
172/// Used to avoid string comparisons in the audio thread hot path.
173#[inline]
174pub fn hash_param_name(name: &str) -> u32 {
175    const FNV_OFFSET: u32 = 2166136261;
176    const FNV_PRIME: u32 = 16777619;
177
178    let mut hash = FNV_OFFSET;
179    for byte in name.bytes() {
180        hash ^= byte as u32;
181        hash = hash.wrapping_mul(FNV_PRIME);
182    }
183    hash
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_hash_param_name_deterministic() {
192        let hash1 = hash_param_name("gain");
193        let hash2 = hash_param_name("gain");
194        assert_eq!(hash1, hash2);
195    }
196
197    #[test]
198    fn test_hash_param_name_different_strings() {
199        let hash1 = hash_param_name("gain");
200        let hash2 = hash_param_name("volume");
201        assert_ne!(hash1, hash2);
202    }
203
204    #[test]
205    fn test_hash_param_name_similar_strings() {
206        let hash1 = hash_param_name("param1");
207        let hash2 = hash_param_name("param2");
208        assert_ne!(hash1, hash2);
209    }
210
211    #[test]
212    fn test_net_message_param_change() {
213        let node_id = NodeId::from_parts(1, 2);
214        let msg = NetMessage::param_change("gain", 0.5, node_id);
215
216        assert_eq!(msg.message_type, NetMessageType::ParameterChange);
217        assert_eq!(msg.param_hash, hash_param_name("gain"));
218        assert!((msg.payload.value().unwrap() - 0.5).abs() < f32::EPSILON);
219        assert_eq!(msg.node_id, node_id);
220    }
221
222    #[test]
223    fn test_net_message_trigger() {
224        let node_id = NodeId::from_parts(3, 4);
225        let msg = NetMessage::trigger("start", node_id);
226
227        assert_eq!(msg.message_type, NetMessageType::Trigger);
228        assert_eq!(msg.param_hash, hash_param_name("start"));
229        assert!(matches!(msg.payload, NetPayload::None));
230    }
231
232    #[test]
233    fn test_net_message_trigger_with_coordinates() {
234        let node_id = NodeId::from_parts(5, 6);
235        let msg = NetMessage::trigger_with_coordinates("droplet", 0.25, 0.75, node_id);
236
237        assert_eq!(msg.message_type, NetMessageType::Trigger);
238        assert_eq!(msg.param_hash, hash_param_name("droplet"));
239        let (x, y) = msg.payload.coordinates().unwrap();
240        assert!((x - 0.25).abs() < f32::EPSILON);
241        assert!((y - 0.75).abs() < f32::EPSILON);
242    }
243
244    #[test]
245    fn test_net_event_immediate() {
246        let node_id = NodeId::from_parts(5, 6);
247        let msg = NetMessage::param_change("freq", 440.0, node_id);
248        let event = NetEvent::immediate(msg);
249
250        assert_eq!(event.sample_offset, 0);
251    }
252
253    #[test]
254    fn test_net_event_with_offset() {
255        let node_id = NodeId::from_parts(7, 8);
256        let msg = NetMessage::param_change("pan", 0.0, node_id);
257        let event = NetEvent::new(msg, 256);
258
259        assert_eq!(event.sample_offset, 256);
260    }
261}