syntax = "proto3"; // Shared contract between the UI (Ngs.AuraMixer.App.Client) and the future engine host // (Ngs.AuraMixer.EngineHost). Every method / event on IAudioEngineService has a home here. // // Design notes: // • Each service groups a coherent domain — clients compose the ones they need. // • Setters return Empty; the engine emits DiagnosticsService.StreamSignals for errors that // don't warrant a synchronous ack (UI is fire-and-forget for realtime knob drags). // • Meter/level updates ride server-side streams (StreamLevels / StreamLufs / StreamGr / // StreamSpectrum) — the historical Action<> events on IAudioEngineService fold into these. // • Bulk snapshots (ApplyChannelSnapshot / ApplyBusSnapshot) are Stage 1.2 — deliberately not // here yet. When Refresh() fan-out becomes a latency problem in Stage 2, we add them without // breaking the per-parameter setters. // // Full architecture: docs/architecture/ENGINE_ISOLATION.md option csharp_namespace = "Ngs.AuraMixer.Api.Proto"; package auramixer; import "google/protobuf/empty.proto"; // ============================================================ // TRANSPORT — engine lifecycle & status // ============================================================ service TransportService { rpc Start(google.protobuf.Empty) returns (google.protobuf.Empty); rpc Stop(google.protobuf.Empty) returns (google.protobuf.Empty); rpc Refresh(google.protobuf.Empty) returns (google.protobuf.Empty); rpc ApplySettings(ApplySettingsRequest) returns (google.protobuf.Empty); rpc GetStatus(google.protobuf.Empty) returns (EngineStatus); rpc ResetDiagnosticCounters(google.protobuf.Empty) returns (google.protobuf.Empty); // Streams IsRunning transitions. Replaces IAudioEngineService.EngineRunStateChanged. rpc StreamRunState(google.protobuf.Empty) returns (stream RunStateEvent); // Replaces the App-side ChannelModel list on the engine so Start can build // ChannelSource[] from it. Client calls this in Refresh, before Start. rpc SyncActiveChannels(SyncActiveChannelsRequest) returns (google.protobuf.Empty); } message SyncActiveChannelsRequest { // Order matters — the index in this list becomes the DSP bus index the engine // assigns to each channel. The client sends channels in the same order the UI // sees them (state.Channels.Take(limit)). repeated ChannelSyncEntry channels = 1; // Stage 2d-3a — output buses + their sends. // // Order matters here too: the position in `outputs` becomes the output bus // index the engine uses when configuring the MixMatrix. MainBus MUST be first // (bus index 0), matching AudioHostService.MainBusMatrixIdx. repeated OutputSyncEntry outputs = 2; // Channel-to-output routing (post-fader sends). The engine looks up each // channel_id + output_id against `channels` / `outputs` positions to derive // bus indices, then writes MixMatrix.SetSend(chIdx, busIdx, gainLinear). repeated SendSyncEntry sends = 3; // Bus-to-bus routing (one output bus feeding another) — mirrors AudioEngineService.Refresh's // own "zero every route then re-apply active ones" block. Previously this had NO sync // representation at all — only the realtime SetBusRouteDb RPC existed, which the Graph/Matrix // toggle-on/off UI never actually calls (only OutputSendsTab's gain slider does), so a route // created via drag-to-connect never reached the engine until this field was added. repeated BusRouteSyncEntry bus_routes = 4; // Engine SDK Phase 1 (docs/architecture/ENGINE_SDK_ARCHITECTURE.md) — Owner-only. Opaque to the // transport layer; handed to IEnginePolicyProvider verbatim. Aura's build verifies it as an ES256 // token and derives Free/Premium quota from it independently of anything the client claims; a // partner's UnrestrictedPolicyProvider build never reads this field. Empty string = no token // (falls back to Free quota under AuraLicensePolicyProvider, same as an invalid/expired one). string license_token = 5; } message OutputSyncEntry { string id = 1; // Optional friendly name — engine only uses it for logs; the effective bus // index is the position in the outputs list. string name = 2; // WASAPI endpoint id the bus feeds into. For MainBus (index 0) this becomes // the engine's render device — matches AudioEngineService.SyncBusOutputAssignments // where MainBus.HwOutput drives the main WASAPI render endpoint. string hw_output = 3; // "phys" / "loop" / "virt" / "vinput" → HwOutput is a real WASAPI endpoint the // engine should attach as a secondary render for this bus (non-Main only). // "app" / anything else → engine spawns/uses AuraBusOutput.exe process instead // (Stage 2d-3d — process output wiring). string type = 4; // Stage 2d-3d — bus fader/gain/mute snapshot, reapplied on every sync (mirrors // AudioEngineService.Refresh's "Set OutputFaderNodes: FaderDb + mute" block). float fader_db = 5; bool muted = 6; float gain_db = 7; // pre-EQ output trim (OutputGainNode) // Stage 2d-3f — output bus EQ + Limiter. Same EqBandSync shape channels use (identical // BiquadEqNode/EqBandModel on both sides — no separate "bus EQ" type). Limiter only // exposes ceiling + release; LookaheadMs/TruePeak/Isp are OutputModel fields the in-proc // engine never reads either (aspirational UI-only fields), so they aren't synced. bool eq_enabled = 8; repeated EqBandSync eq_bands = 9; bool lim_enabled = 10; float lim_thresh_db = 11; float lim_release_ms = 12; // Delay compensation + drift correction. Mirrors AudioEngineService's OutputDelayNode // (SetOutputDelayRealtime/SetOutputDriftRealtime) — structural fields reapplied on every // sync, same contract as the EQ/Limiter block above. float delay_ms = 13; bool delay_enabled = 14; string drift_correction = 15; // "auto" / "manual" float drift_manual_ppm = 16; // Volume Normalization (B-92) — LufsNormalizerNode. Premium gate is enforced client-side // (GrpcAudioEngineClient checks IPremiumEntitlement.LufsEnabled before forwarding the // enabled flag), same pattern as the rest of the LUFS surface — mirrors // MeteringServiceImpl's own "gate is enforced client-side, not here" doc comment. bool norm_enabled = 17; float norm_target_lufs = 18; float norm_headroom_dbtp = 19; } message SendSyncEntry { string channel_id = 1; string output_id = 2; // dB value (before conversion to linear gain). float db = 3; // false → send silenced (mixer gain forced to 0), regardless of `db`. bool active = 4; // Pre-fader tap. Not yet honoured server-side (Stage 2d-3b will map it into // MixMatrix.SetPreSend when the FaderNode arrives); today all sends behave // post-fader because there is no FaderNode to be pre-of. bool pre_fader = 5; } message BusRouteSyncEntry { string src_id = 1; string dst_id = 2; // false → route silenced (bus-route gain forced to 0), regardless of `db`. Mirrors // SendSyncEntry.active — a route can exist (SendDb set) without being active. bool active = 3; float db = 4; } message ChannelSyncEntry { string id = 1; // "phys" / "virt" / "loop" / "apploop" — same tokens as ChannelModel.Type. string type = 2; // WASAPI endpoint ID for phys / virt / loop; empty for apploop. string hw_source = 3; // apploop only — the currently-known PID (may be 0 if the target is not // running; engine keeps app_loop_process_name and reconnects on appearance). int32 process_id = 4; string app_loop_process_name = 5; bool app_loop_include_tree = 6; // Native channel count of the source (2 stereo, 6 5.1, 8 7.1). int32 native_channels = 7; uint32 channel_mask = 8; // Stage 2d-3c — per-channel DSP param snapshot (InputGain → Fader → Pan chain). // Mirrors ChannelModel's own fields; the engine reapplies these to the channel's DSP // slot on every sync, same contract as AudioEngineService.ApplyChannelParams. float fader_db = 9; bool muted = 10; bool soloed = 11; float pan = 12; // UI range [-100, +100] float input_gain_db = 13; bool pad = 14; // Stage 2d-3e — Gate. Internal bandpass sidechain only (gate_sc_*); cross-channel // sidechain routing is out of scope (mirrors AudioEngineService's own limitation). bool gate_enabled = 15; float gate_thresh = 16; float gate_atk = 17; float gate_rel = 18; float gate_hold = 19; float gate_damping = 20; float gate_hyst = 21; bool gate_sc_enabled = 22; float gate_sc_frequency = 23; float gate_sc_q = 24; // Stage 2d-3e — Compressor. comp_sc_* covers only the internal HP-filtered detection // path; CompScSource == "external" (cross-channel sidechain) is not wired yet. bool comp_enabled = 25; float comp_thresh = 26; float comp_ratio = 27; float comp_atk = 28; float comp_rel = 29; float comp_knee = 30; float comp_makeup = 31; float comp_mix = 32; // UI range [0, 100] string comp_detect_mode = 33; // "RMS" / "True Peak" / "Peak" bool comp_sc_enabled = 34; float comp_sc_hp_filter = 35; // Stage 2d-3e — EQ. Always present in the chain (self-bypasses with 0 active bands); // eq_enabled ANDs with each band's own enabled flag, same as BiquadEqNode's contract. bool eq_enabled = 36; repeated EqBandSync eq_bands = 37; // Stage 3 slice A — ChannelMode / PhaseInvert / per-channel clip-protection Limiter. Fixed, // non-reorderable head/tail nodes (mirrors AudioEngineService.BuildChain): ChannelMode + // PhaseInvert sit before InputGain, Limiter sits after Comp/EQ but before Fader. Only // "mono"/"stereo-mono" activate ChannelModeNode here — "5.1"/"7.1" map to a safe Stereo // no-op until the full multichannel/HRTF fold lands (Stage 3 slice B). string channel_mode = 38; // "stereo" / "mono" / "stereo-mono" / "5.1" / "7.1" bool phase_invert = 39; string clip_protection = 40; // "lim03" / "lim10" / "hard" / "off" // Stage 3 slice B1 — StereoSpatial + PointSourceHrtf (stereo/mono channels only; the full // multichannel HRTF fold + SOFA/Immersive profiles are slice B2). spatial_hrtf_model is // ALREADY license-resolved by the client before it's sent here (falls back to "Generic" at // Free tier — mirrors AudioEngineService.EffectiveHrtfModel), so the engine never needs its // own license awareness for this field. bool spatial_enabled = 41; string spatial_mode = 42; // "Stereo" / "Binaural HRTF" string spatial_hrtf_model = 43; // "Generic" / "Enhanced" / "Custom" (Immersive not yet reachable server-side) string spatial_hrtf_wav_file = 44; // Custom model only — filename under %LocalAppData%\AuraMixer\hrir\{wav,sofa} float spatial_hrtf_treble_db = 45; // Custom model only — realtime binaural high-shelf float spatial_width = 46; // UI range [0, 200] % float spatial_haas_ms = 47; float spatial_ms_ratio = 48; // UI range [0, 100] % float spatial_x = 49; // UI range [-100, 100] → azimuth [-180, 180]° float spatial_elevation = 50; // accepted, not yet used (all HRIR positions are horizontal) float spatial_distance = 51; // metres, >= 0.1 float spatial_attenuation = 52; // UI range [0, 100] % // Stage 3 slice B2 — full multichannel HRTF fold (5.1/7.1 sources through HrtfFoldNode). // Per-speaker gain trim applied before convolution (0.0 = mute, 1.0 = 0 dB); index order // matches HrirDatabase speaker order (0=FL 1=FR 2=C 3=LFE 4=BL 5=BR 6=SL 7=SR). Always // length 8; indices beyond the channel's effective count (5.1 → 6/7) are ignored. repeated float speaker_gains = 53 [packed = true]; // Stage 3 slice B2 follow-up — which bundled Immersive profile (AppHrirProfiles id, e.g. // "D2") when spatial_hrtf_model == "Immersive". Only meaningful for that model; the engine // resolves the actual HRIR data out-of-band via ChannelsService.UploadHrirPairs (see there // for why) rather than loading the bundled asset itself. string spatial_hrtf_profile = 54; // Fixed head node bypass — mirrors phase_invert/clip_protection (structural, no dedicated // realtime setter; toggled via a fresh SyncActiveChannels → Rebuild pass, same as Gate/Comp/EQ // enable flags). Default true client-side so existing projects behave unchanged. bool gain_enabled = 55; // Custom FX-graph edges (drag-to-connect editor) for the reorderable "middle" of the chain — // mirrors ChannelModel.Edges/GraphEdgeData. Only the 4 system nodes (sys-gain/sys-gate/sys-eq/ // sys-comp) are meaningful server-side today — FX inserts aren't implemented out-of-proc at // all, so any edge referencing an insert id is silently dropped by the same auto-heal the // resolver already has for a disabled/bypassed node (see EngineChannelGraphService). repeated FxEdgeSync edges = 56; // User-added FX insert nodes (Reverb/Delay/Chorus/…) — mirrors ChannelModel.FxInserts / // FxInsertData. Params use the same display-unit key→float shape as the client model; the // engine converts to internal DSP units the same way AudioEngineService.ApplyChannelParams // does. Gated by Premium client-side (mirrors the LUFS/Normalization pattern) — an empty list // here for a Free-tier user is what actually enforces the gate, not anything server-side. repeated FxInsertSync fx_inserts = 57; // Player (v0.6.0-beta, PLAYER_RECORDER_PLAN.md Stage 2) — type == "player" decodes this file // instead of capturing a device. Empty path = no file loaded yet (bus stays silent, same // admittance rule as apploop's empty app_loop_process_name / phys's empty hw_source). The // decoded file's own channel count/layout drives the capture shape, same as a physical // device's negotiated format does — native_channels/channel_mask above are NOT used for this // type (there is no device to detect ahead of time; the engine reads them straight off the // file's own header the moment it opens it). string player_file_path = 58; bool player_loop = 59; } message FxEdgeSync { string from_id = 1; // canvas id, e.g. "sys-gain" string to_id = 2; // canvas id, e.g. "sys-comp" } message FxInsertSync { string id = 1; string type = 2; // "reverb"/"delay"/"loudness"/"gain"/"expander"/"dcremove"/"chorus"/ // "flanger"/"phaser"/"deesser"/"noisesup"/"limiter"/"eq"/"graphiceq"/"pitch" bool bypassed = 3; map params = 4; // display-unit key→value, e.g. "wet"->30, "time"->250 // Canvas X — tie-breaker fallback ordering when no edges connect this insert to anything // (mirrors FxInsertData.CanvasLeft / ResolveMiddleOrder's X tie-break for the system nodes). float canvas_left = 5; } message EqBandSync { bool enabled = 1; string type = 2; // "bell" / "ls" / "hs" / "hp" / "lp" / "notch" — ChannelModel.EqBandModel tokens float hz = 3; float gain = 4; float q = 5; } message ApplySettingsRequest { // Empty string clears the assignment (use system default). string render_device_id = 1; } message EngineStatus { bool is_running = 1; int32 frame_count = 2; int32 sample_rate = 3; float dsp_cpu_percent = 4; int32 capture_overruns = 5; int32 render_underruns = 6; } message RunStateEvent { bool is_running = 1; } // ============================================================ // DEVICES — enumeration + live-change stream // ============================================================ service DevicesService { rpc GetCaptureDevices(google.protobuf.Empty) returns (DeviceList); rpc GetRenderDevices(google.protobuf.Empty) returns (DeviceList); rpc GetActiveAudioProcesses(google.protobuf.Empty) returns (ProcessList); rpc DetectDeviceChannels(DeviceIdRequest) returns (DeviceChannelInfo); // Player file equivalent of DetectDeviceChannels above (v0.6.0-beta, PLAYER_RECORDER_PLAN.md // Stage 4) — probes a file's own header instead of a WASAPI device's negotiated MixFormat. // Reuses DeviceChannelInfo verbatim; same "falls back to (2, 0x3) on any failure" contract. rpc DetectPlayerFileChannels(PlayerFilePathRequest) returns (DeviceChannelInfo); // Merges IAudioEngineService.DeviceListChanged and AppLoopbackProcessesChanged into // one stream — callers filter by kind. rpc StreamDeviceEvents(google.protobuf.Empty) returns (stream DeviceEvent); } message DeviceList { repeated AudioDevice devices = 1; } message AudioDevice { string id = 1; string friendly_name = 2; } message ProcessList { repeated AudioProcess processes = 1; } message AudioProcess { int32 process_id = 1; string process_name = 2; string display_name = 3; } message DeviceIdRequest { string device_id = 1; } message PlayerFilePathRequest { string file_path = 1; } message DeviceChannelInfo { int32 channels = 1; uint32 mask = 2; } message DeviceEvent { enum Kind { KIND_UNSPECIFIED = 0; DEVICE_LIST_CHANGED = 1; APP_LOOPBACK_PROCESSES_CHANGED = 2; } Kind kind = 1; } // ============================================================ // CHANNELS — per-parameter realtime setters + per-channel reads // ============================================================ service ChannelsService { rpc SetFader(ChannelDbRequest) returns (google.protobuf.Empty); rpc SetInputGain(ChannelDbRequest) returns (google.protobuf.Empty); rpc SetPad(ChannelBoolRequest) returns (google.protobuf.Empty); rpc SetPan(ChannelFloatRequest) returns (google.protobuf.Empty); rpc SetGateParam(ChannelKeyValueRequest) returns (google.protobuf.Empty); rpc SetCompParam(ChannelKeyValueRequest) returns (google.protobuf.Empty); rpc SetFxInsertParam(FxInsertParamRequest) returns (google.protobuf.Empty); rpc SetSpeakerGains(SpeakerGainsRequest) returns (google.protobuf.Empty); rpc UploadHrirPairs(HrirPairsRequest) returns (google.protobuf.Empty); rpc SetTrebleSoften(ChannelFloatRequest) returns (google.protobuf.Empty); rpc SetStereoSpatial(StereoSpatialRequest) returns (google.protobuf.Empty); rpc SetPointSource(PointSourceRequest) returns (google.protobuf.Empty); // Automation "duck on signal" — a SEPARATE gain stage from SetFader, applied post-fader // (see EngineChannelGraphService's ChannelDspSlot.DuckGainNode doc comment). Never touches // the channel's saved FaderDb, so it survives every regular sync untouched and can't fight // with the fader the user actually set. rpc SetDuckGain(ChannelDbRequest) returns (google.protobuf.Empty); rpc GetCaptureModeInfo(ChannelIdRequest) returns (CaptureModeInfo); rpc GetCaptureSampleRate(ChannelIdRequest) returns (SampleRateInfo); rpc GetAppLoopbackStatus(ChannelIdRequest) returns (AppLoopbackStatus); // Player transport (v0.6.0-beta, PLAYER_RECORDER_PLAN.md Stage 3). No-ops (or return an empty/ // absent status) for a channel that isn't Type=="player" or has no capture slot — same // "graceful no-op, not an error" contract as every other channel-id-keyed RPC on this service. rpc PlayerPlay(ChannelIdRequest) returns (google.protobuf.Empty); rpc PlayerPause(ChannelIdRequest) returns (google.protobuf.Empty); rpc PlayerStop(ChannelIdRequest) returns (google.protobuf.Empty); rpc PlayerSeek(PlayerSeekRequest) returns (google.protobuf.Empty); rpc GetPlayerStatus(ChannelIdRequest) returns (PlayerStatusInfo); } message PlayerSeekRequest { string channel_id = 1; int64 position_ms = 2; } message PlayerStatusInfo { // False mirrors AppLoopbackStatus.present's own contract: engine not running, channel not // found, wrong Type, or the file failed to open. bool present = 1; string state = 2; // "stopped" / "playing" / "paused" int64 position_ms = 3; int64 duration_ms = 4; } message ChannelIdRequest { string channel_id = 1; } message ChannelDbRequest { string channel_id = 1; float db = 2; } message ChannelBoolRequest { string channel_id = 1; bool value = 2; } message ChannelFloatRequest { string channel_id = 1; float value = 2; } message ChannelKeyValueRequest { string channel_id = 1; string key = 2; float value = 3; } message FxInsertParamRequest { string channel_id = 1; string insert_id = 2; string key = 3; float value = 4; } message SpeakerGainsRequest { string channel_id = 1; repeated float gains = 2 [packed = true]; } // Pushes an already-extracted HRIR set (Custom .sofa import or a bundled Immersive profile) to // the engine, keyed by the same string EngineChannelGraphService uses for HrtfModelKey // ("Custom:" / "Immersive:") — NOT by channel id, since multiple channels // sharing the same source share one cached HrirDatabase server-side. Decision (2026-07-22): // SOFA parsing (PureHDF) and the bundled Immersive assets (MAUI FileSystem.OpenAppPackageFileAsync) // stay App-only — the extraction happens client-side, exactly as it already does for the in-proc // engine, and only the resulting float data crosses the wire. This was chosen over relocating the // HDF5 dependency into EngineHost: SOFA files are USER-IMPORTED (untrusted input), and parsing // them belongs in the same trust boundary as the file picker that accepted them, not in the // leaner out-of-proc engine. hrir_l/hrir_r are flattened SpeakerCount(8) x taps, speaker-major // (all taps of speaker 0, then speaker 1, ...) — LFE's slot is present but ignored server-side // (HrirDatabase.FromHrirPairs forces it to a bypass unit impulse). message HrirPairsRequest { string key = 1; int32 taps = 2; repeated float hrir_l = 3 [packed = true]; repeated float hrir_r = 4 [packed = true]; } message StereoSpatialRequest { string channel_id = 1; float width = 2; float haas_ms = 3; float ms_ratio = 4; } message PointSourceRequest { string channel_id = 1; float azimuth = 2; float elevation = 3; float distance = 4; float attenuation = 5; } message CaptureModeInfo { string mode = 1; // "exclusive" / "shared-fallback" / "shared" / "loopback" / "apploop" / "none" } message SampleRateInfo { int32 sample_rate = 1; } message AppLoopbackStatus { // False mirrors IAudioEngineService returning null (engine stopped or channel has no slot). bool present = 1; bool attached = 2; int32 process_id = 3; } // ============================================================ // BUSES — output-bus setters + monitor gain // ============================================================ service BusesService { rpc SetOutputFader(OutputDbRequest) returns (google.protobuf.Empty); rpc SetOutputGain(OutputDbRequest) returns (google.protobuf.Empty); rpc SetOutputMute(OutputBoolRequest) returns (google.protobuf.Empty); rpc SetOutputDelay(OutputDelayRequest) returns (google.protobuf.Empty); rpc SetOutputLimParam(OutputKeyValueRequest) returns (google.protobuf.Empty); rpc SetOutputNormalizationEnabled(OutputBoolRequest) returns (google.protobuf.Empty); rpc SetOutputNormalizationTarget(OutputFloatRequest) returns (google.protobuf.Empty); rpc SetOutputNormalizationHeadroom(OutputFloatRequest) returns (google.protobuf.Empty); rpc GetOutputNormalizationGainDb(OutputIdRequest) returns (FloatValue); rpc SetOutputDrift(OutputDriftRequest) returns (google.protobuf.Empty); rpc ApplyBusOutput(BusDeviceRequest) returns (google.protobuf.Empty); rpc SetBusProcessOutput(BusProcessOutputRequest) returns (google.protobuf.Empty); rpc SetMonitorGain(FloatValue) returns (google.protobuf.Empty); } message OutputIdRequest { string output_id = 1; } message OutputDbRequest { string output_id = 1; float db = 2; } message OutputBoolRequest { string output_id = 1; bool value = 2; } message OutputFloatRequest { string output_id = 1; float value = 2; } message OutputKeyValueRequest { string output_id = 1; string key = 2; float value = 3; } message OutputDelayRequest { string output_id = 1; float ms = 2; bool enabled = 3; } message OutputDriftRequest { string output_id = 1; string mode = 2; float manual_ppm = 3; } message BusDeviceRequest { string output_id = 1; // Empty string clears the assignment. string device_id = 2; } message BusProcessOutputRequest { string output_id = 1; string device_id = 2; string bus_name = 3; bool enabled = 4; } message FloatValue { float value = 1; } // ============================================================ // ROUTING — sends + bus-to-bus + VCA // ============================================================ service RoutingService { rpc SetSendDb(SendDbRequest) returns (google.protobuf.Empty); rpc SetBusRouteDb(BusRouteDbRequest) returns (google.protobuf.Empty); rpc SetVcaGain(VcaGainRequest) returns (google.protobuf.Empty); } message SendDbRequest { string channel_id = 1; string output_id = 2; float db = 3; } message BusRouteDbRequest { string src_id = 1; string dst_id = 2; float db = 3; } message VcaGainRequest { string group_id = 1; float db = 2; } // ============================================================ // EQ — shared surface (channel or bus is picked by entity_id) // ============================================================ service EqService { rpc SetEqBand(EqBandRequest) returns (google.protobuf.Empty); } message EqBandRequest { string entity_id = 1; // channel or output-bus id int32 band_index = 2; string type = 3; // "peak" / "hi_shelf" / "lo_shelf" / "hpf" / "lpf" / "notch" float hz = 4; float gain = 5; float q = 6; bool band_enabled = 7; bool eq_enabled = 8; } // ============================================================ // METERING — server-side streams (levels/LUFS/GR/spectrum) + point queries // ============================================================ service MeteringService { // 30–60 Hz peaks for every channel + bus. Replaces the 60 Hz meter loop in // AudioEngineService that reads shared float arrays directly. rpc StreamLevels(google.protobuf.Empty) returns (stream LevelBatch); // ~10 Hz LUFS updates (M/S/I + true peak + LRA). UI updates loudness displays from this // instead of polling GetChannelLufs / GetOutputBusLufs / GetMainBusLufs. rpc StreamLufs(google.protobuf.Empty) returns (stream LufsBatch); // ~30 Hz gate/comp/limiter gain-reduction snapshots for GR overlays. rpc StreamGr(google.protobuf.Empty) returns (stream GrBatch); // On-demand server-side FFT: entity_id chooses the tap point (channel or bus post-EQ). // Stage 3.1: moves SpectrumAnalyzerService's client-side FFT here. rpc StreamSpectrum(SpectrumRequest) returns (stream SpectrumFrame); // Point queries retained for callers that don't want to subscribe (e.g. tooltip fetches). rpc GetChannelLufs(ChannelIdRequest) returns (LufsReadout); rpc GetOutputBusLufs(OutputIdRequest) returns (LufsReadout); rpc GetMainBusLufs(google.protobuf.Empty) returns (LufsReadout); rpc ResetChannelLufsIntegrated(ChannelIdRequest) returns (google.protobuf.Empty); rpc ResetOutputBusLufsIntegrated(OutputIdRequest) returns (google.protobuf.Empty); rpc ResetMainBusLufsIntegrated(google.protobuf.Empty) returns (google.protobuf.Empty); rpc GetChannelGr(ChannelIdRequest) returns (ChannelGrReadout); rpc GetOutputGr(OutputIdRequest) returns (OutputGrReadout); rpc GetOutputLatencyMetrics(google.protobuf.Empty) returns (LatencyMetrics); } message LevelBatch { // Monotonic sequence number — clients detect dropped/reordered updates without wall-clock. uint64 sequence = 1; repeated LevelSample samples = 2; } message LevelSample { string entity_id = 1; // channel id, bus id, "id-hin", "id-hout", "id-hpre", "id-hpost" (mirror MeterBridge keys) float peak_l = 2; float peak_r = 3; bool clip = 4; // Optional per-speaker peaks for surround VU (empty on stereo channels). // Two-slot layout for the spatial-scope view (post + ghost) can double the length; consumer // knows the layout from the channel model. repeated float surround_peaks = 5 [packed = true]; } message LufsBatch { uint64 sequence = 1; repeated LufsSample samples = 2; } message LufsSample { string entity_id = 1; LufsReadout readout = 2; } message LufsReadout { float momentary = 1; float short_term = 2; float integrated = 3; float true_peak_dbtp = 4; float loudness_range_lu = 5; } message GrBatch { uint64 sequence = 1; repeated GrSample samples = 2; } message GrSample { string entity_id = 1; // is_channel discriminates the payload: channels carry gate/comp levels; buses carry // level + limiter GR only. bool is_channel = 2; float gate_level_db = 3; // channel only float gate_gr_db = 4; // channel only float comp_level_db = 5; // channel only float comp_gr_db = 6; // channel only float bus_level_db = 7; // bus only float bus_lim_gr_db = 8; // bus only } message ChannelGrReadout { float gate_level_db = 1; float gate_gr_db = 2; float comp_level_db = 3; float comp_gr_db = 4; } message OutputGrReadout { float level_db = 1; float lim_gr_db = 2; } message LatencyMetrics { float jitter_us = 1; float hw_ms = 2; float buf_ms = 3; } message SpectrumRequest { // Empty string closes the stream server-side. string entity_id = 1; } message SpectrumFrame { uint64 sequence = 1; // Log-frequency binned magnitudes in [0..1]. Bin count is fixed per stream (agreed // out-of-band; today 160 bins across 20 Hz–20 kHz). repeated float bins = 2 [packed = true]; } // ============================================================ // DIAGNOSTICS — reliability signal stream // ============================================================ service DiagnosticsService { // Replaces IAudioEngineService.DiagnosticSignal Action. rpc StreamSignals(google.protobuf.Empty) returns (stream DiagnosticSignal); } message DiagnosticSignal { string type = 1; // "engine_start_fail" / "exclusive_denied" / "hotswap_recovered" / … int32 count = 2; string detail = 3; // sanitised tokens only — never device names }