sampler improvements, live waveform preview
This commit is contained in:
parent
c10f42da8f
commit
21a49235fc
|
|
@ -53,6 +53,7 @@ pub struct Engine {
|
|||
// Recording state
|
||||
recording_state: Option<RecordingState>,
|
||||
input_rx: Option<rtrb::Consumer<f32>>,
|
||||
recording_mirror_tx: Option<rtrb::Producer<f32>>,
|
||||
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<f32>) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<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 {
|
||||
|
|
@ -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<rtrb::Consumer<f32>> {
|
||||
self.recording_mirror_rx.take()
|
||||
}
|
||||
|
||||
/// 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>) {
|
||||
std::thread::spawn(move || {
|
||||
|
|
|
|||
|
|
@ -667,9 +667,11 @@ struct EditorApp {
|
|||
audio_pools_with_new_waveforms: HashSet<usize>,
|
||||
/// Raw audio sample cache for GPU waveform rendering
|
||||
/// 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)
|
||||
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: Option<std::path::PathBuf>,
|
||||
/// 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<usize>,
|
||||
/// 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
|
||||
waveform_gpu_dirty: &'a mut HashSet<usize>,
|
||||
/// Effect ID to load into shader editor (set by asset library, consumed by shader editor)
|
||||
|
|
|
|||
|
|
@ -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<f32>, u32, u32), // (samples, sample_rate, channels)
|
||||
raw: &(std::sync::Arc<Vec<f32>>, u32, u32), // (samples, sample_rate, channels)
|
||||
num_peaks: usize,
|
||||
) -> Vec<(f32, f32)> {
|
||||
let (samples, _sr, channels) = raw;
|
||||
|
|
|
|||
|
|
@ -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<usize>,
|
||||
/// 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
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -86,8 +86,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -925,7 +925,7 @@ impl TimelinePane {
|
|||
active_layer_id: &Option<uuid::Uuid>,
|
||||
selection: &lightningbeam_core::selection::Selection,
|
||||
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>,
|
||||
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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ pub struct WaveformCallback {
|
|||
|
||||
/// Raw audio data waiting to be uploaded to GPU
|
||||
pub struct PendingUpload {
|
||||
pub samples: Vec<f32>,
|
||||
pub samples: std::sync::Arc<Vec<f32>>,
|
||||
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<half::f16> = 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<half::f16> = 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,7 +466,8 @@ impl WaveformGpuResources {
|
|||
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(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &texture,
|
||||
|
|
@ -459,14 +479,15 @@ impl WaveformGpuResources {
|
|||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(TEX_WIDTH * 8),
|
||||
rows_per_image: Some(tex_height),
|
||||
rows_per_image: Some(data_height),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: TEX_WIDTH,
|
||||
height: tex_height,
|
||||
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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue