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}
180
181/// Information about a discovered processor in the signal chain.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct ProcessorInfo {
184    /// Canonical processor ID (snake_case type-derived identifier).
185    pub id: String,
186}
187
188/// Parameter type discriminator
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(rename_all = "lowercase")]
191pub enum ParameterType {
192    Float,
193    Bool,
194    Enum,
195}
196
197// ----------------------------------------------------------------------------
198// Notification: parameterChanged
199// ----------------------------------------------------------------------------
200
201/// Notification sent when a parameter changes (e.g., from host automation)
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ParameterChangedNotification {
204    /// Parameter ID that changed
205    pub id: String,
206    /// New parameter value in the parameter's declared range.
207    pub value: f32,
208}
209
210// ============================================================================
211// Method Name Constants
212// ============================================================================
213
214/// Method: Get single parameter value
215pub const METHOD_GET_PARAMETER: &str = "getParameter";
216/// Method: Set single parameter value
217pub const METHOD_SET_PARAMETER: &str = "setParameter";
218/// Method: Get all parameters with metadata
219pub const METHOD_GET_ALL_PARAMETERS: &str = "getAllParameters";
220/// Method: Get current meter frame (peak/RMS levels)
221pub const METHOD_GET_METER_FRAME: &str = "getMeterFrame";
222/// Method: Get current oscilloscope frame (1024-point waveform)
223pub const METHOD_GET_OSCILLOSCOPE_FRAME: &str = "getOscilloscopeFrame";
224/// Method: Get current audio runtime status
225pub const METHOD_GET_AUDIO_STATUS: &str = "getAudioStatus";
226/// Method: Request resize of editor window
227pub const METHOD_REQUEST_RESIZE: &str = "requestResize";
228/// Method: Register audio client with dev server
229pub const METHOD_REGISTER_AUDIO: &str = "registerAudio";
230/// Notification: Parameter changed (push from Rust to UI)
231pub const NOTIFICATION_PARAMETER_CHANGED: &str = "parameterChanged";
232/// Notification: Meter update from audio binary (push to browser)
233pub const NOTIFICATION_METER_UPDATE: &str = "meterUpdate";
234/// Notification: Audio runtime status changed
235pub const NOTIFICATION_AUDIO_STATUS_CHANGED: &str = "audioStatusChanged";
236
237// ============================================================================
238// Helper Constructors
239// ============================================================================
240
241impl IpcRequest {
242    /// Create a new request
243    pub fn new(
244        id: RequestId,
245        method: impl Into<String>,
246        params: Option<serde_json::Value>,
247    ) -> Self {
248        Self {
249            jsonrpc: "2.0".to_string(),
250            id,
251            method: method.into(),
252            params,
253        }
254    }
255}
256
257impl IpcResponse {
258    /// Create a success response
259    pub fn success(id: RequestId, result: impl Serialize) -> Self {
260        Self {
261            jsonrpc: "2.0".to_string(),
262            id,
263            result: Some(serde_json::to_value(result).unwrap()),
264            error: None,
265        }
266    }
267
268    /// Create an error response
269    pub fn error(id: RequestId, error: IpcError) -> Self {
270        Self {
271            jsonrpc: "2.0".to_string(),
272            id,
273            result: None,
274            error: Some(error),
275        }
276    }
277}
278
279impl IpcNotification {
280    /// Create a new notification
281    pub fn new(method: impl Into<String>, params: impl Serialize) -> Self {
282        Self {
283            jsonrpc: "2.0".to_string(),
284            method: method.into(),
285            params: Some(serde_json::to_value(params).unwrap()),
286        }
287    }
288}
289
290impl IpcError {
291    /// Create a new error
292    pub fn new(code: i32, message: impl Into<String>) -> Self {
293        Self {
294            code,
295            message: message.into(),
296            data: None,
297        }
298    }
299
300    /// Create an error with additional data
301    pub fn with_data(code: i32, message: impl Into<String>, data: impl Serialize) -> Self {
302        Self {
303            code,
304            message: message.into(),
305            data: Some(serde_json::to_value(data).unwrap()),
306        }
307    }
308
309    /// Parse error
310    pub fn parse_error() -> Self {
311        Self::new(ERROR_PARSE, "Parse error")
312    }
313
314    /// Invalid request error
315    pub fn invalid_request(reason: impl Into<String>) -> Self {
316        Self::new(
317            ERROR_INVALID_REQUEST,
318            format!("Invalid request: {}", reason.into()),
319        )
320    }
321
322    /// Method not found error
323    pub fn method_not_found(method: impl AsRef<str>) -> Self {
324        Self::new(
325            ERROR_METHOD_NOT_FOUND,
326            format!("Method not found: {}", method.as_ref()),
327        )
328    }
329
330    /// Invalid params error
331    pub fn invalid_params(reason: impl Into<String>) -> Self {
332        Self::new(
333            ERROR_INVALID_PARAMS,
334            format!("Invalid params: {}", reason.into()),
335        )
336    }
337
338    /// Internal error
339    pub fn internal_error(reason: impl Into<String>) -> Self {
340        Self::new(ERROR_INTERNAL, format!("Internal error: {}", reason.into()))
341    }
342
343    /// Parameter not found error
344    pub fn param_not_found(id: impl AsRef<str>) -> Self {
345        Self::new(
346            ERROR_PARAM_NOT_FOUND,
347            format!("Parameter not found: {}", id.as_ref()),
348        )
349    }
350
351    /// Parameter out of range error
352    pub fn param_out_of_range(id: impl AsRef<str>, value: f32) -> Self {
353        Self::new(
354            ERROR_PARAM_OUT_OF_RANGE,
355            format!("Parameter '{}' value {} out of range", id.as_ref(), value),
356        )
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_request_serialization() {
366        let req = IpcRequest::new(
367            RequestId::Number(1),
368            METHOD_GET_PARAMETER,
369            Some(serde_json::json!({"id": "gain"})),
370        );
371
372        let json = serde_json::to_string(&req).unwrap();
373        assert!(json.contains("\"jsonrpc\":\"2.0\""));
374        assert!(json.contains("\"method\":\"getParameter\""));
375    }
376
377    #[test]
378    fn test_response_serialization() {
379        let resp = IpcResponse::success(
380            RequestId::Number(1),
381            GetParameterResult {
382                id: "gain".to_string(),
383                value: 0.5,
384            },
385        );
386
387        let json = serde_json::to_string(&resp).unwrap();
388        assert!(json.contains("\"jsonrpc\":\"2.0\""));
389        assert!(json.contains("\"result\""));
390        assert!(!json.contains("\"error\""));
391    }
392
393    #[test]
394    fn test_error_response() {
395        let resp = IpcResponse::error(
396            RequestId::String("test".to_string()),
397            IpcError::method_not_found("unknownMethod"),
398        );
399
400        let json = serde_json::to_string(&resp).unwrap();
401        assert!(json.contains("\"error\""));
402        assert!(!json.contains("\"result\""));
403    }
404
405    #[test]
406    fn test_notification_serialization() {
407        let notif = IpcNotification::new(
408            NOTIFICATION_PARAMETER_CHANGED,
409            ParameterChangedNotification {
410                id: "gain".to_string(),
411                value: 0.8,
412            },
413        );
414
415        let json = serde_json::to_string(&notif).unwrap();
416        println!("Notification JSON: {}", json);
417        assert!(json.contains("\"jsonrpc\":\"2.0\""));
418        assert!(json.contains("\"method\":\"parameterChanged\""));
419        // The ParameterChangedNotification has an "id" field, which is OK
420        // We're checking that the notification itself doesn't have a request id
421    }
422
423    #[test]
424    fn test_register_audio_serialization() {
425        let req = IpcRequest::new(
426            RequestId::String("audio-1".to_string()),
427            METHOD_REGISTER_AUDIO,
428            Some(serde_json::json!({
429                "client_id": "dev-audio",
430                "sample_rate": 44100.0,
431                "buffer_size": 512
432            })),
433        );
434
435        let json = serde_json::to_string(&req).unwrap();
436        assert!(json.contains("\"method\":\"registerAudio\""));
437        assert!(json.contains("\"sample_rate\":44100"));
438    }
439
440    #[test]
441    fn test_meter_update_notification() {
442        let notif = IpcNotification::new(
443            NOTIFICATION_METER_UPDATE,
444            MeterUpdateNotification {
445                timestamp_us: 1000,
446                left_peak: 0.5,
447                left_rms: 0.3,
448                right_peak: 0.6,
449                right_rms: 0.4,
450            },
451        );
452
453        let json = serde_json::to_string(&notif).unwrap();
454        assert!(json.contains("\"method\":\"meterUpdate\""));
455        assert!(json.contains("\"left_peak\":0.5"));
456    }
457
458    #[test]
459    fn test_audio_status_serialization() {
460        let result = GetAudioStatusResult {
461            status: Some(AudioRuntimeStatus {
462                phase: AudioRuntimePhase::RunningFullDuplex,
463                diagnostic: None,
464                sample_rate: Some(44100.0),
465                buffer_size: Some(512),
466                updated_at_ms: 123,
467            }),
468        };
469
470        let json = serde_json::to_string(&result).expect("status result should serialize");
471        assert!(json.contains("\"phase\":\"runningFullDuplex\""));
472        assert!(json.contains("\"sample_rate\":44100"));
473    }
474
475    #[test]
476    fn test_oscilloscope_frame_serialization() {
477        let result = GetOscilloscopeFrameResult {
478            frame: Some(OscilloscopeFrame {
479                points_l: vec![0.0; 1024],
480                points_r: vec![0.0; 1024],
481                sample_rate: 44100.0,
482                timestamp: 7,
483                no_signal: true,
484                trigger_mode: OscilloscopeTriggerMode::RisingZeroCrossing,
485            }),
486        };
487
488        let json = serde_json::to_string(&result).expect("oscilloscope result should serialize");
489        assert!(json.contains("\"sample_rate\":44100"));
490        assert!(json.contains("\"trigger_mode\":\"risingZeroCrossing\""));
491    }
492}
493
494// ============================================================================
495// Metering Types
496// ============================================================================
497
498/// Meter frame data for UI visualization.
499///
500/// All values are in linear scale (not dB).
501#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
502pub struct MeterFrame {
503    /// Left channel peak (linear, 0.0 to 1.0+)
504    pub peak_l: f32,
505    /// Right channel peak (linear, 0.0 to 1.0+)
506    pub peak_r: f32,
507    /// Left channel RMS (linear, 0.0 to 1.0+)
508    pub rms_l: f32,
509    /// Right channel RMS (linear, 0.0 to 1.0+)
510    pub rms_r: f32,
511    /// Sample timestamp (monotonic)
512    pub timestamp: u64,
513}
514
515/// Result for getMeterFrame method
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct GetMeterFrameResult {
518    /// Latest meter frame, or null if no data available
519    pub frame: Option<MeterFrame>,
520}
521
522// ============================================================================
523// Oscilloscope Types
524// ============================================================================
525
526/// Trigger mode for oscilloscope frame alignment.
527#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
528#[serde(rename_all = "camelCase")]
529pub enum OscilloscopeTriggerMode {
530    RisingZeroCrossing,
531}
532
533/// Channel view mode for oscilloscope visualization.
534#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
535#[serde(rename_all = "camelCase")]
536pub enum OscilloscopeChannelView {
537    Overlay,
538    Left,
539    Right,
540}
541
542/// Oscilloscope waveform frame data for UI visualization.
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct OscilloscopeFrame {
545    /// Left channel waveform points (length 1024).
546    pub points_l: Vec<f32>,
547    /// Right channel waveform points (length 1024).
548    pub points_r: Vec<f32>,
549    /// Sample rate in Hz used to capture the frame.
550    pub sample_rate: f32,
551    /// Sample timestamp (monotonic).
552    pub timestamp: u64,
553    /// True when signal amplitude stayed below threshold for full frame.
554    pub no_signal: bool,
555    /// Trigger mode used for alignment.
556    pub trigger_mode: OscilloscopeTriggerMode,
557}
558
559/// Result for getOscilloscopeFrame method.
560#[derive(Debug, Clone, Serialize, Deserialize)]
561pub struct GetOscilloscopeFrameResult {
562    /// Latest oscilloscope frame, or null if no data available.
563    pub frame: Option<OscilloscopeFrame>,
564}
565
566// ----------------------------------------------------------------------------
567// getAudioStatus
568// ----------------------------------------------------------------------------
569
570/// Audio runtime phase as observed by browser dev mode.
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
572#[serde(rename_all = "camelCase")]
573pub enum AudioRuntimePhase {
574    Disabled,
575    Initializing,
576    RunningFullDuplex,
577    RunningInputOnly,
578    Degraded,
579    Failed,
580}
581
582/// Structured diagnostic code for audio startup/runtime issues.
583#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
584#[serde(rename_all = "camelCase")]
585pub enum AudioDiagnosticCode {
586    LoaderUnavailable,
587    VtableMissing,
588    ProcessorCreateFailed,
589    NoInputDevice,
590    InputPermissionDenied,
591    NoOutputDevice,
592    StreamStartFailed,
593    Unknown,
594}
595
596/// Optional diagnostic details for the current runtime status.
597#[derive(Debug, Clone, Serialize, Deserialize)]
598pub struct AudioDiagnostic {
599    /// Machine-readable diagnostic code.
600    pub code: AudioDiagnosticCode,
601    /// Human-readable error/diagnostic message.
602    pub message: String,
603    /// Optional actionable hint for the user.
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub hint: Option<String>,
606}
607
608/// Current audio runtime status for browser dev mode.
609#[derive(Debug, Clone, Serialize, Deserialize)]
610pub struct AudioRuntimeStatus {
611    /// Current runtime phase.
612    pub phase: AudioRuntimePhase,
613    /// Optional startup/runtime diagnostic details.
614    #[serde(skip_serializing_if = "Option::is_none")]
615    pub diagnostic: Option<AudioDiagnostic>,
616    /// Active sample rate when available.
617    #[serde(skip_serializing_if = "Option::is_none")]
618    pub sample_rate: Option<f32>,
619    /// Active audio buffer size when available.
620    #[serde(skip_serializing_if = "Option::is_none")]
621    pub buffer_size: Option<u32>,
622    /// Last update timestamp (milliseconds since UNIX epoch).
623    pub updated_at_ms: u64,
624}
625
626/// Result for getAudioStatus method.
627#[derive(Debug, Clone, Serialize, Deserialize)]
628pub struct GetAudioStatusResult {
629    /// Current status if available on this host.
630    pub status: Option<AudioRuntimeStatus>,
631}
632
633// ----------------------------------------------------------------------------
634// requestResize
635// ----------------------------------------------------------------------------
636
637/// Parameters for requestResize request
638#[derive(Debug, Clone, Serialize, Deserialize)]
639pub struct RequestResizeParams {
640    /// Desired width in logical pixels
641    pub width: u32,
642    /// Desired height in logical pixels
643    pub height: u32,
644}
645
646/// Result of requestResize request
647#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct RequestResizeResult {
649    /// Whether the host approved the resize
650    pub accepted: bool,
651}
652
653// ----------------------------------------------------------------------------
654// registerAudio
655// ----------------------------------------------------------------------------
656
657/// Parameters for registerAudio request (audio binary → dev server)
658#[derive(Debug, Clone, Serialize, Deserialize)]
659pub struct RegisterAudioParams {
660    /// Unique client identifier
661    pub client_id: String,
662    /// Audio sample rate (e.g., 44100.0)
663    pub sample_rate: f32,
664    /// Buffer size in samples
665    pub buffer_size: u32,
666}
667
668/// Result of registerAudio request
669#[derive(Debug, Clone, Serialize, Deserialize)]
670pub struct RegisterAudioResult {
671    /// Acknowledgment message
672    pub status: String,
673}
674
675// ----------------------------------------------------------------------------
676// Notification: meterUpdate
677// ----------------------------------------------------------------------------
678
679/// Notification sent from audio binary to browser via dev server
680#[derive(Debug, Clone, Serialize, Deserialize)]
681pub struct MeterUpdateNotification {
682    /// Timestamp in microseconds
683    pub timestamp_us: u64,
684    /// Left channel peak (linear scale)
685    pub left_peak: f32,
686    /// Left channel RMS (linear scale)
687    pub left_rms: f32,
688    /// Right channel peak (linear scale)
689    pub right_peak: f32,
690    /// Right channel RMS (linear scale)
691    pub right_rms: f32,
692}