sampler improvements, live waveform preview

This commit is contained in:
Skyler Lehmkuhl 2026-02-17 10:08:49 -05:00
parent c10f42da8f
commit 21a49235fc
11 changed files with 210 additions and 53 deletions

View File

@ -53,6 +53,7 @@ pub struct Engine {
// Recording state // Recording state
recording_state: Option<RecordingState>, recording_state: Option<RecordingState>,
input_rx: Option<rtrb::Consumer<f32>>, input_rx: Option<rtrb::Consumer<f32>>,
recording_mirror_tx: Option<rtrb::Producer<f32>>,
recording_progress_counter: usize, recording_progress_counter: usize,
// MIDI recording state // MIDI recording state
@ -130,6 +131,7 @@ impl Engine {
next_clip_id: 0, next_clip_id: 0,
recording_state: None, recording_state: None,
input_rx: None, input_rx: None,
recording_mirror_tx: None,
recording_progress_counter: 0, recording_progress_counter: 0,
midi_recording_state: None, midi_recording_state: None,
midi_input_manager: None, midi_input_manager: None,
@ -151,6 +153,11 @@ impl Engine {
self.input_rx = Some(input_rx); self.input_rx = Some(input_rx);
} }
/// Set the recording mirror producer for streaming audio to UI during recording
pub fn set_recording_mirror_tx(&mut self, tx: rtrb::Producer<f32>) {
self.recording_mirror_tx = Some(tx);
}
/// Set the MIDI input manager for external MIDI devices /// Set the MIDI input manager for external MIDI devices
pub fn set_midi_input_manager(&mut self, manager: MidiInputManager) { pub fn set_midi_input_manager(&mut self, manager: MidiInputManager) {
self.midi_input_manager = Some(manager); self.midi_input_manager = Some(manager);
@ -393,8 +400,24 @@ impl Engine {
// Add samples to recording // Add samples to recording
if !self.recording_sample_buffer.is_empty() { if !self.recording_sample_buffer.is_empty() {
// Calculate how many samples will be skipped (stale buffer data)
let skip = if recording.paused {
self.recording_sample_buffer.len()
} else {
recording.samples_to_skip.min(self.recording_sample_buffer.len())
};
match recording.add_samples(&self.recording_sample_buffer) { match recording.add_samples(&self.recording_sample_buffer) {
Ok(_flushed) => { Ok(_flushed) => {
// Mirror non-skipped samples to UI for live waveform display
if skip < self.recording_sample_buffer.len() {
if let Some(ref mut mirror_tx) = self.recording_mirror_tx {
for &sample in &self.recording_sample_buffer[skip..] {
let _ = mirror_tx.push(sample);
}
}
}
// Update clip duration every callback for sample-accurate timing // Update clip duration every callback for sample-accurate timing
let duration = recording.duration(); let duration = recording.duration();
let clip_id = recording.clip_id; let clip_id = recording.clip_id;
@ -2540,7 +2563,7 @@ impl Engine {
} }
// Notify UI that recording has started // Notify UI that recording has started
let _ = self.event_tx.push(AudioEvent::RecordingStarted(track_id, clip_id)); let _ = self.event_tx.push(AudioEvent::RecordingStarted(track_id, clip_id, self.sample_rate, self.channels));
} }
Err(e) => { Err(e) => {
// Send error event to UI // Send error event to UI

View File

@ -235,8 +235,8 @@ pub enum AudioEvent {
BufferPoolStats(BufferPoolStats), BufferPoolStats(BufferPoolStats),
/// Automation lane created (track_id, lane_id, parameter_id) /// Automation lane created (track_id, lane_id, parameter_id)
AutomationLaneCreated(TrackId, AutomationLaneId, ParameterId), AutomationLaneCreated(TrackId, AutomationLaneId, ParameterId),
/// Recording started (track_id, clip_id) /// Recording started (track_id, clip_id, sample_rate, channels)
RecordingStarted(TrackId, ClipId), RecordingStarted(TrackId, ClipId, u32, u32),
/// Recording progress update (clip_id, current_duration) /// Recording progress update (clip_id, current_duration)
RecordingProgress(ClipId, f64), RecordingProgress(ClipId, f64),
/// Recording stopped (clip_id, pool_index, waveform) /// Recording stopped (clip_id, pool_index, waveform)

View File

@ -39,6 +39,8 @@ pub struct AudioSystem {
pub channels: u32, pub channels: u32,
/// Event receiver for polling audio events (only present when no EventEmitter is provided) /// Event receiver for polling audio events (only present when no EventEmitter is provided)
pub event_rx: Option<rtrb::Consumer<AudioEvent>>, pub event_rx: Option<rtrb::Consumer<AudioEvent>>,
/// Consumer for recording audio mirror (streams recorded samples to UI for live waveform)
recording_mirror_rx: Option<rtrb::Consumer<f32>>,
} }
impl AudioSystem { impl AudioSystem {
@ -85,9 +87,13 @@ impl AudioSystem {
let input_buffer_size = (sample_rate * channels * 10) as usize; let input_buffer_size = (sample_rate * channels * 10) as usize;
let (mut input_tx, input_rx) = rtrb::RingBuffer::new(input_buffer_size); let (mut input_tx, input_rx) = rtrb::RingBuffer::new(input_buffer_size);
// Create mirror ringbuffer for streaming recorded audio to UI (live waveform)
let (mirror_tx, mirror_rx) = rtrb::RingBuffer::new(input_buffer_size);
// Create engine // Create engine
let mut engine = Engine::new(sample_rate, channels, command_rx, event_tx, query_rx, query_response_tx); let mut engine = Engine::new(sample_rate, channels, command_rx, event_tx, query_rx, query_response_tx);
engine.set_input_rx(input_rx); engine.set_input_rx(input_rx);
engine.set_recording_mirror_tx(mirror_tx);
let controller = engine.get_controller(command_tx, query_tx, query_response_rx); let controller = engine.get_controller(command_tx, query_tx, query_response_rx);
// Initialize MIDI input manager for external MIDI devices // Initialize MIDI input manager for external MIDI devices
@ -151,6 +157,7 @@ impl AudioSystem {
sample_rate, sample_rate,
channels, channels,
event_rx: None, // No event receiver when audio device unavailable event_rx: None, // No event receiver when audio device unavailable
recording_mirror_rx: None,
}); });
} }
}; };
@ -176,6 +183,7 @@ impl AudioSystem {
sample_rate, sample_rate,
channels, channels,
event_rx: None, event_rx: None,
recording_mirror_rx: None,
}); });
} }
}; };
@ -207,6 +215,7 @@ impl AudioSystem {
sample_rate, sample_rate,
channels, channels,
event_rx: None, event_rx: None,
recording_mirror_rx: None,
}); });
} }
}; };
@ -232,9 +241,15 @@ impl AudioSystem {
sample_rate, sample_rate,
channels, channels,
event_rx: event_rx_option, event_rx: event_rx_option,
recording_mirror_rx: Some(mirror_rx),
}) })
} }
/// Take the recording mirror consumer for streaming recorded audio to UI
pub fn take_recording_mirror_rx(&mut self) -> Option<rtrb::Consumer<f32>> {
self.recording_mirror_rx.take()
}
/// Spawn a background thread to emit events from the ringbuffer /// Spawn a background thread to emit events from the ringbuffer
fn spawn_emitter_thread(mut event_rx: rtrb::Consumer<AudioEvent>, emitter: std::sync::Arc<dyn EventEmitter>) { fn spawn_emitter_thread(mut event_rx: rtrb::Consumer<AudioEvent>, emitter: std::sync::Arc<dyn EventEmitter>) {
std::thread::spawn(move || { std::thread::spawn(move || {

View File

@ -667,9 +667,11 @@ struct EditorApp {
audio_pools_with_new_waveforms: HashSet<usize>, audio_pools_with_new_waveforms: HashSet<usize>,
/// Raw audio sample cache for GPU waveform rendering /// Raw audio sample cache for GPU waveform rendering
/// Format: pool_index -> (samples, sample_rate, channels) /// Format: pool_index -> (samples, sample_rate, channels)
raw_audio_cache: HashMap<usize, (Vec<f32>, u32, u32)>, raw_audio_cache: HashMap<usize, (Arc<Vec<f32>>, u32, u32)>,
/// Pool indices that need GPU texture upload (set when raw audio arrives, cleared after upload) /// Pool indices that need GPU texture upload (set when raw audio arrives, cleared after upload)
waveform_gpu_dirty: HashSet<usize>, waveform_gpu_dirty: HashSet<usize>,
/// Consumer for recording audio mirror (streams recorded samples to UI for live waveform)
recording_mirror_rx: Option<rtrb::Consumer<f32>>,
/// Current file path (None if not yet saved) /// Current file path (None if not yet saved)
current_file_path: Option<std::path::PathBuf>, current_file_path: Option<std::path::PathBuf>,
/// Application configuration (recent files, etc.) /// Application configuration (recent files, etc.)
@ -771,12 +773,13 @@ impl EditorApp {
let action_executor = lightningbeam_core::action::ActionExecutor::new(document); let action_executor = lightningbeam_core::action::ActionExecutor::new(document);
// Initialize audio system and destructure it for sharing // Initialize audio system and destructure it for sharing
let (audio_stream, audio_controller, audio_event_rx, audio_sample_rate, audio_channels, file_command_tx) = let (audio_stream, audio_controller, audio_event_rx, audio_sample_rate, audio_channels, file_command_tx, recording_mirror_rx) =
match daw_backend::AudioSystem::new(None, config.audio_buffer_size) { match daw_backend::AudioSystem::new(None, config.audio_buffer_size) {
Ok(audio_system) => { Ok(mut audio_system) => {
println!("✅ Audio engine initialized successfully"); println!("✅ Audio engine initialized successfully");
// Extract components // Extract components
let mirror_rx = audio_system.take_recording_mirror_rx();
let stream = audio_system.stream; let stream = audio_system.stream;
let sample_rate = audio_system.sample_rate; let sample_rate = audio_system.sample_rate;
let channels = audio_system.channels; let channels = audio_system.channels;
@ -788,7 +791,7 @@ impl EditorApp {
// Spawn file operations worker // Spawn file operations worker
let file_command_tx = FileOperationsWorker::spawn(controller.clone()); let file_command_tx = FileOperationsWorker::spawn(controller.clone());
(Some(stream), Some(controller), event_rx, sample_rate, channels, file_command_tx) (Some(stream), Some(controller), event_rx, sample_rate, channels, file_command_tx, mirror_rx)
} }
Err(e) => { Err(e) => {
eprintln!("❌ Failed to initialize audio engine: {}", e); eprintln!("❌ Failed to initialize audio engine: {}", e);
@ -796,7 +799,7 @@ impl EditorApp {
// Create a dummy channel for file operations (won't be used) // Create a dummy channel for file operations (won't be used)
let (tx, _rx) = std::sync::mpsc::channel(); let (tx, _rx) = std::sync::mpsc::channel();
(None, None, None, 48000, 2, tx) (None, None, None, 48000, 2, tx, None)
} }
}; };
@ -872,6 +875,7 @@ impl EditorApp {
audio_pools_with_new_waveforms: HashSet::new(), // Track pool indices with new raw audio audio_pools_with_new_waveforms: HashSet::new(), // Track pool indices with new raw audio
raw_audio_cache: HashMap::new(), raw_audio_cache: HashMap::new(),
waveform_gpu_dirty: HashSet::new(), waveform_gpu_dirty: HashSet::new(),
recording_mirror_rx,
current_file_path: None, // No file loaded initially current_file_path: None, // No file loaded initially
config, config,
file_command_tx, file_command_tx,
@ -2701,7 +2705,7 @@ impl EditorApp {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
match controller.get_pool_audio_samples(pool_index) { match controller.get_pool_audio_samples(pool_index) {
Ok((samples, sr, ch)) => { Ok((samples, sr, ch)) => {
self.raw_audio_cache.insert(pool_index, (samples, sr, ch)); self.raw_audio_cache.insert(pool_index, (Arc::new(samples), sr, ch));
self.waveform_gpu_dirty.insert(pool_index); self.waveform_gpu_dirty.insert(pool_index);
raw_fetched += 1; raw_fetched += 1;
} }
@ -3516,7 +3520,7 @@ impl EditorApp {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
match controller.get_pool_audio_samples(pool_index) { match controller.get_pool_audio_samples(pool_index) {
Ok((samples, sr, ch)) => { Ok((samples, sr, ch)) => {
self.raw_audio_cache.insert(pool_index, (samples, sr, ch)); self.raw_audio_cache.insert(pool_index, (Arc::new(samples), sr, ch));
self.waveform_gpu_dirty.insert(pool_index); self.waveform_gpu_dirty.insert(pool_index);
} }
Err(e) => eprintln!("Failed to fetch raw audio for extracted audio: {}", e), Err(e) => eprintln!("Failed to fetch raw audio for extracted audio: {}", e),
@ -3738,6 +3742,24 @@ impl eframe::App for EditorApp {
ctx.request_repaint(); ctx.request_repaint();
} }
// Drain recording mirror buffer for live waveform display
if self.is_recording {
if let Some(ref mut mirror_rx) = self.recording_mirror_rx {
let mut drained = 0usize;
if let Some(entry) = self.raw_audio_cache.get_mut(&usize::MAX) {
let samples = Arc::make_mut(&mut entry.0);
while let Ok(sample) = mirror_rx.pop() {
samples.push(sample);
drained += 1;
}
}
if drained > 0 {
self.waveform_gpu_dirty.insert(usize::MAX);
ctx.request_repaint();
}
}
}
// Poll audio events from the audio engine // Poll audio events from the audio engine
if let Some(event_rx) = &mut self.audio_event_rx { if let Some(event_rx) = &mut self.audio_event_rx {
let mut polled_events = false; let mut polled_events = false;
@ -3777,7 +3799,7 @@ impl eframe::App for EditorApp {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
match controller.get_pool_audio_samples(pool_index) { match controller.get_pool_audio_samples(pool_index) {
Ok((samples, sr, ch)) => { Ok((samples, sr, ch)) => {
self.raw_audio_cache.insert(pool_index, (samples, sr, ch)); self.raw_audio_cache.insert(pool_index, (Arc::new(samples), sr, ch));
self.waveform_gpu_dirty.insert(pool_index); self.waveform_gpu_dirty.insert(pool_index);
self.audio_pools_with_new_waveforms.insert(pool_index); self.audio_pools_with_new_waveforms.insert(pool_index);
} }
@ -3789,7 +3811,7 @@ impl eframe::App for EditorApp {
ctx.request_repaint(); ctx.request_repaint();
} }
// Recording events // Recording events
AudioEvent::RecordingStarted(track_id, backend_clip_id) => { AudioEvent::RecordingStarted(track_id, backend_clip_id, rec_sample_rate, rec_channels) => {
println!("🎤 Recording started on track {:?}, backend_clip_id={}", track_id, backend_clip_id); println!("🎤 Recording started on track {:?}, backend_clip_id={}", track_id, backend_clip_id);
// Create clip in document and add instance to layer // Create clip in document and add instance to layer
@ -3817,6 +3839,10 @@ impl eframe::App for EditorApp {
// Store mapping for later updates // Store mapping for later updates
self.recording_clips.insert(layer_id, backend_clip_id); self.recording_clips.insert(layer_id, backend_clip_id);
} }
// Initialize live waveform cache for recording
self.raw_audio_cache.insert(usize::MAX, (Arc::new(Vec::new()), rec_sample_rate, rec_channels));
ctx.request_repaint(); ctx.request_repaint();
} }
AudioEvent::RecordingProgress(_clip_id, duration) => { AudioEvent::RecordingProgress(_clip_id, duration) => {
@ -3850,12 +3876,16 @@ impl eframe::App for EditorApp {
AudioEvent::RecordingStopped(_backend_clip_id, pool_index, _waveform) => { AudioEvent::RecordingStopped(_backend_clip_id, pool_index, _waveform) => {
println!("🎤 Recording stopped: pool_index={}", pool_index); println!("🎤 Recording stopped: pool_index={}", pool_index);
// Clean up live recording waveform cache
self.raw_audio_cache.remove(&usize::MAX);
self.waveform_gpu_dirty.remove(&usize::MAX);
// Fetch raw audio samples for GPU waveform rendering // Fetch raw audio samples for GPU waveform rendering
if let Some(ref controller_arc) = self.audio_controller { if let Some(ref controller_arc) = self.audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
match controller.get_pool_audio_samples(pool_index) { match controller.get_pool_audio_samples(pool_index) {
Ok((samples, sr, ch)) => { Ok((samples, sr, ch)) => {
self.raw_audio_cache.insert(pool_index, (samples, sr, ch)); self.raw_audio_cache.insert(pool_index, (Arc::new(samples), sr, ch));
self.waveform_gpu_dirty.insert(pool_index); self.waveform_gpu_dirty.insert(pool_index);
self.audio_pools_with_new_waveforms.insert(pool_index); self.audio_pools_with_new_waveforms.insert(pool_index);
} }
@ -4074,7 +4104,7 @@ impl eframe::App for EditorApp {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
match controller.get_pool_audio_samples(pool_index) { match controller.get_pool_audio_samples(pool_index) {
Ok((samples, sr, ch)) => { Ok((samples, sr, ch)) => {
self.raw_audio_cache.insert(pool_index, (samples, sr, ch)); self.raw_audio_cache.insert(pool_index, (Arc::new(samples), sr, ch));
self.waveform_gpu_dirty.insert(pool_index); self.waveform_gpu_dirty.insert(pool_index);
} }
Err(e) => eprintln!("Failed to fetch raw audio for pool {}: {}", pool_index, e), Err(e) => eprintln!("Failed to fetch raw audio for pool {}: {}", pool_index, e),
@ -4088,9 +4118,9 @@ impl eframe::App for EditorApp {
AudioEvent::AudioDecodeProgress { pool_index, samples, sample_rate, channels } => { AudioEvent::AudioDecodeProgress { pool_index, samples, sample_rate, channels } => {
// Samples arrive as deltas — append to existing cache // Samples arrive as deltas — append to existing cache
if let Some(entry) = self.raw_audio_cache.get_mut(&pool_index) { if let Some(entry) = self.raw_audio_cache.get_mut(&pool_index) {
entry.0.extend_from_slice(&samples); Arc::make_mut(&mut entry.0).extend_from_slice(&samples);
} else { } else {
self.raw_audio_cache.insert(pool_index, (samples, sample_rate, channels)); self.raw_audio_cache.insert(pool_index, (Arc::new(samples), sample_rate, channels));
} }
self.waveform_gpu_dirty.insert(pool_index); self.waveform_gpu_dirty.insert(pool_index);
ctx.request_repaint(); ctx.request_repaint();
@ -4680,7 +4710,7 @@ struct RenderContext<'a> {
/// Audio pool indices with new raw audio data this frame (for thumbnail invalidation) /// Audio pool indices with new raw audio data this frame (for thumbnail invalidation)
audio_pools_with_new_waveforms: &'a HashSet<usize>, audio_pools_with_new_waveforms: &'a HashSet<usize>,
/// Raw audio samples for GPU waveform rendering (pool_index -> (samples, sample_rate, channels)) /// Raw audio samples for GPU waveform rendering (pool_index -> (samples, sample_rate, channels))
raw_audio_cache: &'a HashMap<usize, (Vec<f32>, u32, u32)>, raw_audio_cache: &'a HashMap<usize, (Arc<Vec<f32>>, u32, u32)>,
/// Pool indices needing GPU texture upload /// Pool indices needing GPU texture upload
waveform_gpu_dirty: &'a mut HashSet<usize>, waveform_gpu_dirty: &'a mut HashSet<usize>,
/// Effect ID to load into shader editor (set by asset library, consumed by shader editor) /// Effect ID to load into shader editor (set by asset library, consumed by shader editor)

View File

@ -20,7 +20,7 @@ use crate::widgets::ImeTextField;
/// Derive min/max peak pairs from raw audio samples for thumbnail rendering. /// Derive min/max peak pairs from raw audio samples for thumbnail rendering.
/// Downsamples to `num_peaks` (min, max) pairs by scanning chunks of samples. /// Downsamples to `num_peaks` (min, max) pairs by scanning chunks of samples.
fn peaks_from_raw_audio( fn peaks_from_raw_audio(
raw: &(Vec<f32>, u32, u32), // (samples, sample_rate, channels) raw: &(std::sync::Arc<Vec<f32>>, u32, u32), // (samples, sample_rate, channels)
num_peaks: usize, num_peaks: usize,
) -> Vec<(f32, f32)> { ) -> Vec<(f32, f32)> {
let (samples, _sr, channels) = raw; let (samples, _sr, channels) = raw;

View File

@ -198,7 +198,7 @@ pub struct SharedPaneState<'a> {
/// Audio pool indices that got new raw audio data this frame (for thumbnail invalidation) /// Audio pool indices that got new raw audio data this frame (for thumbnail invalidation)
pub audio_pools_with_new_waveforms: &'a std::collections::HashSet<usize>, pub audio_pools_with_new_waveforms: &'a std::collections::HashSet<usize>,
/// Raw audio samples for GPU waveform rendering (pool_index -> (samples, sample_rate, channels)) /// Raw audio samples for GPU waveform rendering (pool_index -> (samples, sample_rate, channels))
pub raw_audio_cache: &'a std::collections::HashMap<usize, (Vec<f32>, u32, u32)>, pub raw_audio_cache: &'a std::collections::HashMap<usize, (std::sync::Arc<Vec<f32>>, u32, u32)>,
/// Pool indices needing GPU waveform texture upload /// Pool indices needing GPU waveform texture upload
pub waveform_gpu_dirty: &'a mut std::collections::HashSet<usize>, pub waveform_gpu_dirty: &'a mut std::collections::HashSet<usize>,
/// Effect ID to load into shader editor (set by asset library, consumed by shader editor) /// Effect ID to load into shader editor (set by asset library, consumed by shader editor)

View File

@ -86,8 +86,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let mip_frame = frame_f / reduction; let mip_frame = frame_f / reduction;
// Convert 1D mip-space index to 2D UV coordinates // Convert 1D mip-space index to 2D UV coordinates
let mip_tex_width = params.tex_width / pow(2.0, f32(mip_floor)); // Use actual texture dimensions (not computed from total_frames) because the
let mip_tex_height = ceil(params.total_frames / reduction / mip_tex_width); // texture may be pre-allocated larger for live recording.
let mip_dims = textureDimensions(peak_tex, mip_floor);
let mip_tex_width = f32(mip_dims.x);
let mip_tex_height = f32(mip_dims.y);
let texel_x = mip_frame % mip_tex_width; let texel_x = mip_frame % mip_tex_width;
let texel_y = floor(mip_frame / mip_tex_width); let texel_y = floor(mip_frame / mip_tex_width);
let uv = vec2((texel_x + 0.5) / mip_tex_width, (texel_y + 0.5) / mip_tex_height); let uv = vec2((texel_x + 0.5) / mip_tex_width, (texel_y + 0.5) / mip_tex_height);

View File

@ -925,7 +925,7 @@ impl TimelinePane {
active_layer_id: &Option<uuid::Uuid>, active_layer_id: &Option<uuid::Uuid>,
selection: &lightningbeam_core::selection::Selection, selection: &lightningbeam_core::selection::Selection,
midi_event_cache: &std::collections::HashMap<u32, Vec<(f64, u8, u8, bool)>>, midi_event_cache: &std::collections::HashMap<u32, Vec<(f64, u8, u8, bool)>>,
raw_audio_cache: &std::collections::HashMap<usize, (Vec<f32>, u32, u32)>, raw_audio_cache: &std::collections::HashMap<usize, (std::sync::Arc<Vec<f32>>, u32, u32)>,
waveform_gpu_dirty: &mut std::collections::HashSet<usize>, waveform_gpu_dirty: &mut std::collections::HashSet<usize>,
target_format: wgpu::TextureFormat, target_format: wgpu::TextureFormat,
waveform_stereo: bool, waveform_stereo: bool,
@ -1292,9 +1292,74 @@ impl TimelinePane {
} }
} }
} }
// Recording in progress: no visualization yet // Recording in progress: show live waveform
lightningbeam_core::clip::AudioClipType::Recording => { lightningbeam_core::clip::AudioClipType::Recording => {
// Could show a pulsing "Recording..." indicator here let rec_pool_idx = usize::MAX;
if let Some((samples, sr, ch)) = raw_audio_cache.get(&rec_pool_idx) {
let total_frames = samples.len() / (*ch).max(1) as usize;
if total_frames > 0 {
let audio_file_duration = total_frames as f64 / *sr as f64;
let screen_size = ui.ctx().content_rect().size();
let pending_upload = if waveform_gpu_dirty.contains(&rec_pool_idx) {
waveform_gpu_dirty.remove(&rec_pool_idx);
Some(crate::waveform_gpu::PendingUpload {
samples: samples.clone(),
sample_rate: *sr,
channels: *ch,
})
} else {
None
};
let tint = [
bright_color.r() as f32 / 255.0,
bright_color.g() as f32 / 255.0,
bright_color.b() as f32 / 255.0,
bright_color.a() as f32 / 255.0,
];
let clip_screen_start = rect.min.x + ((instance_start - self.viewport_start_time) * self.pixels_per_second as f64) as f32;
let clip_screen_end = clip_screen_start + (preview_clip_duration * self.pixels_per_second as f64) as f32;
let waveform_rect = egui::Rect::from_min_max(
egui::pos2(clip_screen_start.max(clip_rect.min.x), clip_rect.min.y),
egui::pos2(clip_screen_end.min(clip_rect.max.x), clip_rect.max.y),
);
if waveform_rect.width() > 0.0 && waveform_rect.height() > 0.0 {
let instance_id = clip_instance.id.as_u128() as u64;
let callback = crate::waveform_gpu::WaveformCallback {
pool_index: rec_pool_idx,
segment_index: 0,
params: crate::waveform_gpu::WaveformParams {
clip_rect: [waveform_rect.min.x, waveform_rect.min.y, waveform_rect.max.x, waveform_rect.max.y],
viewport_start_time: self.viewport_start_time as f32,
pixels_per_second: self.pixels_per_second as f32,
audio_duration: audio_file_duration as f32,
sample_rate: *sr as f32,
clip_start_time: clip_screen_start,
trim_start: preview_trim_start as f32,
tex_width: crate::waveform_gpu::tex_width() as f32,
total_frames: total_frames as f32,
segment_start_frame: 0.0,
display_mode: if waveform_stereo { 1.0 } else { 0.0 },
_pad1: [0.0, 0.0],
tint_color: tint,
screen_size: [screen_size.x, screen_size.y],
_pad: [0.0, 0.0],
},
target_format,
pending_upload,
instance_id,
};
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
waveform_rect,
callback,
));
}
}
}
} }
} }
} }

View File

@ -104,7 +104,7 @@ pub struct WaveformCallback {
/// Raw audio data waiting to be uploaded to GPU /// Raw audio data waiting to be uploaded to GPU
pub struct PendingUpload { pub struct PendingUpload {
pub samples: Vec<f32>, pub samples: std::sync::Arc<Vec<f32>>,
pub sample_rate: u32, pub sample_rate: u32,
pub channels: u32, pub channels: u32,
} }
@ -378,10 +378,21 @@ impl WaveformGpuResources {
let total_frames = new_total_frames; let total_frames = new_total_frames;
// For live recording (pool_index == usize::MAX), pre-allocate extra texture
// height to avoid frequent full recreates as recording grows.
// Allocate 60 seconds ahead so incremental updates can fill without recreating.
let alloc_frames = if pool_index == usize::MAX {
let extra = sample_rate as usize * 60; // 60s of mono frames (texture is per-frame, not per-sample)
total_frames + extra
} else {
total_frames
};
let max_frames_per_segment = (TEX_WIDTH as u64) let max_frames_per_segment = (TEX_WIDTH as u64)
* (device.limits().max_texture_dimension_2d as u64); * (device.limits().max_texture_dimension_2d as u64);
// Use alloc_frames for texture sizing but total_frames for data
let segment_count = let segment_count =
((total_frames as u64 + max_frames_per_segment - 1) / max_frames_per_segment) as usize; ((total_frames as u64 + max_frames_per_segment - 1) / max_frames_per_segment).max(1) as usize;
let frames_per_segment = if segment_count == 1 { let frames_per_segment = if segment_count == 1 {
total_frames as u32 total_frames as u32
} else { } else {
@ -400,7 +411,13 @@ impl WaveformGpuResources {
.min(total_frames as u64); .min(total_frames as u64);
let seg_frame_count = (seg_end_frame - seg_start_frame) as u32; let seg_frame_count = (seg_end_frame - seg_start_frame) as u32;
let tex_height = (seg_frame_count + TEX_WIDTH - 1) / TEX_WIDTH; // Allocate texture large enough for future growth (recording) or exact fit (normal)
let alloc_seg_frames = if pool_index == usize::MAX {
(alloc_frames as u32).min(seg_frame_count + sample_rate * 60)
} else {
seg_frame_count
};
let tex_height = (alloc_seg_frames + TEX_WIDTH - 1) / TEX_WIDTH;
let mip_count = compute_mip_count(TEX_WIDTH, tex_height); let mip_count = compute_mip_count(TEX_WIDTH, tex_height);
// Create texture with mip levels // Create texture with mip levels
@ -422,8 +439,10 @@ impl WaveformGpuResources {
}); });
// Pack raw samples into Rgba16Float data for mip 0 // Pack raw samples into Rgba16Float data for mip 0
let texel_count = (TEX_WIDTH * tex_height) as usize; // Only pack rows containing actual data (not the pre-allocated empty region)
let mut mip0_data: Vec<half::f16> = vec![half::f16::ZERO; texel_count * 4]; let data_height = (seg_frame_count + TEX_WIDTH - 1) / TEX_WIDTH;
let data_texel_count = (TEX_WIDTH * data_height) as usize;
let mut mip0_data: Vec<half::f16> = vec![half::f16::ZERO; data_texel_count * 4];
for frame in 0..seg_frame_count as usize { for frame in 0..seg_frame_count as usize {
let global_frame = seg_start_frame as usize + frame; let global_frame = seg_start_frame as usize + frame;
@ -447,7 +466,8 @@ impl WaveformGpuResources {
mip0_data[texel_offset + 3] = half::f16::from_f32(right); mip0_data[texel_offset + 3] = half::f16::from_f32(right);
} }
// Upload mip 0 // Upload mip 0 (only rows with actual data)
if data_height > 0 {
queue.write_texture( queue.write_texture(
wgpu::TexelCopyTextureInfo { wgpu::TexelCopyTextureInfo {
texture: &texture, texture: &texture,
@ -459,14 +479,15 @@ impl WaveformGpuResources {
wgpu::TexelCopyBufferLayout { wgpu::TexelCopyBufferLayout {
offset: 0, offset: 0,
bytes_per_row: Some(TEX_WIDTH * 8), bytes_per_row: Some(TEX_WIDTH * 8),
rows_per_image: Some(tex_height), rows_per_image: Some(data_height),
}, },
wgpu::Extent3d { wgpu::Extent3d {
width: TEX_WIDTH, width: TEX_WIDTH,
height: tex_height, height: data_height,
depth_or_array_layers: 1, depth_or_array_layers: 1,
}, },
); );
}
// Generate mipmaps via compute shader // Generate mipmaps via compute shader
let cmds = self.generate_mipmaps( let cmds = self.generate_mipmaps(
@ -528,7 +549,7 @@ impl WaveformGpuResources {
uniform_buffers, uniform_buffers,
frames_per_segment, frames_per_segment,
total_frames: total_frames as u64, total_frames: total_frames as u64,
tex_height: (total_frames as u32 + TEX_WIDTH - 1) / TEX_WIDTH, tex_height: (alloc_frames as u32 + TEX_WIDTH - 1) / TEX_WIDTH,
sample_rate, sample_rate,
channels, channels,
}, },

View File

@ -48,10 +48,10 @@ raw-window-handle = "0.6"
image = "0.24" image = "0.24"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
ffmpeg-next = { version = "7.0", features = ["build"] } ffmpeg-next = { version = "8.0", features = ["build"] }
[target.'cfg(not(target_os = "macos"))'.dependencies] [target.'cfg(not(target_os = "macos"))'.dependencies]
ffmpeg-next = "7.0" ffmpeg-next = "8.0"
[profile.dev] [profile.dev]

View File

@ -83,7 +83,7 @@ impl EventEmitter for TauriEventEmitter {
AudioEvent::PlaybackPosition(time) => { AudioEvent::PlaybackPosition(time) => {
SerializedAudioEvent::PlaybackPosition { time } SerializedAudioEvent::PlaybackPosition { time }
} }
AudioEvent::RecordingStarted(track_id, clip_id) => { AudioEvent::RecordingStarted(track_id, clip_id, _, _) => {
SerializedAudioEvent::RecordingStarted { track_id, clip_id } SerializedAudioEvent::RecordingStarted { track_id, clip_id }
} }
AudioEvent::RecordingProgress(clip_id, duration) => { AudioEvent::RecordingProgress(clip_id, duration) => {