Fix UI hang on audio import

This commit is contained in:
Skyler Lehmkuhl 2026-02-15 00:50:22 -05:00
parent 408343094a
commit 12d927ed3d
3 changed files with 50 additions and 48 deletions

View File

@ -272,20 +272,11 @@ impl Engine {
// Forward chunk generation events from background threads
while let Ok(event) = self.chunk_generation_rx.try_recv() {
match event {
AudioEvent::WaveformDecodeComplete { pool_index, samples, decoded_frames: df, total_frames: _tf } => {
// Update pool entry and forward samples directly to UI
if let Some(file) = self.audio_pool.get_file_mut(pool_index) {
AudioEvent::WaveformDecodeComplete { pool_index, samples, decoded_frames: _df, total_frames: _tf } => {
// Forward samples directly to UI — no clone, just move
if let Some(file) = self.audio_pool.get_file(pool_index) {
let sr = file.sample_rate;
let ch = file.channels;
if let crate::audio::pool::AudioStorage::Compressed {
ref mut decoded_for_waveform,
ref mut decoded_frames,
..
} = file.storage {
*decoded_for_waveform = samples.clone();
*decoded_frames = df;
}
// Send samples inline — UI won't need to query back
let _ = self.event_tx.push(AudioEvent::AudioDecodeProgress {
pool_index,
samples,
@ -1825,6 +1816,22 @@ impl Engine {
format: metadata.format,
});
// For PCM files, send samples inline so the UI doesn't need to
// do a blocking get_pool_audio_samples() query.
if metadata.format == crate::io::AudioFormat::Pcm {
if let Some(file) = self.audio_pool.get_file(pool_index) {
let samples = file.data().to_vec();
if !samples.is_empty() {
let _ = self.event_tx.push(AudioEvent::AudioDecodeProgress {
pool_index,
samples,
sample_rate: metadata.sample_rate,
channels: metadata.channels,
});
}
}
}
Ok(pool_index)
}

View File

@ -436,10 +436,11 @@ impl AudioFile {
}
// Send progressive update (fast initial, then periodic)
// Only send NEW samples since last update (delta) to avoid large copies
let interval = if sent_first { steady_interval } else { initial_interval };
if audio_data.len() - last_update_len >= interval {
let decoded_frames = audio_data.len() as u64 / channels as u64;
on_progress(&audio_data, decoded_frames, total_frames);
on_progress(&audio_data[last_update_len..], decoded_frames, total_frames);
last_update_len = audio_data.len();
sent_first = true;
}
@ -449,9 +450,9 @@ impl AudioFile {
}
}
// Final update with all data
// Final update with remaining data (delta since last update)
let decoded_frames = audio_data.len() as u64 / channels as u64;
on_progress(&audio_data, decoded_frames, decoded_frames.max(total_frames));
on_progress(&audio_data[last_update_len..], decoded_frames, decoded_frames.max(total_frames));
}
/// Calculate the duration of the audio file in seconds

View File

@ -761,6 +761,7 @@ impl EditorApp {
let current_layout = layouts[0].layout.clone();
// Disable egui's "Unaligned" debug overlay (on by default in debug builds)
#[cfg(debug_assertions)]
cc.egui_ctx.style_mut(|style| style.debug.show_unaligned = false);
// Load application config
@ -1670,6 +1671,7 @@ impl EditorApp {
let file = dialog.pick_file();
if let Some(path) = file {
let _import_timer = std::time::Instant::now();
// Get extension and detect file type
let extension = path.extension()
.and_then(|e| e.to_str())
@ -1698,12 +1700,16 @@ impl EditorApp {
}
};
eprintln!("[TIMING] import took {:.1}ms", _import_timer.elapsed().as_secs_f64() * 1000.0);
// Auto-place if this is "Import" (not "Import to Library")
if auto_place {
if let Some(asset_info) = imported_asset {
let _place_timer = std::time::Instant::now();
self.auto_place_asset(asset_info);
eprintln!("[TIMING] auto_place took {:.1}ms", _place_timer.elapsed().as_secs_f64() * 1000.0);
}
}
eprintln!("[TIMING] total import+place took {:.1}ms", _import_timer.elapsed().as_secs_f64() * 1000.0);
}
}
MenuAction::Export => {
@ -3080,6 +3086,8 @@ impl EditorApp {
impl eframe::App for EditorApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
let _frame_start = std::time::Instant::now();
// Disable egui's built-in Ctrl+Plus/Minus zoom behavior
// We handle zoom ourselves for the Stage pane
ctx.options_mut(|o| {
@ -3111,37 +3119,10 @@ impl eframe::App for EditorApp {
// Will switch to editor mode when file finishes loading
}
// Fetch missing raw audio on-demand (for lazy loading after project load)
// Collect pool indices that need raw audio data
let missing_raw_audio: Vec<usize> = self.action_executor.document()
.audio_clips.values()
.filter_map(|clip| {
if let lightningbeam_core::clip::AudioClipType::Sampled { audio_pool_index } = &clip.clip_type {
if !self.raw_audio_cache.contains_key(audio_pool_index) {
Some(*audio_pool_index)
} else {
None
}
} else {
None
}
})
.collect();
// Fetch missing raw audio samples
for pool_index in missing_raw_audio {
if let Some(ref controller_arc) = self.audio_controller {
let mut controller = controller_arc.lock().unwrap();
match controller.get_pool_audio_samples(pool_index) {
Ok((samples, sr, ch)) => {
self.raw_audio_cache.insert(pool_index, (samples, sr, ch));
self.waveform_gpu_dirty.insert(pool_index);
self.audio_pools_with_new_waveforms.insert(pool_index);
}
Err(e) => eprintln!("Failed to fetch raw audio for pool {}: {}", pool_index, e),
}
}
}
// NOTE: Missing raw audio samples for newly imported files will arrive
// via AudioDecodeProgress events (compressed) or inline with AudioFileReady
// (PCM). No blocking query needed here.
// For project loading, audio files are re-imported which also sends events.
// Initialize and update effect thumbnail generator (GPU-based effect previews)
if let Some(render_state) = frame.wgpu_render_state() {
@ -3296,6 +3277,7 @@ impl eframe::App for EditorApp {
ctx.request_repaint();
}
let _pre_events_ms = _frame_start.elapsed().as_secs_f64() * 1000.0;
// Check if audio events are pending and request repaint if needed
if self.audio_events_pending.load(std::sync::atomic::Ordering::Relaxed) {
ctx.request_repaint();
@ -3639,8 +3621,12 @@ impl eframe::App for EditorApp {
ctx.request_repaint();
}
AudioEvent::AudioDecodeProgress { pool_index, samples, sample_rate, channels } => {
// Samples arrive inline — no query needed
self.raw_audio_cache.insert(pool_index, (samples, sample_rate, channels));
// Samples arrive as deltas — append to existing cache
if let Some(entry) = self.raw_audio_cache.get_mut(&pool_index) {
entry.0.extend_from_slice(&samples);
} else {
self.raw_audio_cache.insert(pool_index, (samples, sample_rate, channels));
}
self.waveform_gpu_dirty.insert(pool_index);
ctx.request_repaint();
}
@ -3658,6 +3644,8 @@ impl eframe::App for EditorApp {
}
}
let _post_events_ms = _frame_start.elapsed().as_secs_f64() * 1000.0;
// Request continuous repaints when playing to update time display
if self.is_playing {
ctx.request_repaint();
@ -4145,6 +4133,12 @@ impl eframe::App for EditorApp {
);
debug_overlay::render_debug_overlay(ctx, &stats);
}
let frame_ms = _frame_start.elapsed().as_secs_f64() * 1000.0;
if frame_ms > 50.0 {
eprintln!("[TIMING] SLOW FRAME: {:.1}ms (pre-events={:.1}, events={:.1}, post-events={:.1})",
frame_ms, _pre_events_ms, _post_events_ms - _pre_events_ms, frame_ms - _post_events_ms);
}
}
}