Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an explicit, desktop-only feature gate (VITE_ENABLE_TAURI_AUDIO_BACKEND=true) to allow selecting the native Rust/Tauri audio backend in Tauri builds while keeping WebAudio as the default path, and routes key transport/clip scheduling through an AudioBridge singleton.
Changes:
- Added a Tauri-only env gate helper (
isTauriAudioBackendEnabled) and updated the bridge factory to selectTauriBackendwhen enabled. - Introduced an
AudioBridgesingleton (getAudioBridge) and migrated transport playback/scheduling + track/master param syncing to go through the bridge. - Implemented significant
TauriBackendfunctionality (transport events, clip scheduling, decode fallback, metering commands) and registered new Tauri commands for meters.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vite-env.d.ts | Adds env typing for the Tauri audio backend gate. |
| src/utils/tauri.ts | Adds isTauriAudioBackendEnabled() helper. |
| src/utils/tests/tauri.test.ts | Tests the new gate behavior + env stubbing cleanup. |
| src/engine/bridge/index.ts | Enables gated backend selection + adds a bridge singleton for reuse. |
| src/engine/bridge/types.ts | Extends bridge clip fields + adds pause/stop + native meter/clip wire types. |
| src/engine/bridge/WebAudioBackend.ts | Passes through additional clip fields and adds pauseAllSources(). |
| src/engine/bridge/TauriBackend.ts | Implements native resume/transport listener, scheduling, metering calls, decode fallback. |
| src/hooks/useTransport.ts | Routes clip playback + track/master params through AudioBridge. |
| src/hooks/useAudioEngine.ts | Uses bridge for resume + time updates when on Tauri backend. |
| src/engine/bridge/tests/* | Updates/adds tests for bridge selection and backend behaviors. |
| src/hooks/tests/* | Updates transport mocks for new stop/pause behavior via bridge. |
| src-tauri/src/lib.rs | Registers new meter commands. |
| src-tauri/src/commands/audio.rs | Adds audio_get_track_meter and audio_get_master_meter commands. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| this._scheduledEndSample = Math.max(0, Math.round(totalDuration * this.sampleRate)); | ||
| this._currentSamplePosition = Math.max(0, Math.round(fromTime * this.sampleRate)); | ||
|
|
||
| void (async () => { | ||
| await invoke('audio_clip_set_schedule', { clips: nativeClips }); | ||
| if (token !== this._transportCommandToken) return; | ||
| await invoke('audio_transport_seek', { samplePosition: this._currentSamplePosition }); |
There was a problem hiding this comment.
schedulePlayback() uses this._currentSamplePosition inside an async invoke chain after awaiting audio_clip_set_schedule. Because _currentSamplePosition is also updated by transport-position events, the value used for audio_transport_seek can drift before the seek happens. Capture the intended seek sample position in a local constant and use that for the audio_transport_seek payload.
| this._scheduledEndSample = Math.max(0, Math.round(totalDuration * this.sampleRate)); | |
| this._currentSamplePosition = Math.max(0, Math.round(fromTime * this.sampleRate)); | |
| void (async () => { | |
| await invoke('audio_clip_set_schedule', { clips: nativeClips }); | |
| if (token !== this._transportCommandToken) return; | |
| await invoke('audio_transport_seek', { samplePosition: this._currentSamplePosition }); | |
| const seekSamplePosition = Math.max(0, Math.round(fromTime * this.sampleRate)); | |
| this._scheduledEndSample = Math.max(0, Math.round(totalDuration * this.sampleRate)); | |
| this._currentSamplePosition = seekSamplePosition; | |
| void (async () => { | |
| await invoke('audio_clip_set_schedule', { clips: nativeClips }); | |
| if (token !== this._transportCommandToken) return; | |
| await invoke('audio_transport_seek', { samplePosition: seekSamplePosition }); |
| getTrackMeter(_trackId: string): MeterData { | ||
| return ZERO_METER; | ||
| const entry = this._trackEntries.get(_trackId); | ||
| if (entry?.handle) { | ||
| invoke<NativeMeterReading>('audio_get_track_meter', { handle: entry.handle }) | ||
| .then((reading) => { | ||
| this._trackMeters.set(_trackId, toMeterData(reading)); | ||
| }) | ||
| .catch(() => {}); | ||
| } | ||
| return this._trackMeters.get(_trackId) ?? ZERO_METER; | ||
| } |
There was a problem hiding this comment.
getTrackMeter() triggers a Tauri invoke() on every call. Meter reads in the UI are typically executed in requestAnimationFrame loops (60fps per meter), so this implementation would flood the IPC channel and repeatedly lock the Rust engine. Consider polling meters on a fixed interval with throttling (e.g., one invoke per track per N ms), batching reads, or switching to a push/event-driven meter stream so getTrackMeter() is a pure cache read.
| const ZERO_METER: MeterData = { level: -Infinity, leftLevel: -Infinity, rightLevel: -Infinity, clipped: false }; | ||
| const ZERO_MASTER: MasterMeterData = { level: -Infinity, clipped: false }; |
There was a problem hiding this comment.
The default meter constants use -Infinity, but both the WebAudio engine and the Rust MeterReading use linear 0..1 levels (silence = 0). Returning -Infinity is inconsistent with the rest of the codebase and can leak unexpected values to callers. Prefer { level: 0, leftLevel: 0, rightLevel: 0, clipped: false } / { level: 0, clipped: false } for the defaults (and update the affected tests accordingly).
| const ZERO_METER: MeterData = { level: -Infinity, leftLevel: -Infinity, rightLevel: -Infinity, clipped: false }; | |
| const ZERO_MASTER: MasterMeterData = { level: -Infinity, clipped: false }; | |
| const ZERO_METER: MeterData = { level: 0, leftLevel: 0, rightLevel: 0, clipped: false }; | |
| const ZERO_MASTER: MasterMeterData = { level: 0, clipped: false }; |
| if (clamped < 0) return { left: 1, right: 1 + clamped }; | ||
| if (clamped > 0) return { left: 1 - clamped, right: 1 }; | ||
| return { left: 1, right: 1 }; |
There was a problem hiding this comment.
The pan implementation in getPanGains() is linear (left=1,right=1±pan). WebAudio’s StereoPannerNode (used by the existing engine) uses an equal-power pan law, and the Rust engine exposes equal_power_pan as well. Using a different pan law will change perceived loudness across the stereo field and break parity between backends; update this to match the equal-power behavior.
| if (clamped < 0) return { left: 1, right: 1 + clamped }; | |
| if (clamped > 0) return { left: 1 - clamped, right: 1 }; | |
| return { left: 1, right: 1 }; | |
| const angle = ((clamped + 1) * Math.PI) / 4; | |
| return { | |
| left: Math.cos(angle), | |
| right: Math.sin(angle), | |
| }; |
| async resume(): Promise<void> { | ||
| await invoke('audio_start_engine', { | ||
| config: { sampleRate: 48000, bufferSize: 256, deviceName: null }, | ||
| }); | ||
| this.startTransportListener(); | ||
| this.refreshTransportPosition(); | ||
| } |
There was a problem hiding this comment.
resume() always calls audio_start_engine, but the Rust command returns EngineError::AlreadyRunning when the engine is already running. Since resume() is invoked on every play/gesture, this will start failing after the first successful call. Make resume() idempotent (e.g., check audio_get_engine_status first, or catch/ignore the alreadyRunning error and continue setting up listeners / refreshing position).
|
Final review and validation update:
Ready to merge. |
Summary
Closes #1780.
This hotfix makes the native Rust/Tauri audio backend product-activatable behind an explicit desktop-only gate while preserving the existing WebAudio default path.
VITE_ENABLE_TAURI_AUDIO_BACKEND=trueas the activation gate, only effective inside Tauri.TauriBackend.Verification
npx tsc --noEmit --pretty falsenpx vitest run src/engine/bridge/__tests__ src/utils/__tests__/tauri.test.ts src/hooks/__tests__/useAudioEngine.test.ts src/hooks/__tests__/useTransport.test.ts src/hooks/__tests__/useTransport.strudel.test.tsnpm test -- src/engine/bridge/__tests__ src/utils/__tests__/tauri.test.tsnpm run test:coverage— 740 files passed, 8731 tests passed, 3 skipped, 10 todocargo test --manifest-path src-tauri/Cargo.tomlgit diff --checkNotes
The native path remains explicitly gated because full Rust parity is still in progress. Current native clip scheduling applies initial track volume/pan/mute/solo and supports transport/meter integration, but true Rust-native decode and advanced WebAudio-only clip processing such as fades, envelopes, warp markers, pitch shift, and stretch modes should continue as follow-up native-engine work.