From 21a49235fcb385cbf2de18579d8d7d98b6626c79 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 17 Feb 2026 10:08:49 -0500 Subject: [PATCH] sampler improvements, live waveform preview --- daw-backend/src/audio/engine.rs | 25 ++++++- daw-backend/src/command/types.rs | 4 +- daw-backend/src/lib.rs | 15 ++++ .../lightningbeam-editor/src/main.rs | 58 +++++++++++---- .../src/panes/asset_library.rs | 2 +- .../lightningbeam-editor/src/panes/mod.rs | 2 +- .../src/panes/shaders/waveform.wgsl | 7 +- .../src/panes/timeline.rs | 71 +++++++++++++++++- .../lightningbeam-editor/src/waveform_gpu.rs | 73 ++++++++++++------- src-tauri/Cargo.toml | 4 +- src-tauri/src/audio.rs | 2 +- 11 files changed, 210 insertions(+), 53 deletions(-) diff --git a/daw-backend/src/audio/engine.rs b/daw-backend/src/audio/engine.rs index 62593cc..205950e 100644 --- a/daw-backend/src/audio/engine.rs +++ b/daw-backend/src/audio/engine.rs @@ -53,6 +53,7 @@ pub struct Engine { // Recording state recording_state: Option, input_rx: Option>, + recording_mirror_tx: Option>, recording_progress_counter: usize, // MIDI recording state @@ -130,6 +131,7 @@ impl Engine { next_clip_id: 0, recording_state: None, input_rx: None, + recording_mirror_tx: None, recording_progress_counter: 0, midi_recording_state: None, midi_input_manager: None, @@ -151,6 +153,11 @@ impl Engine { 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) { + self.recording_mirror_tx = Some(tx); + } + /// Set the MIDI input manager for external MIDI devices pub fn set_midi_input_manager(&mut self, manager: MidiInputManager) { self.midi_input_manager = Some(manager); @@ -393,8 +400,24 @@ impl Engine { // Add samples to recording 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) { 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 let duration = recording.duration(); let clip_id = recording.clip_id; @@ -2540,7 +2563,7 @@ impl Engine { } // 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) => { // Send error event to UI diff --git a/daw-backend/src/command/types.rs b/daw-backend/src/command/types.rs index 16ce411..1a70a5a 100644 --- a/daw-backend/src/command/types.rs +++ b/daw-backend/src/command/types.rs @@ -235,8 +235,8 @@ pub enum AudioEvent { BufferPoolStats(BufferPoolStats), /// Automation lane created (track_id, lane_id, parameter_id) AutomationLaneCreated(TrackId, AutomationLaneId, ParameterId), - /// Recording started (track_id, clip_id) - RecordingStarted(TrackId, ClipId), + /// Recording started (track_id, clip_id, sample_rate, channels) + RecordingStarted(TrackId, ClipId, u32, u32), /// Recording progress update (clip_id, current_duration) RecordingProgress(ClipId, f64), /// Recording stopped (clip_id, pool_index, waveform) diff --git a/daw-backend/src/lib.rs b/daw-backend/src/lib.rs index bb7c664..9ef18c8 100644 --- a/daw-backend/src/lib.rs +++ b/daw-backend/src/lib.rs @@ -39,6 +39,8 @@ pub struct AudioSystem { pub channels: u32, /// Event receiver for polling audio events (only present when no EventEmitter is provided) pub event_rx: Option>, + /// Consumer for recording audio mirror (streams recorded samples to UI for live waveform) + recording_mirror_rx: Option>, } impl AudioSystem { @@ -85,9 +87,13 @@ impl AudioSystem { let input_buffer_size = (sample_rate * channels * 10) as usize; 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 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_recording_mirror_tx(mirror_tx); let controller = engine.get_controller(command_tx, query_tx, query_response_rx); // Initialize MIDI input manager for external MIDI devices @@ -151,6 +157,7 @@ impl AudioSystem { sample_rate, channels, event_rx: None, // No event receiver when audio device unavailable + recording_mirror_rx: None, }); } }; @@ -176,6 +183,7 @@ impl AudioSystem { sample_rate, channels, event_rx: None, + recording_mirror_rx: None, }); } }; @@ -207,6 +215,7 @@ impl AudioSystem { sample_rate, channels, event_rx: None, + recording_mirror_rx: None, }); } }; @@ -232,9 +241,15 @@ impl AudioSystem { sample_rate, channels, 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> { + self.recording_mirror_rx.take() + } + /// Spawn a background thread to emit events from the ringbuffer fn spawn_emitter_thread(mut event_rx: rtrb::Consumer, emitter: std::sync::Arc) { std::thread::spawn(move || { diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index 74957df..0ee0fbc 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -667,9 +667,11 @@ struct EditorApp { audio_pools_with_new_waveforms: HashSet, /// Raw audio sample cache for GPU waveform rendering /// Format: pool_index -> (samples, sample_rate, channels) - raw_audio_cache: HashMap, u32, u32)>, + raw_audio_cache: HashMap>, u32, u32)>, /// Pool indices that need GPU texture upload (set when raw audio arrives, cleared after upload) waveform_gpu_dirty: HashSet, + /// Consumer for recording audio mirror (streams recorded samples to UI for live waveform) + recording_mirror_rx: Option>, /// Current file path (None if not yet saved) current_file_path: Option, /// Application configuration (recent files, etc.) @@ -771,12 +773,13 @@ impl EditorApp { let action_executor = lightningbeam_core::action::ActionExecutor::new(document); // 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) { - Ok(audio_system) => { + Ok(mut audio_system) => { println!("✅ Audio engine initialized successfully"); // Extract components + let mirror_rx = audio_system.take_recording_mirror_rx(); let stream = audio_system.stream; let sample_rate = audio_system.sample_rate; let channels = audio_system.channels; @@ -788,7 +791,7 @@ impl EditorApp { // Spawn file operations worker 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) => { eprintln!("❌ Failed to initialize audio engine: {}", e); @@ -796,7 +799,7 @@ impl EditorApp { // Create a dummy channel for file operations (won't be used) 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 raw_audio_cache: HashMap::new(), waveform_gpu_dirty: HashSet::new(), + recording_mirror_rx, current_file_path: None, // No file loaded initially config, file_command_tx, @@ -2701,7 +2705,7 @@ impl EditorApp { 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.raw_audio_cache.insert(pool_index, (Arc::new(samples), sr, ch)); self.waveform_gpu_dirty.insert(pool_index); raw_fetched += 1; } @@ -3516,7 +3520,7 @@ impl EditorApp { 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.raw_audio_cache.insert(pool_index, (Arc::new(samples), sr, ch)); self.waveform_gpu_dirty.insert(pool_index); } Err(e) => eprintln!("Failed to fetch raw audio for extracted audio: {}", e), @@ -3738,6 +3742,24 @@ impl eframe::App for EditorApp { 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 if let Some(event_rx) = &mut self.audio_event_rx { let mut polled_events = false; @@ -3777,7 +3799,7 @@ impl eframe::App for EditorApp { 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.raw_audio_cache.insert(pool_index, (Arc::new(samples), sr, ch)); self.waveform_gpu_dirty.insert(pool_index); self.audio_pools_with_new_waveforms.insert(pool_index); } @@ -3789,7 +3811,7 @@ impl eframe::App for EditorApp { ctx.request_repaint(); } // 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); // Create clip in document and add instance to layer @@ -3817,6 +3839,10 @@ impl eframe::App for EditorApp { // Store mapping for later updates 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(); } AudioEvent::RecordingProgress(_clip_id, duration) => { @@ -3850,12 +3876,16 @@ impl eframe::App for EditorApp { AudioEvent::RecordingStopped(_backend_clip_id, pool_index, _waveform) => { 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 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.raw_audio_cache.insert(pool_index, (Arc::new(samples), sr, ch)); self.waveform_gpu_dirty.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(); match controller.get_pool_audio_samples(pool_index) { 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); } 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 } => { // 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); + Arc::make_mut(&mut entry.0).extend_from_slice(&samples); } 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); ctx.request_repaint(); @@ -4680,7 +4710,7 @@ struct RenderContext<'a> { /// Audio pool indices with new raw audio data this frame (for thumbnail invalidation) audio_pools_with_new_waveforms: &'a HashSet, /// Raw audio samples for GPU waveform rendering (pool_index -> (samples, sample_rate, channels)) - raw_audio_cache: &'a HashMap, u32, u32)>, + raw_audio_cache: &'a HashMap>, u32, u32)>, /// Pool indices needing GPU texture upload waveform_gpu_dirty: &'a mut HashSet, /// Effect ID to load into shader editor (set by asset library, consumed by shader editor) diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs index 74696c9..6e9c751 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/asset_library.rs @@ -20,7 +20,7 @@ use crate::widgets::ImeTextField; /// Derive min/max peak pairs from raw audio samples for thumbnail rendering. /// Downsamples to `num_peaks` (min, max) pairs by scanning chunks of samples. fn peaks_from_raw_audio( - raw: &(Vec, u32, u32), // (samples, sample_rate, channels) + raw: &(std::sync::Arc>, u32, u32), // (samples, sample_rate, channels) num_peaks: usize, ) -> Vec<(f32, f32)> { let (samples, _sr, channels) = raw; diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs index a5f1614..0b45b08 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/mod.rs @@ -198,7 +198,7 @@ pub struct SharedPaneState<'a> { /// Audio pool indices that got new raw audio data this frame (for thumbnail invalidation) pub audio_pools_with_new_waveforms: &'a std::collections::HashSet, /// Raw audio samples for GPU waveform rendering (pool_index -> (samples, sample_rate, channels)) - pub raw_audio_cache: &'a std::collections::HashMap, u32, u32)>, + pub raw_audio_cache: &'a std::collections::HashMap>, u32, u32)>, /// Pool indices needing GPU waveform texture upload pub waveform_gpu_dirty: &'a mut std::collections::HashSet, /// Effect ID to load into shader editor (set by asset library, consumed by shader editor) diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/waveform.wgsl b/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/waveform.wgsl index 40bdb5f..9a857ba 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/waveform.wgsl +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/shaders/waveform.wgsl @@ -86,8 +86,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let mip_frame = frame_f / reduction; // Convert 1D mip-space index to 2D UV coordinates - let mip_tex_width = params.tex_width / pow(2.0, f32(mip_floor)); - let mip_tex_height = ceil(params.total_frames / reduction / mip_tex_width); + // Use actual texture dimensions (not computed from total_frames) because the + // 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_y = floor(mip_frame / mip_tex_width); let uv = vec2((texel_x + 0.5) / mip_tex_width, (texel_y + 0.5) / mip_tex_height); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs index 5ea6601..3d0e81f 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/timeline.rs @@ -925,7 +925,7 @@ impl TimelinePane { active_layer_id: &Option, selection: &lightningbeam_core::selection::Selection, midi_event_cache: &std::collections::HashMap>, - raw_audio_cache: &std::collections::HashMap, u32, u32)>, + raw_audio_cache: &std::collections::HashMap>, u32, u32)>, waveform_gpu_dirty: &mut std::collections::HashSet, target_format: wgpu::TextureFormat, 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 => { - // 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, + )); + } + } + } } } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/waveform_gpu.rs b/lightningbeam-ui/lightningbeam-editor/src/waveform_gpu.rs index 50b6f6c..0477c96 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/waveform_gpu.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/waveform_gpu.rs @@ -104,7 +104,7 @@ pub struct WaveformCallback { /// Raw audio data waiting to be uploaded to GPU pub struct PendingUpload { - pub samples: Vec, + pub samples: std::sync::Arc>, pub sample_rate: u32, pub channels: u32, } @@ -378,10 +378,21 @@ impl WaveformGpuResources { 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) * (device.limits().max_texture_dimension_2d as u64); + // Use alloc_frames for texture sizing but total_frames for data 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 { total_frames as u32 } else { @@ -400,7 +411,13 @@ impl WaveformGpuResources { .min(total_frames as u64); 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); // Create texture with mip levels @@ -422,8 +439,10 @@ impl WaveformGpuResources { }); // Pack raw samples into Rgba16Float data for mip 0 - let texel_count = (TEX_WIDTH * tex_height) as usize; - let mut mip0_data: Vec = vec![half::f16::ZERO; texel_count * 4]; + // Only pack rows containing actual data (not the pre-allocated empty region) + 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 = vec![half::f16::ZERO; data_texel_count * 4]; for frame in 0..seg_frame_count as usize { let global_frame = seg_start_frame as usize + frame; @@ -447,26 +466,28 @@ impl WaveformGpuResources { mip0_data[texel_offset + 3] = half::f16::from_f32(right); } - // Upload mip 0 - queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - bytemuck::cast_slice(&mip0_data), - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(TEX_WIDTH * 8), - rows_per_image: Some(tex_height), - }, - wgpu::Extent3d { - width: TEX_WIDTH, - height: tex_height, - depth_or_array_layers: 1, - }, - ); + // Upload mip 0 (only rows with actual data) + if data_height > 0 { + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + bytemuck::cast_slice(&mip0_data), + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(TEX_WIDTH * 8), + rows_per_image: Some(data_height), + }, + wgpu::Extent3d { + width: TEX_WIDTH, + height: data_height, + depth_or_array_layers: 1, + }, + ); + } // Generate mipmaps via compute shader let cmds = self.generate_mipmaps( @@ -528,7 +549,7 @@ impl WaveformGpuResources { uniform_buffers, frames_per_segment, 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, channels, }, diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2f78fea..b914805 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -48,10 +48,10 @@ raw-window-handle = "0.6" image = "0.24" [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] -ffmpeg-next = "7.0" +ffmpeg-next = "8.0" [profile.dev] diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index 9f4e456..6c6085f 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -83,7 +83,7 @@ impl EventEmitter for TauriEventEmitter { AudioEvent::PlaybackPosition(time) => { SerializedAudioEvent::PlaybackPosition { time } } - AudioEvent::RecordingStarted(track_id, clip_id) => { + AudioEvent::RecordingStarted(track_id, clip_id, _, _) => { SerializedAudioEvent::RecordingStarted { track_id, clip_id } } AudioEvent::RecordingProgress(clip_id, duration) => {