From ca38de6bd0256be66805b81412775a47bdb3dd39 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Jan 2026 00:35:25 +0000 Subject: [PATCH] fix(audio): Synchronize buffer size and sample rate across all components This fix addresses the issue where buffer size and sample rate settings were not synchronized between the web UI, native bridge, and TUI: 1. Native bridge now sends DeviceConfig message back to browser when buffer size or sample rate changes, confirming the actual values 2. Native bridge now sends DeviceInfo event to TUI when settings change, so the TUI display updates in real-time 3. Changed default buffer size from 128 to 256 samples (matching web UI default) 4. Added helper methods to AudioEngine: - get_input_device_id() - get_output_device_id() - get_channel_config() 5. Web frontend now listens for deviceConfig events and updates the store with confirmed values from native bridge This ensures all components show consistent values and the audio engine actually uses the settings the user selected. --- native-bridge/src/audio/engine.rs | 21 +++++++++++- native-bridge/src/protocol/server.rs | 48 ++++++++++++++++++++++++++-- src/hooks/useNativeBridge.ts | 17 ++++++++++ src/lib/audio/native-bridge.ts | 26 +++++++++++++++ 4 files changed, 108 insertions(+), 4 deletions(-) diff --git a/native-bridge/src/audio/engine.rs b/native-bridge/src/audio/engine.rs index 8ac6b222..d9f8de92 100644 --- a/native-bridge/src/audio/engine.rs +++ b/native-bridge/src/audio/engine.rs @@ -59,7 +59,7 @@ impl Default for EngineConfig { fn default() -> Self { Self { sample_rate: SampleRate::Hz48000, - buffer_size: BufferSize::Samples128, + buffer_size: BufferSize::Samples256, input_device_id: None, output_device_id: None, channel_config: ChannelConfig::default(), @@ -598,6 +598,25 @@ impl AudioEngine { } } + /// Get current input device ID + pub fn get_input_device_id(&self) -> Option { + self.config.input_device_id.clone() + } + + /// Get current output device ID + pub fn get_output_device_id(&self) -> Option { + self.config.output_device_id.clone() + } + + /// Get current channel configuration + pub fn get_channel_config(&self) -> ChannelConfig { + if let Ok(state) = self.processing_state.try_read() { + state.channel_config.clone() + } else { + self.config.channel_config.clone() + } + } + /// Get raw audio samples for streaming to browser (for WebRTC broadcast) /// Returns up to `max_samples` stereo samples, or fewer if buffer has less pub fn get_browser_stream_audio(&self, max_samples: usize) -> Vec { diff --git a/native-bridge/src/protocol/server.rs b/native-bridge/src/protocol/server.rs index 6ed5d130..2409c742 100644 --- a/native-bridge/src/protocol/server.rs +++ b/native-bridge/src/protocol/server.rs @@ -508,11 +508,32 @@ impl BridgeServer { 256 => crate::audio::BufferSize::Samples256, 512 => crate::audio::BufferSize::Samples512, 1024 => crate::audio::BufferSize::Samples1024, - _ => crate::audio::BufferSize::Samples128, + _ => crate::audio::BufferSize::Samples256, }; match app.audio_engine.set_buffer_size(buffer_size) { - Ok(_) => None, + Ok(_) => { + // Send updated device info to TUI + let device_info = app.audio_engine.get_device_info(); + if let Some(ref tx) = app.tui_tx { + let _ = tx.try_send(AppEvent::DeviceInfo { + input_device: device_info.input_device.clone(), + output_device: device_info.output_device.clone(), + sample_rate: device_info.sample_rate, + buffer_size: device_info.buffer_size, + }); + } + // Send DeviceConfig back to browser to confirm + Some(NativeMessage::DeviceConfig { + input_device: app.audio_engine.get_input_devices().ok() + .and_then(|devs| devs.into_iter().find(|d| Some(d.id.clone()) == app.audio_engine.get_input_device_id())), + output_device: app.audio_engine.get_output_devices().ok() + .and_then(|devs| devs.into_iter().find(|d| Some(d.id.clone()) == app.audio_engine.get_output_device_id())), + sample_rate: device_info.sample_rate, + buffer_size: device_info.buffer_size, + channel_config: app.audio_engine.get_channel_config(), + }) + } Err(e) => Some(NativeMessage::Error { code: "CONFIG_ERROR".to_string(), message: e.to_string(), @@ -531,7 +552,28 @@ impl BridgeServer { }; match app.audio_engine.set_sample_rate(sample_rate) { - Ok(_) => None, + Ok(_) => { + // Send updated device info to TUI + let device_info = app.audio_engine.get_device_info(); + if let Some(ref tx) = app.tui_tx { + let _ = tx.try_send(AppEvent::DeviceInfo { + input_device: device_info.input_device.clone(), + output_device: device_info.output_device.clone(), + sample_rate: device_info.sample_rate, + buffer_size: device_info.buffer_size, + }); + } + // Send DeviceConfig back to browser to confirm + Some(NativeMessage::DeviceConfig { + input_device: app.audio_engine.get_input_devices().ok() + .and_then(|devs| devs.into_iter().find(|d| Some(d.id.clone()) == app.audio_engine.get_input_device_id())), + output_device: app.audio_engine.get_output_devices().ok() + .and_then(|devs| devs.into_iter().find(|d| Some(d.id.clone()) == app.audio_engine.get_output_device_id())), + sample_rate: device_info.sample_rate, + buffer_size: device_info.buffer_size, + channel_config: app.audio_engine.get_channel_config(), + }) + } Err(e) => Some(NativeMessage::Error { code: "CONFIG_ERROR".to_string(), message: e.to_string(), diff --git a/src/hooks/useNativeBridge.ts b/src/hooks/useNativeBridge.ts index 24c26ecd..5359c043 100644 --- a/src/hooks/useNativeBridge.ts +++ b/src/hooks/useNativeBridge.ts @@ -170,6 +170,21 @@ export function useNativeBridge() { s.setError(data); }; + // Handle device config confirmation from native bridge - sync actual values back to store + const handleDeviceConfig = (data: { + inputDevice: BridgeDevice | null; + outputDevice: BridgeDevice | null; + sampleRate: number; + bufferSize: number; + channelConfig: { channelCount: 1 | 2; leftChannel: number; rightChannel?: number }; + }) => { + console.log('[useNativeBridge] DeviceConfig confirmed:', data.sampleRate, 'Hz,', data.bufferSize, 'samples'); + const s = useBridgeAudioStore.getState(); + // Update store with actual values from native bridge + s.setSampleRate(data.sampleRate as 44100 | 48000); + s.setBufferSize(data.bufferSize as 32 | 64 | 128 | 256 | 512 | 1024); + }; + // Handle audio levels from native bridge - update track levels for waveform display const handleLevels = (data: { inputLevel: number; @@ -211,6 +226,7 @@ export function useNativeBridge() { nativeBridge.on('disconnected', handleDisconnected); nativeBridge.on('audioStatus', handleAudioStatus); nativeBridge.on('devices', handleDevices); + nativeBridge.on('deviceConfig', handleDeviceConfig); nativeBridge.on('error', handleError); nativeBridge.on('levels', handleLevels); @@ -244,6 +260,7 @@ export function useNativeBridge() { nativeBridge.off('disconnected', handleDisconnected); nativeBridge.off('audioStatus', handleAudioStatus); nativeBridge.off('devices', handleDevices); + nativeBridge.off('deviceConfig', handleDeviceConfig); nativeBridge.off('error', handleError); nativeBridge.off('levels', handleLevels); initialized.current = false; diff --git a/src/lib/audio/native-bridge.ts b/src/lib/audio/native-bridge.ts index e9cac34f..6489fddc 100644 --- a/src/lib/audio/native-bridge.ts +++ b/src/lib/audio/native-bridge.ts @@ -50,12 +50,25 @@ interface BridgeStreamHealth { msSinceLastRead: number; } +interface BridgeDeviceConfig { + inputDevice: BridgeDevice | null; + outputDevice: BridgeDevice | null; + sampleRate: number; + bufferSize: number; + channelConfig: { + channelCount: 1 | 2; + leftChannel: number; + rightChannel?: number; + }; +} + // Native bridge sends snake_case, we normalize to camelCase type NativeMessage = | { type: 'welcome'; version: string; driverType?: string; driver_type?: string } | { type: 'pong'; timestamp: number; nativeTime: number } | { type: 'error'; code: string; message: string } | { type: 'devices'; inputs: BridgeDevice[]; outputs: BridgeDevice[] } + | { type: 'deviceConfig' } & BridgeDeviceConfig | { type: 'audioStatus' } & BridgeAudioStatus | { type: 'levels' } & BridgeLevels | { type: 'streamHealth' } & BridgeStreamHealth @@ -88,6 +101,7 @@ export type BridgeEventType = | 'connected' | 'disconnected' | 'devices' + | 'deviceConfig' | 'levels' | 'audioStatus' | 'streamHealth' @@ -99,6 +113,7 @@ type BridgeEventData = { connected: { version: string; driverType: string }; disconnected: { reason: string }; devices: { inputs: BridgeDevice[]; outputs: BridgeDevice[] }; + deviceConfig: BridgeDeviceConfig; levels: BridgeLevels; audioStatus: BridgeAudioStatus; streamHealth: BridgeStreamHealth; @@ -320,6 +335,17 @@ export class NativeBridge { this.emit('devices', { inputs: msg.inputs, outputs: msg.outputs }); break; + case 'deviceConfig': + console.log('[NativeBridge] DeviceConfig received:', msg.sampleRate, 'Hz,', msg.bufferSize, 'samples'); + this.emit('deviceConfig', { + inputDevice: msg.inputDevice, + outputDevice: msg.outputDevice, + sampleRate: msg.sampleRate, + bufferSize: msg.bufferSize, + channelConfig: msg.channelConfig, + }); + break; + case 'levels': this.emit('levels', { inputLevel: msg.inputLevel,