Reset audio backend on new file / project load

The backend's Command::Reset (full teardown: rebuilds Project, audio/buffer
pools, ID counters) was never invoked from the UI. New File and project loads
only cleared app-side maps, so the previous file's tracks and loaded
instruments stayed resident in the backend and kept getting mixed on every
audio callback.

Add a reset_audio_backend() helper (controller.reset() + clear app-side track
maps and backend-derived caches) and call it in the three teardown paths:
NewFile, create_new_project_with_focus, and apply_loaded_project. Also drops a
duplicate layer_to_track_map.clear() in the NewFile handler.

Race-free: the audio thread drains all command_tx commands before any query_tx
queries each callback, so Reset (a command) runs before the track/pool rebuild
(queries), even though they travel on separate channels.
This commit is contained in:
Skyler Lehmkuhl 2026-07-09 03:36:56 -04:00
parent bb6b6fa9e3
commit 6e361aa30c
1 changed files with 40 additions and 11 deletions

View File

@ -1735,10 +1735,39 @@ impl EditorApp {
); );
} }
/// Tear down the audio backend for the currently-open project.
///
/// Sends `Command::Reset` (fully rebuilds the backend `Project`, audio/buffer pools, and ID
/// counters) and clears the app-side track maps + backend-derived caches that pointed at the old
/// tracks. Must be called before building a new document's tracks, otherwise the previous file's
/// tracks/instruments stay resident in the backend and keep getting mixed (orphaned voices).
///
/// Ordering is safe: the audio thread drains all `command_tx` commands before any `query_tx`
/// queries each callback, so a `reset()` pushed here always runs before the `create_*_track_sync`
/// queries that rebuild the project.
fn reset_audio_backend(&mut self) {
if let Some(ref controller_arc) = self.audio_controller {
controller_arc.lock().unwrap().reset();
}
self.layer_to_track_map.clear();
self.track_to_layer_map.clear();
self.clip_instance_to_backend_map.clear();
self.midi_event_cache.clear();
self.audio_duration_cache.clear();
self.raw_audio_cache.clear();
self.waveform_gpu_dirty.clear();
self.waveform_minmax_pools.clear();
self.waveform_pyramid_blobs.clear();
}
/// Create a new project with the specified focus/layout /// Create a new project with the specified focus/layout
fn create_new_project_with_focus(&mut self, layout_index: usize) { fn create_new_project_with_focus(&mut self, layout_index: usize) {
use lightningbeam_core::layer::{AnyLayer, AudioLayer, VectorLayer, VideoLayer}; use lightningbeam_core::layer::{AnyLayer, AudioLayer, VectorLayer, VideoLayer};
// Drop the previous project's backend tracks/instruments before building the new one, so a
// "new file" while a project is open doesn't leave orphaned tracks resident in the backend.
self.reset_audio_backend();
// Create a new blank document // Create a new blank document
let mut document = lightningbeam_core::document::Document::with_size( let mut document = lightningbeam_core::document::Document::with_size(
"Untitled", "Untitled",
@ -3185,23 +3214,17 @@ impl EditorApp {
println!("Menu: New File"); println!("Menu: New File");
// TODO: Prompt to save current file if modified // TODO: Prompt to save current file if modified
// Reset state and return to start screen // Tear down the backend (stops old instruments/voices immediately) and clear the
self.layer_to_track_map.clear(); // app-side track maps + backend-derived caches.
self.track_to_layer_map.clear(); self.reset_audio_backend();
self.layer_to_track_map.clear();
self.clip_instance_to_backend_map.clear(); // Reset UI state and return to start screen
self.current_file_path = None; self.current_file_path = None;
self.selection.clear(); self.selection.clear();
self.editing_context = EditingContext::default(); self.editing_context = EditingContext::default();
self.active_layer_id = None; self.active_layer_id = None;
self.playback_time = 0.0; self.playback_time = 0.0;
self.is_playing = false; self.is_playing = false;
self.midi_event_cache.clear();
self.audio_duration_cache.clear();
self.raw_audio_cache.clear();
self.waveform_gpu_dirty.clear();
self.waveform_minmax_pools.clear();
self.waveform_pyramid_blobs.clear();
self.pane_instances.clear(); self.pane_instances.clear();
self.project_generation += 1; self.project_generation += 1;
self.app_mode = AppMode::StartScreen; self.app_mode = AppMode::StartScreen;
@ -4146,6 +4169,12 @@ impl EditorApp {
// TODO Phase 5: Show recovery dialog // TODO Phase 5: Show recovery dialog
} }
// Tear down the previously-open project's backend tracks/instruments before restoring this
// file's audio pool + tracks, so an open-over-open doesn't leave orphaned tracks resident in
// the backend. Reset is a command; the audio-pool/track restoration below uses queries, which
// the audio thread drains after all commands each callback, so the ordering holds.
self.reset_audio_backend();
// Replace document // Replace document
let step1_start = std::time::Instant::now(); let step1_start = std::time::Instant::now();
self.action_executor = ActionExecutor::new(loaded_project.document); self.action_executor = ActionExecutor::new(loaded_project.document);