Skip to main content

wavecraft_protocol/
ipc.rs

1//! IPC message contracts for WebView ↔ Rust communication
2//!
3//! This module defines JSON-RPC 2.0 style messages used for bidirectional
4//! communication between the React UI (running in WebView) and the Rust
5//! application logic.
6//!
7//! # Architecture
8//!
9//! - **Request/Response**: UI initiates, Rust responds (e.g., setParameter, getParameter)
10//! - **Notifications**: Rust pushes updates to UI (e.g., parameter changes from host)
11//!
12//! # JSON-RPC 2.0 Compatibility
13//!
14//! Messages follow JSON-RPC 2.0 conventions:
15//! - Requests have `id`, `method`, and `params`
16//! - Responses have `id` and either `result` or `error`
17//! - Notifications have `method` and `params` but no `id`
18
19use serde::{Deserialize, Serialize};
20
21/// Request message sent from UI to Rust
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct IpcRequest {
24    /// JSON-RPC version (always "2.0")
25    pub jsonrpc: String,
26    /// Unique request identifier for matching responses
27    pub id: RequestId,
28    /// Method name to invoke
29    pub method: String,
30    /// Method parameters (method-specific)
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub params: Option<serde_json::Value>,
33}
34
35/// Response message sent from Rust to UI
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct IpcResponse {
38    /// JSON-RPC version (always "2.0")
39    pub jsonrpc: String,
40    /// Request ID this response corresponds to
41    pub id: RequestId,
42    /// Success result (mutually exclusive with error)
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub result: Option<serde_json::Value>,
45    /// Error result (mutually exclusive with result)
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub error: Option<IpcError>,
48}
49
50/// Notification message sent from Rust to UI (no response expected)
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct IpcNotification {
53    /// JSON-RPC version (always "2.0")
54    pub jsonrpc: String,
55    /// Event type
56    pub method: String,
57    /// Event data
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub params: Option<serde_json::Value>,
60}
61
62/// Request ID can be string or number
63#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(untagged)]
65pub enum RequestId {
66    String(String),
67    Number(i64),
68}
69
70/// Error returned in IpcResponse
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct IpcError {
73    /// Error code (see error code constants)
74    pub code: i32,
75    /// Human-readable error message
76    pub message: String,
77    /// Additional error data (optional)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub data: Option<serde_json::Value>,
80}
81
82// ============================================================================
83// Error Codes (JSON-RPC 2.0 standard codes + custom extensions)
84// ============================================================================
85
86/// JSON-RPC parse error (invalid JSON)
87pub const ERROR_PARSE: i32 = -32700;
88/// JSON-RPC invalid request (malformed structure)
89pub const ERROR_INVALID_REQUEST: i32 = -32600;
90/// JSON-RPC method not found
91pub const ERROR_METHOD_NOT_FOUND: i32 = -32601;
92/// JSON-RPC invalid method parameters
93pub const ERROR_INVALID_PARAMS: i32 = -32602;
94/// JSON-RPC internal error
95pub const ERROR_INTERNAL: i32 = -32603;
96
97// Custom application error codes (start at -32000)
98/// Parameter not found
99pub const ERROR_PARAM_NOT_FOUND: i32 = -32000;
100/// Parameter value out of valid range
101pub const ERROR_PARAM_OUT_OF_RANGE: i32 = -32001;
102
103// ============================================================================
104// Method-Specific Types
105// ============================================================================
106
107// ----------------------------------------------------------------------------
108// getParameter
109// ----------------------------------------------------------------------------
110
111/// Parameters for getParameter request
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct GetParameterParams {
114    /// Parameter ID to retrieve
115    pub id: String,
116}
117
118/// Result of getParameter request
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct GetParameterResult {
121    /// Parameter ID
122    pub id: String,
123    /// Current parameter value in the parameter's declared range.
124    pub value: f32,
125}
126
127// ----------------------------------------------------------------------------
128// setParameter
129// ----------------------------------------------------------------------------
130
131/// Parameters for setParameter request
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct SetParameterParams {
134    /// Parameter ID to update
135    pub id: String,
136    /// New parameter value in the parameter's declared range.
137    pub value: f32,
138}
139
140/// Result of setParameter request (empty success)
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct SetParameterResult {}
143
144// ----------------------------------------------------------------------------
145// getAllParameters
146// ----------------------------------------------------------------------------
147
148/// Result of getAllParameters request
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct GetAllParametersResult {
151    /// List of all parameters with their metadata and current values
152    pub parameters: Vec<ParameterInfo>,
153}
154
155/// Information about a single parameter
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct ParameterInfo {
158    /// Parameter ID (unique identifier)
159    pub id: String,
160    /// Human-readable name
161    pub name: String,
162    /// Parameter type (float, bool, enum, etc.)
163    #[serde(rename = "type")]
164    pub param_type: ParameterType,
165    /// Current parameter value in the parameter's declared range.
166    pub value: f32,
167    /// Default parameter value in the parameter's declared range.
168    pub default: f32,
169    /// Minimum value for this parameter.
170    pub min: f32,
171    /// Maximum value for this parameter.
172    pub max: f32,
173    /// Unit suffix for display (e.g., "dB", "%", "Hz")
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub unit: Option<String>,
176    /// Group name for UI organization (e.g., "Input", "Processing", "Output")
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub group: Option<String>,
179    /// Variant labels for enum parameters (e.g., ["Sine", "Square", "Saw", "Triangle"]).
180    /// Only present when `param_type` is `Enum`.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub variants: Option<Vec<String>>,
183}
184
185/// Information about a discovered processor in the signal chain.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct ProcessorInfo {
188    /// Canonical processor ID (snake_case type-derived identifier).
189    pub id: String,
190}
191
192/// Parameter type discriminator
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
194#[serde(rename_all = "lowercase")]
195pub enum ParameterType {
196    Float,
197    Bool,
198    Enum,
199}
200
201// ----------------------------------------------------------------------------
202// Notification: parameterChanged
203// ----------------------------------------------------------------------------
204
205/// Notification sent when a parameter changes (e.g., from host automation)
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ParameterChangedNotification {
208    /// Parameter ID that changed
209    pub id: String,
210    /// New parameter value in the parameter's declared range.
211    pub value: f32,
212}
213
214// ============================================================================
215// Method Name Constants
216// ============================================================================
217
218/// Method: Get single parameter value
219pub const METHOD_GET_PARAMETER: &str = "getParameter";
220/// Method: Set single parameter value
221pub const METHOD_SET_PARAMETER: &str = "setParameter";
222/// Method: Get all parameters with metadata
223pub const METHOD_GET_ALL_PARAMETERS: &str = "getAllParameters";
224/// Method: Get current meter frame (peak/RMS levels)
225pub const METHOD_GET_METER_FRAME: &str = "getMeterFrame";
226/// Method: Get current oscilloscope frame (1024-point waveform)
227pub const METHOD_GET_OSCILLOSCOPE_FRAME: &str = "getOscilloscopeFrame";
228/// Method: Get current audio runtime status
229pub const METHOD_GET_AUDIO_STATUS: &str = "getAudioStatus";
230/// Method: Request resize of editor window
231pub const METHOD_REQUEST_RESIZE: &str = "requestResize";
232/// Method: Register audio client with dev server
233pub const METHOD_REGISTER_AUDIO: &str = "registerAudio";
234/// Notification: Parameter changed (push from Rust to UI)
235pub const NOTIFICATION_PARAMETER_CHANGED: &str = "parameterChanged";
236/// Notification: Meter update from audio binary (push to browser)
237pub const NOTIFICATION_METER_UPDATE: &str = "meterUpdate";
238/// Notification: Audio runtime status changed
239pub const NOTIFICATION_AUDIO_STATUS_CHANGED: &str = "audioStatusChanged";
240
241// ============================================================================
242// Helper Constructors
243// ============================================================================
244
245impl IpcRequest {
246    /// Create a new request
247    pub fn new(
248        id: RequestId,
249        method: impl Into<String>,
250        params: Option<serde_json::Value>,
251    ) -> Self {
252        Self {
253            jsonrpc: "2.0".to_string(),
254            id,
255            method: method.into(),
256            params,
257        }
258    }
259}
260
261impl IpcResponse {
262    /// Create a success response
263    pub fn success(id: RequestId, result: impl Serialize) -> Self {
264        Self {
265            jsonrpc: "2.0".to_string(),
266            id,
267            result: Some(serde_json::to_value(result).unwrap()),
268            error: None,
269        }
270    }
271
272    /// Create an error response
273    pub fn error(id: RequestId, error: IpcError) -> Self {
274        Self {
275            jsonrpc: "2.0".to_string(),
276            id,
277            result: None,
278            error: Some(error),
279        }
280    }
281}
282
283impl IpcNotification {
284    /// Create a new notification
285    pub fn new(method: impl Into<String>, params: impl Serialize) -> Self {
286        Self {
287            jsonrpc: "2.0".to_string(),
288            method: method.into(),
289            params: Some(serde_json::to_value(params).unwrap()),
290        }
291    }
292}
293
294impl IpcError {
295    /// Create a new error
296    pub fn new(code: i32, message: impl Into<String>) -> Self {
297        Self {
298            code,
299            message: message.into(),
300            data: None,
301        }
302    }
303
304    /// Create an error with additional data
305    pub fn with_data(code: i32, message: impl Into<String>, data: impl Serialize) -> Self {
306        Self {
307            code,
308            message: message.into(),
309            data: Some(serde_json::to_value(data).unwrap()),
310        }
311    }
312
313    /// Parse error
314    pub fn parse_error() -> Self {
315        Self::new(ERROR_PARSE, "Parse error")
316    }
317
318    /// Invalid request error
319    pub fn invalid_request(reason: impl Into<String>) -> Self {
320        Self::new(
321            ERROR_INVALID_REQUEST,
322            format!("Invalid request: {}", reason.into()),
323        )
324    }
325
326    /// Method not found error
327    pub fn method_not_found(method: impl AsRef<str>) -> Self {
328        Self::new(
329            ERROR_METHOD_NOT_FOUND,
330            format!("Method not found: {}", method.as_ref()),
331        )
332    }
333
334    /// Invalid params error
335    pub fn invalid_params(reason: impl Into<String>) -> Self {
336        Self::new(
337            ERROR_INVALID_PARAMS,
338            format!("Invalid params: {}", reason.into()),
339        )
340    }
341
342    /// Internal error
343    pub fn internal_error(reason: impl Into<String>) -> Self {
344        Self::new(ERROR_INTERNAL, format!("Internal error: {}", reason.into()))
345    }
346
347    /// Parameter not found error
348    pub fn param_not_found(id: impl AsRef<str>) -> Self {
349        Self::new(
350            ERROR_PARAM_NOT_FOUND,
351            format!("Parameter not found: {}", id.as_ref()),
352        )
353    }
354
355    /// Parameter out of range error
356    pub fn param_out_of_range(id: impl AsRef<str>, value: f32) -> Self {
357        Self::new(
358            ERROR_PARAM_OUT_OF_RANGE,
359            format!("Parameter '{}' value {} out of range", id.as_ref(), value),
360        )
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn test_request_serialization() {
370        let req = IpcRequest::new(
371            RequestId::Number(1),
372            METHOD_GET_PARAMETER,
373            Some(serde_json::json!({"id": "gain"})),
374        );
375
376        let json = serde_json::to_string(&req).unwrap();
377        assert!(json.contains("\"jsonrpc\":\"2.0\""));
378        assert!(json.contains("\"method\":\"getParameter\""));
379    }
380
381    #[test]
382    fn test_response_serialization() {
383        let resp = IpcResponse::success(
384            RequestId::Number(1),
385            GetParameterResult {
386                id: "gain".to_string(),
387                value: 0.5,
388            },
389        );
390
391        let json = serde_json::to_string(&resp).unwrap();
392        assert!(json.contains("\"jsonrpc\":\"2.0\""));
393        assert!(json.contains("\"result\""));
394        assert!(!json.contains("\"error\""));
395    }
396
397    #[test]
398    fn test_error_response() {
399        let resp = IpcResponse::error(
400            RequestId::String("test".to_string()),
401            IpcError::method_not_found("unknownMethod"),
402        );
403
404        let json = serde_json::to_string(&resp).unwrap();
405        assert!(json.contains("\"error\""));
406        assert!(!json.contains("\"result\""));
407    }
408
409    #[test]
410    fn test_notification_serialization() {
411        let notif = IpcNotification::new(
412            NOTIFICATION_PARAMETER_CHANGED,
413            ParameterChangedNotification {
414                id: "gain".to_string(),
415                value: 0.8,
416            },
417        );
418
419        let json = serde_json::to_string(&notif).unwrap();
420        println!("Notification JSON: {}", json);
421        assert!(json.contains("\"jsonrpc\":\"2.0\""));
422        assert!(json.contains("\"method\":\"parameterChanged\""));
423        // The ParameterChangedNotification has an "id" field, which is OK
424        // We're checking that the notification itself doesn't have a request id
425    }
426
427    #[test]
428    fn test_register_audio_serialization() {
429        let req = IpcRequest::new(
430            RequestId::String("audio-1".to_string()),
431            METHOD_REGISTER_AUDIO,
432            Some(serde_json::json!({
433                "client_id": "dev-audio",
434                "sample_rate": 44100.0,
435                "buffer_size": 512
436            })),
437        );
438
439        let json = serde_json::to_string(&req).unwrap();
440        assert!(json.contains("\"method\":\"registerAudio\""));
441        assert!(json.contains("\"sample_rate\":44100"));
442    }
443
444    #[test]
445    fn test_meter_update_notification() {
446        let notif = IpcNotification::new(
447            NOTIFICATION_METER_UPDATE,
448            MeterUpdateNotification {
449                timestamp_us: 1000,
450                left_peak: 0.5,
451                left_rms: 0.3,
452                right_peak: 0.6,
453                right_rms: 0.4,
454            },
455        );
456
457        let json = serde_json::to_string(&notif).unwrap();
458        assert!(json.contains("\"method\":\"meterUpdate\""));
459        assert!(json.contains("\"left_peak\":0.5"));
460    }
461
462    #[test]
463    fn test_audio_status_serialization() {
464        let result = GetAudioStatusResult {
465            status: Some(AudioRuntimeStatus {
466                phase: AudioRuntimePhase::RunningFullDuplex,
467                diagnostic: None,
468                sample_rate: Some(44100.0),
469                buffer_size: Some(512),
470                updated_at_ms: 123,
471            }),
472        };
473
474        let json = serde_json::to_string(&result).expect("status result should serialize");
475        assert!(json.contains("\"phase\":\"runningFullDuplex\""));
476        assert!(json.contains("\"sample_rate\":44100"));
477    }
478
479    #[test]
480    fn test_oscilloscope_frame_serialization() {
481        let result = GetOscilloscopeFrameResult {
482            frame: Some(OscilloscopeFrame {
483                points_l: vec![0.0; 1024],
484                points_r: vec![0.0; 1024],
485                sample_rate: 44100.0,
486                timestamp: 7,
487                no_signal: true,
488                trigger_mode: OscilloscopeTriggerMode::RisingZeroCrossing,
489            }),
490        };
491
492        let json = serde_json::to_string(&result).expect("oscilloscope result should serialize");
493        assert!(json.contains("\"sample_rate\":44100"));
494        assert!(json.contains("\"trigger_mode\":\"risingZeroCrossing\""));
495    }
496
497    #[test]
498    fn parameter_info_with_variants_serializes_correctly() {
499        let info = ParameterInfo {
500            id: "osc_waveform".to_string(),
501            name: "Waveform".to_string(),
502            param_type: ParameterType::Enum,
503            value: 0.0,
504            default: 0.0,
505            min: 0.0,
506            max: 3.0,
507            unit: None,
508            group: None,
509            variants: Some(vec![
510                "Sine".to_string(),
511                "Square".to_string(),
512                "Saw".to_string(),
513                "Triangle".to_string(),
514            ]),
515        };
516
517        let json = serde_json::to_string(&info).expect("parameter info should serialize");
518        assert!(json.contains("\"variants\""));
519
520        let deserialized: ParameterInfo =
521            serde_json::from_str(&json).expect("parameter info should deserialize");
522        assert_eq!(
523            deserialized.variants.expect("variants should exist").len(),
524            4
525        );
526    }
527
528    #[test]
529    fn parameter_info_without_variants_omits_field() {
530        let info = ParameterInfo {
531            id: "gain".to_string(),
532            name: "Gain".to_string(),
533            param_type: ParameterType::Float,
534            value: 0.5,
535            default: 0.5,
536            min: 0.0,
537            max: 1.0,
538            unit: Some("dB".to_string()),
539            group: None,
540            variants: None,
541        };
542
543        let json = serde_json::to_string(&info).expect("parameter info should serialize");
544        assert!(!json.contains("\"variants\""));
545    }
546}
547
548// ============================================================================
549// Metering Types
550// ============================================================================
551
552/// Meter frame data for UI visualization.
553///
554/// All values are in linear scale (not dB).
555#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
556pub struct MeterFrame {
557    /// Left channel peak (linear, 0.0 to 1.0+)
558    pub peak_l: f32,
559    /// Right channel peak (linear, 0.0 to 1.0+)
560    pub peak_r: f32,
561    /// Left channel RMS (linear, 0.0 to 1.0+)
562    pub rms_l: f32,
563    /// Right channel RMS (linear, 0.0 to 1.0+)
564    pub rms_r: f32,
565    /// Sample timestamp (monotonic)
566    pub timestamp: u64,
567}
568
569/// Result for getMeterFrame method
570#[derive(Debug, Clone, Serialize, Deserialize)]
571pub struct GetMeterFrameResult {
572    /// Latest meter frame, or null if no data available
573    pub frame: Option<MeterFrame>,
574}
575
576// ============================================================================
577// Oscilloscope Types
578// ============================================================================
579
580/// Trigger mode for oscilloscope frame alignment.
581#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
582#[serde(rename_all = "camelCase")]
583pub enum OscilloscopeTriggerMode {
584    RisingZeroCrossing,
585}
586
587/// Channel view mode for oscilloscope visualization.
588#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
589#[serde(rename_all = "camelCase")]
590pub enum OscilloscopeChannelView {
591    Overlay,
592    Left,
593    Right,
594}
595
596/// Oscilloscope waveform frame data for UI visualization.
597#[derive(Debug, Clone, Serialize, Deserialize)]
598pub struct OscilloscopeFrame {
599    /// Left channel waveform points (length 1024).
600    pub points_l: Vec<f32>,
601    /// Right channel waveform points (length 1024).
602    pub points_r: Vec<f32>,
603    /// Sample rate in Hz used to capture the frame.
604    pub sample_rate: f32,
605    /// Sample timestamp (monotonic).
606    pub timestamp: u64,
607    /// True when signal amplitude stayed below threshold for full frame.
608    pub no_signal: bool,
609    /// Trigger mode used for alignment.
610    pub trigger_mode: OscilloscopeTriggerMode,
611}
612
613/// Result for getOscilloscopeFrame method.
614#[derive(Debug, Clone, Serialize, Deserialize)]
615pub struct GetOscilloscopeFrameResult {
616    /// Latest oscilloscope frame, or null if no data available.
617    pub frame: Option<OscilloscopeFrame>,
618}
619
620// ----------------------------------------------------------------------------
621// getAudioStatus
622// ----------------------------------------------------------------------------
623
624/// Audio runtime phase as observed by browser dev mode.
625#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
626#[serde(rename_all = "camelCase")]
627pub enum AudioRuntimePhase {
628    Disabled,
629    Initializing,
630    RunningFullDuplex,
631    RunningInputOnly,
632    Degraded,
633    Failed,
634}
635
636/// Structured diagnostic code for audio startup/runtime issues.
637#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
638#[serde(rename_all = "camelCase")]
639pub enum AudioDiagnosticCode {
640    LoaderUnavailable,
641    VtableMissing,
642    ProcessorCreateFailed,
643    NoInputDevice,
644    InputPermissionDenied,
645    NoOutputDevice,
646    StreamStartFailed,
647    Unknown,
648}
649
650/// Optional diagnostic details for the current runtime status.
651#[derive(Debug, Clone, Serialize, Deserialize)]
652pub struct AudioDiagnostic {
653    /// Machine-readable diagnostic code.
654    pub code: AudioDiagnosticCode,
655    /// Human-readable error/diagnostic message.
656    pub message: String,
657    /// Optional actionable hint for the user.
658    #[serde(skip_serializing_if = "Option::is_none")]
659    pub hint: Option<String>,
660}
661
662/// Current audio runtime status for browser dev mode.
663#[derive(Debug, Clone, Serialize, Deserialize)]
664pub struct AudioRuntimeStatus {
665    /// Current runtime phase.
666    pub phase: AudioRuntimePhase,
667    /// Optional startup/runtime diagnostic details.
668    #[serde(skip_serializing_if = "Option::is_none")]
669    pub diagnostic: Option<AudioDiagnostic>,
670    /// Active sample rate when available.
671    #[serde(skip_serializing_if = "Option::is_none")]
672    pub sample_rate: Option<f32>,
673    /// Active audio buffer size when available.
674    #[serde(skip_serializing_if = "Option::is_none")]
675    pub buffer_size: Option<u32>,
676    /// Last update timestamp (milliseconds since UNIX epoch).
677    pub updated_at_ms: u64,
678}
679
680/// Result for getAudioStatus method.
681#[derive(Debug, Clone, Serialize, Deserialize)]
682pub struct GetAudioStatusResult {
683    /// Current status if available on this host.
684    pub status: Option<AudioRuntimeStatus>,
685}
686
687// ----------------------------------------------------------------------------
688// requestResize
689// ----------------------------------------------------------------------------
690
691/// Parameters for requestResize request
692#[derive(Debug, Clone, Serialize, Deserialize)]
693pub struct RequestResizeParams {
694    /// Desired width in logical pixels
695    pub width: u32,
696    /// Desired height in logical pixels
697    pub height: u32,
698}
699
700/// Result of requestResize request
701#[derive(Debug, Clone, Serialize, Deserialize)]
702pub struct RequestResizeResult {
703    /// Whether the host approved the resize
704    pub accepted: bool,
705}
706
707// ----------------------------------------------------------------------------
708// registerAudio
709// ----------------------------------------------------------------------------
710
711/// Parameters for registerAudio request (audio binary → dev server)
712#[derive(Debug, Clone, Serialize, Deserialize)]
713pub struct RegisterAudioParams {
714    /// Unique client identifier
715    pub client_id: String,
716    /// Audio sample rate (e.g., 44100.0)
717    pub sample_rate: f32,
718    /// Buffer size in samples
719    pub buffer_size: u32,
720}
721
722/// Result of registerAudio request
723#[derive(Debug, Clone, Serialize, Deserialize)]
724pub struct RegisterAudioResult {
725    /// Acknowledgment message
726    pub status: String,
727}
728
729// ----------------------------------------------------------------------------
730// Notification: meterUpdate
731// ----------------------------------------------------------------------------
732
733/// Notification sent from audio binary to browser via dev server
734#[derive(Debug, Clone, Serialize, Deserialize)]
735pub struct MeterUpdateNotification {
736    /// Timestamp in microseconds
737    pub timestamp_us: u64,
738    /// Left channel peak (linear scale)
739    pub left_peak: f32,
740    /// Left channel RMS (linear scale)
741    pub left_rms: f32,
742    /// Right channel peak (linear scale)
743    pub right_peak: f32,
744    /// Right channel RMS (linear scale)
745    pub right_rms: f32,
746}