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
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 || {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue