use std::path::{Path, PathBuf}; use std::sync::Arc; use std::f32::consts::PI; use serde::{Deserialize, Serialize}; /// Windowed sinc interpolation for high-quality time stretching /// This is stateless and can handle arbitrary fractional positions #[inline] fn sinc(x: f32) -> f32 { if x.abs() < 1e-5 { 1.0 } else { let px = PI * x; px.sin() / px } } /// Blackman window function #[inline] fn blackman_window(x: f32, width: f32) -> f32 { if x.abs() > width { 0.0 } else { let a0 = 0.42; let a1 = 0.5; let a2 = 0.08; // Map x from [-width, width] to [0, 1] for proper Blackman window evaluation let n = (x / width + 1.0) / 2.0; a0 - a1 * (2.0 * PI * n).cos() + a2 * (4.0 * PI * n).cos() } } /// High-quality windowed sinc interpolation /// Uses a 32-tap windowed sinc kernel for smooth, artifact-free interpolation /// frac: fractional position to interpolate at (0.0 to 1.0) /// samples: array of samples centered around the target position #[inline] fn windowed_sinc_interpolate(samples: &[f32], frac: f32) -> f32 { let mut result = 0.0; let kernel_size = samples.len(); let half_kernel = (kernel_size / 2) as f32; for i in 0..kernel_size { // Distance from interpolation point // samples[half_kernel] is at position 0, we want to interpolate at position frac let x = frac + half_kernel - (i as f32); let sinc_val = sinc(x); let window_val = blackman_window(x, half_kernel); result += samples[i] * sinc_val * window_val; } result } /// PCM sample format for memory-mapped audio files #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PcmSampleFormat { I16, I24, F32, } /// How audio data is stored for a pool entry #[derive(Debug, Clone)] pub enum AudioStorage { /// Fully decoded interleaved f32 samples in memory InMemory(Vec), /// Memory-mapped PCM file (WAV/AIFF) — instant load, OS-managed paging Mapped { mmap: Arc, data_offset: usize, sample_format: PcmSampleFormat, bytes_per_sample: usize, total_frames: u64, }, /// Compressed audio — playback handled by disk reader's stream decoder. /// `decoded_for_waveform` is progressively filled by a background thread. Compressed { decoded_for_waveform: Vec, decoded_frames: u64, total_frames: u64, }, } /// Audio file stored in the pool #[derive(Debug, Clone)] pub struct AudioFile { pub path: PathBuf, pub storage: AudioStorage, pub channels: u32, pub sample_rate: u32, pub frames: u64, /// Original file format (mp3, ogg, wav, flac, etc.) /// Used to determine if we should preserve lossy encoding during save pub original_format: Option, /// Original compressed file bytes (preserved across save/load to avoid re-encoding) pub original_bytes: Option>, } impl AudioFile { /// Create a new AudioFile with in-memory interleaved f32 data pub fn new(path: PathBuf, data: Vec, channels: u32, sample_rate: u32) -> Self { let frames = (data.len() / channels as usize) as u64; Self { path, storage: AudioStorage::InMemory(data), channels, sample_rate, frames, original_format: None, original_bytes: None, } } /// Create a new AudioFile with original format information pub fn with_format(path: PathBuf, data: Vec, channels: u32, sample_rate: u32, original_format: Option) -> Self { let frames = (data.len() / channels as usize) as u64; Self { path, storage: AudioStorage::InMemory(data), channels, sample_rate, frames, original_format, original_bytes: None, } } /// Create an AudioFile backed by a memory-mapped WAV/AIFF file pub fn from_mmap( path: PathBuf, mmap: memmap2::Mmap, data_offset: usize, sample_format: PcmSampleFormat, channels: u32, sample_rate: u32, total_frames: u64, ) -> Self { let bytes_per_sample = match sample_format { PcmSampleFormat::I16 => 2, PcmSampleFormat::I24 => 3, PcmSampleFormat::F32 => 4, }; Self { path, storage: AudioStorage::Mapped { mmap: Arc::new(mmap), data_offset, sample_format, bytes_per_sample, total_frames, }, channels, sample_rate, frames: total_frames, original_format: Some("wav".to_string()), original_bytes: None, } } /// Create a placeholder AudioFile for a compressed format (playback via disk reader) pub fn from_compressed( path: PathBuf, channels: u32, sample_rate: u32, total_frames: u64, original_format: Option, ) -> Self { Self { path, storage: AudioStorage::Compressed { decoded_for_waveform: Vec::new(), decoded_frames: 0, total_frames, }, channels, sample_rate, frames: total_frames, original_format, original_bytes: None, } } /// Get interleaved f32 sample data. /// /// - **InMemory**: returns the full slice directly. /// - **Mapped F32**: reinterprets the mmap'd bytes as `&[f32]` (zero-copy). /// - **Mapped I16/I24 or Compressed**: returns an empty slice (use /// `read_samples()` or the disk reader's `ReadAheadBuffer` instead). pub fn data(&self) -> &[f32] { match &self.storage { AudioStorage::InMemory(data) => data, AudioStorage::Mapped { mmap, data_offset, sample_format, total_frames, .. } if *sample_format == PcmSampleFormat::F32 => { let byte_slice = &mmap[*data_offset..]; let ptr = byte_slice.as_ptr(); // Check 4-byte alignment (required for f32) if ptr.align_offset(std::mem::align_of::()) == 0 { let len = (*total_frames as usize) * self.channels as usize; let available = byte_slice.len() / 4; let safe_len = len.min(available); // SAFETY: pointer is aligned, mmap is read-only and outlives // this borrow, and we clamp to the available byte range. unsafe { std::slice::from_raw_parts(ptr as *const f32, safe_len) } } else { &[] } } _ => &[], } } /// Read samples for a specific channel into the output buffer. /// Works for InMemory and Mapped storage. Returns the number of frames read. pub fn read_samples( &self, start_frame: usize, count: usize, channel: usize, out: &mut [f32], ) -> usize { let channels = self.channels as usize; let total_frames = self.frames as usize; match &self.storage { AudioStorage::InMemory(data) => { let mut written = 0; for i in 0..count.min(out.len()) { let frame = start_frame + i; if frame >= total_frames { break; } let idx = frame * channels + channel; out[i] = data[idx]; written += 1; } written } AudioStorage::Mapped { mmap, data_offset, sample_format, bytes_per_sample, .. } => { let mut written = 0; for i in 0..count.min(out.len()) { let frame = start_frame + i; if frame >= total_frames { break; } let sample_index = frame * channels + channel; let byte_offset = data_offset + sample_index * bytes_per_sample; let end = byte_offset + bytes_per_sample; if end > mmap.len() { break; } let bytes = &mmap[byte_offset..end]; out[i] = match sample_format { PcmSampleFormat::I16 => { let val = i16::from_le_bytes([bytes[0], bytes[1]]); val as f32 / 32768.0 } PcmSampleFormat::I24 => { // Sign-extend 24-bit to 32-bit let val = ((bytes[0] as i32) | ((bytes[1] as i32) << 8) | ((bytes[2] as i32) << 16)) << 8 >> 8; val as f32 / 8388608.0 } PcmSampleFormat::F32 => { f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) } }; written += 1; } written } AudioStorage::Compressed { .. } => { // Compressed files are read through the disk reader 0 } } } /// Get duration in seconds pub fn duration_seconds(&self) -> f64 { self.frames as f64 / self.sample_rate as f64 } /// Generate a waveform overview with the specified number of peaks /// This creates a downsampled representation suitable for timeline visualization pub fn generate_waveform_overview(&self, target_peaks: usize) -> Vec { self.generate_waveform_overview_range(0, self.frames as usize, target_peaks) } /// Generate a waveform overview for a specific range of frames /// /// # Arguments /// * `start_frame` - Starting frame index (0-based) /// * `end_frame` - Ending frame index (exclusive) /// * `target_peaks` - Desired number of peaks to generate pub fn generate_waveform_overview_range( &self, start_frame: usize, end_frame: usize, target_peaks: usize, ) -> Vec { if self.frames == 0 || target_peaks == 0 { return Vec::new(); } let total_frames = self.frames as usize; let start_frame = start_frame.min(total_frames); let end_frame = end_frame.min(total_frames); if start_frame >= end_frame { return Vec::new(); } let range_frames = end_frame - start_frame; let frames_per_peak = (range_frames / target_peaks).max(1); let actual_peaks = (range_frames + frames_per_peak - 1) / frames_per_peak; let mut peaks = Vec::with_capacity(actual_peaks); for peak_idx in 0..actual_peaks { let peak_start = start_frame + peak_idx * frames_per_peak; let peak_end = (start_frame + (peak_idx + 1) * frames_per_peak).min(end_frame); let mut min = f32::MAX; let mut max = f32::MIN; // Scan all samples in this window let data = self.data(); for frame_idx in peak_start..peak_end { // For multi-channel audio, combine all channels for ch in 0..self.channels as usize { let sample_idx = frame_idx * self.channels as usize + ch; if sample_idx < data.len() { let sample = data[sample_idx]; min = min.min(sample); max = max.max(sample); } } } // If no samples were found, clamp to safe defaults if min == f32::MAX { min = 0.0; } if max == f32::MIN { max = 0.0; } peaks.push(crate::io::WaveformPeak { min, max }); } peaks } } /// Pool of shared audio files (audio clip content) pub struct AudioClipPool { files: Vec, /// Waveform chunk cache for multi-resolution waveform generation waveform_cache: crate::audio::waveform_cache::WaveformCache, } /// Type alias for backwards compatibility pub type AudioPool = AudioClipPool; impl AudioClipPool { /// Create a new empty audio clip pool pub fn new() -> Self { Self { files: Vec::new(), waveform_cache: crate::audio::waveform_cache::WaveformCache::new(100), // 100MB cache } } /// Get the number of files in the pool pub fn len(&self) -> usize { self.files.len() } /// Check if the pool is empty pub fn is_empty(&self) -> bool { self.files.is_empty() } /// Get file info for waveform generation (duration, sample_rate, channels) pub fn get_file_info(&self, pool_index: usize) -> Option<(f64, u32, u32)> { self.files.get(pool_index).map(|file| { (file.duration_seconds(), file.sample_rate, file.channels) }) } /// Generate waveform overview for a file in the pool pub fn generate_waveform(&self, pool_index: usize, target_peaks: usize) -> Option> { self.files.get(pool_index).map(|file| { file.generate_waveform_overview(target_peaks) }) } /// Generate waveform overview for a specific range of a file in the pool /// /// # Arguments /// * `pool_index` - Index of the file in the pool /// * `start_frame` - Starting frame index (0-based) /// * `end_frame` - Ending frame index (exclusive) /// * `target_peaks` - Desired number of peaks to generate pub fn generate_waveform_range( &self, pool_index: usize, start_frame: usize, end_frame: usize, target_peaks: usize, ) -> Option> { self.files.get(pool_index).map(|file| { file.generate_waveform_overview_range(start_frame, end_frame, target_peaks) }) } /// Add an audio file to the pool and return its index pub fn add_file(&mut self, file: AudioFile) -> usize { let index = self.files.len(); self.files.push(file); index } /// Get an audio file by index pub fn get_file(&self, index: usize) -> Option<&AudioFile> { self.files.get(index) } /// Get a mutable reference to an audio file by index pub fn get_file_mut(&mut self, index: usize) -> Option<&mut AudioFile> { self.files.get_mut(index) } /// Get number of files in the pool pub fn file_count(&self) -> usize { self.files.len() } /// Render audio from a file in the pool with high-quality windowed sinc interpolation /// start_time_seconds: position in the audio file to start reading from (in seconds) /// clip_read_ahead: per-clip-instance read-ahead buffer for compressed audio streaming /// Returns the number of samples actually rendered pub fn render_from_file( &self, pool_index: usize, output: &mut [f32], start_time_seconds: f64, gain: f32, engine_sample_rate: u32, engine_channels: u32, clip_read_ahead: Option<&super::disk_reader::ReadAheadBuffer>, ) -> usize { let Some(audio_file) = self.files.get(pool_index) else { return 0; }; let audio_data = audio_file.data(); let read_ahead = clip_read_ahead; let use_read_ahead = audio_data.is_empty(); let src_channels = audio_file.channels as usize; // Nothing to render: no data and no read-ahead buffer if use_read_ahead && read_ahead.is_none() { // Log once per pool_index to diagnose silent clips static LOGGED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(u64::MAX); let prev = LOGGED.swap(pool_index as u64, std::sync::atomic::Ordering::Relaxed); if prev != pool_index as u64 { eprintln!("[RENDER] pool={}: data empty, no read_ahead! storage={:?}, frames={}", pool_index, std::mem::discriminant(&audio_file.storage), audio_file.frames); } return 0; } // In export mode, block-wait until the disk reader has filled the // frames we need, so offline rendering never gets buffer misses. if use_read_ahead { let ra = read_ahead.unwrap(); if ra.is_export_mode() { let src_start = (start_time_seconds * audio_file.sample_rate as f64) as u64; // Tell the disk reader where we need data BEFORE waiting ra.set_target_frame(src_start); // Pad by 64 frames for sinc interpolation taps let frames_needed = (output.len() / engine_channels as usize) as u64 + 64; // Spin-wait with small sleeps until the disk reader fills the buffer let mut wait_iters = 0u64; while !ra.has_range(src_start, frames_needed) { std::thread::sleep(std::time::Duration::from_micros(100)); wait_iters += 1; if wait_iters > 100_000 { // Safety valve: 10 seconds of waiting eprintln!("[EXPORT] Timed out waiting for disk reader (need frames {}..{})", src_start, src_start + frames_needed); break; } } } } // Snapshot the read-ahead buffer range once for the entire render call. // This ensures all sinc interpolation taps within a single callback // see a consistent range, preventing crackle from concurrent updates. let (ra_start, ra_end) = if use_read_ahead { read_ahead.unwrap().snapshot() } else { (0, 0) }; // Buffer-miss counter: how many times we wanted a sample the ring // buffer didn't have (frame in file range but outside buffer range). let mut buffer_misses: u32 = 0; // Read a single interleaved sample by (frame, channel). // Uses direct slice access for InMemory/Mapped, or the disk reader's // ReadAheadBuffer for compressed files. macro_rules! get_sample { ($frame:expr, $ch:expr) => {{ if use_read_ahead { let f = $frame as u64; let s = read_ahead.unwrap().read_sample(f, $ch, ra_start, ra_end); if s == 0.0 && (f < ra_start || f >= ra_end) { buffer_misses += 1; } s } else { let idx = ($frame) * src_channels + ($ch); if idx < audio_data.len() { audio_data[idx] } else { 0.0 } } }}; } let dst_channels = engine_channels as usize; let output_frames = output.len() / dst_channels; let src_start_position = start_time_seconds * audio_file.sample_rate as f64; // Tell the disk reader where we're reading so it buffers the right region. if use_read_ahead { read_ahead.unwrap().set_target_frame(src_start_position as u64); } let mut rendered_frames = 0; if audio_file.sample_rate == engine_sample_rate { // Fast path: matching sample rates — direct sample copy, no interpolation let src_start_frame = src_start_position.floor() as i64; // Continuity check: detect gaps/overlaps between consecutive callbacks (DAW_AUDIO_DEBUG=1) if std::env::var("DAW_AUDIO_DEBUG").is_ok() { use std::sync::atomic::{AtomicI64, Ordering as AO}; static EXPECTED_NEXT: AtomicI64 = AtomicI64::new(-1); static DISCONTINUITIES: AtomicI64 = AtomicI64::new(0); let expected = EXPECTED_NEXT.load(AO::Relaxed); if expected >= 0 && src_start_frame != expected { let count = DISCONTINUITIES.fetch_add(1, AO::Relaxed) + 1; eprintln!("[RENDER CONTINUITY] DISCONTINUITY #{}: expected frame {}, got {} (delta={})", count, expected, src_start_frame, src_start_frame - expected); } EXPECTED_NEXT.store(src_start_frame + output_frames as i64, AO::Relaxed); } for output_frame in 0..output_frames { let src_frame = src_start_frame + output_frame as i64; if src_frame < 0 || src_frame as u64 >= audio_file.frames { break; } let sf = src_frame as usize; for dst_ch in 0..dst_channels { let sample = if src_channels == dst_channels { get_sample!(sf, dst_ch) } else if src_channels == 1 { get_sample!(sf, 0) } else if dst_channels == 1 { let mut sum = 0.0f32; for src_ch in 0..src_channels { sum += get_sample!(sf, src_ch); } sum / src_channels as f32 } else { get_sample!(sf, dst_ch % src_channels) }; output[output_frame * dst_channels + dst_ch] += sample * gain; } rendered_frames += 1; } } else { // Sample rate conversion with windowed sinc interpolation let rate_ratio = audio_file.sample_rate as f64 / engine_sample_rate as f64; const KERNEL_SIZE: usize = 32; const HALF_KERNEL: usize = KERNEL_SIZE / 2; for output_frame in 0..output_frames { let src_position = src_start_position + (output_frame as f64 * rate_ratio); let src_frame = src_position.floor() as i32; let frac = (src_position - src_frame as f64) as f32; if src_frame < 0 || src_frame as usize >= audio_file.frames as usize { break; } for dst_ch in 0..dst_channels { let src_ch = if src_channels == dst_channels { dst_ch } else if src_channels == 1 { 0 } else if dst_channels == 1 { usize::MAX // sentinel: average all channels below } else { dst_ch % src_channels }; let sample = if src_ch == usize::MAX { let mut sum = 0.0; for ch in 0..src_channels { let mut channel_samples = [0.0f32; KERNEL_SIZE]; for (j, i) in (-(HALF_KERNEL as i32)..(HALF_KERNEL as i32)).enumerate() { let idx = src_frame + i; if idx >= 0 && (idx as usize) < audio_file.frames as usize { channel_samples[j] = get_sample!(idx as usize, ch); } } sum += windowed_sinc_interpolate(&channel_samples, frac); } sum / src_channels as f32 } else { let mut channel_samples = [0.0f32; KERNEL_SIZE]; for (j, i) in (-(HALF_KERNEL as i32)..(HALF_KERNEL as i32)).enumerate() { let idx = src_frame + i; if idx >= 0 && (idx as usize) < audio_file.frames as usize { channel_samples[j] = get_sample!(idx as usize, src_ch); } } windowed_sinc_interpolate(&channel_samples, frac) }; output[output_frame * dst_channels + dst_ch] += sample * gain; } rendered_frames += 1; } } if use_read_ahead && buffer_misses > 0 { static MISS_COUNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); let total = MISS_COUNT.fetch_add(buffer_misses as u64, std::sync::atomic::Ordering::Relaxed) + buffer_misses as u64; // Log every 100 misses to avoid flooding if total % 100 < buffer_misses as u64 { eprintln!("[RENDER] buffer misses this call: {}, total: {}, snap=[{}..{}], src_start_frame={}", buffer_misses, total, ra_start, ra_end, (start_time_seconds * audio_file.sample_rate as f64) as u64); } } rendered_frames * dst_channels } /// Generate waveform chunks for a file in the pool /// /// This generates chunks at a specific detail level and caches them. /// Returns the generated chunks. pub fn generate_waveform_chunks( &mut self, pool_index: usize, detail_level: u8, chunk_indices: &[u32], ) -> Vec { let file = match self.files.get(pool_index) { Some(f) => f, None => return Vec::new(), }; let chunks = crate::audio::waveform_cache::WaveformCache::generate_chunks( file, pool_index, detail_level, chunk_indices, ); // Store chunks in cache for chunk in &chunks { let key = crate::io::WaveformChunkKey { pool_index, detail_level: chunk.detail_level, chunk_index: chunk.chunk_index, }; self.waveform_cache.store_chunk(key, chunk.peaks.clone()); } chunks } /// Generate Level 0 (overview) chunks for a file /// /// This should be called immediately when a file is imported. /// Returns the generated chunks. pub fn generate_overview_chunks( &mut self, pool_index: usize, ) -> Vec { let file = match self.files.get(pool_index) { Some(f) => f, None => return Vec::new(), }; self.waveform_cache.generate_overview_chunks(file, pool_index) } /// Get a cached waveform chunk pub fn get_waveform_chunk( &self, pool_index: usize, detail_level: u8, chunk_index: u32, ) -> Option<&Vec> { let key = crate::io::WaveformChunkKey { pool_index, detail_level, chunk_index, }; self.waveform_cache.get_chunk(&key) } /// Check if a waveform chunk is cached pub fn has_waveform_chunk( &self, pool_index: usize, detail_level: u8, chunk_index: u32, ) -> bool { let key = crate::io::WaveformChunkKey { pool_index, detail_level, chunk_index, }; self.waveform_cache.has_chunk(&key) } /// Get waveform cache memory usage in MB pub fn waveform_cache_memory_mb(&self) -> f64 { self.waveform_cache.memory_usage_mb() } /// Get number of cached waveform chunks pub fn waveform_chunk_count(&self) -> usize { self.waveform_cache.chunk_count() } } impl Default for AudioClipPool { fn default() -> Self { Self::new() } } /// Embedded audio data stored as base64 in the project file #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmbeddedAudioData { /// Base64-encoded audio data pub data_base64: String, /// Original file format (wav, mp3, etc.) pub format: String, } /// Serializable audio pool entry for project save/load #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AudioPoolEntry { /// Index in the audio pool pub pool_index: usize, /// Original filename pub name: String, /// Path relative to project file (None if embedded) pub relative_path: Option, /// Duration in seconds pub duration: f64, /// Sample rate pub sample_rate: u32, /// Number of channels pub channels: u32, /// Embedded audio data (for files < 10MB) pub embedded_data: Option, } impl AudioClipPool { /// Serialize the audio clip pool for project saving /// /// Files smaller than 10MB are embedded as base64. /// Larger files are stored as relative paths to the project file. pub fn serialize(&self, project_path: &Path) -> Result, String> { let project_dir = project_path.parent() .ok_or_else(|| "Project path has no parent directory".to_string())?; let mut entries = Vec::new(); for (index, file) in self.files.iter().enumerate() { let file_path = &file.path; let file_path_str = file_path.to_string_lossy(); // Check if this is a temp file (from recording) or previously embedded audio // Always embed these let is_temp_file = file_path.starts_with(std::env::temp_dir()); let is_embedded = file_path_str.starts_with(" bool { const TEN_MB: u64 = 10_000_000; std::fs::metadata(file_path) .map(|m| m.len() < TEN_MB) .unwrap_or(false) } /// Embed audio from memory (already loaded in the pool) fn embed_from_memory(audio_file: &AudioFile) -> EmbeddedAudioData { use base64::{Engine as _, engine::general_purpose}; // Check if this is a lossy format that should be preserved let is_lossy = audio_file.original_format.as_ref().map_or(false, |fmt| { let fmt_lower = fmt.to_lowercase(); fmt_lower == "mp3" || fmt_lower == "ogg" || fmt_lower == "aac" || fmt_lower == "m4a" || fmt_lower == "opus" }); // Check for preserved original bytes first (from previous load cycle) if let Some(ref original_bytes) = audio_file.original_bytes { let data_base64 = general_purpose::STANDARD.encode(original_bytes); return EmbeddedAudioData { data_base64, format: audio_file.original_format.clone().unwrap_or_else(|| "wav".to_string()), }; } if is_lossy { // For lossy formats, read the original file bytes (if it still exists) if let Ok(original_bytes) = std::fs::read(&audio_file.path) { let data_base64 = general_purpose::STANDARD.encode(&original_bytes); return EmbeddedAudioData { data_base64, format: audio_file.original_format.clone().unwrap_or_else(|| "mp3".to_string()), }; } // If we can't read the original file, fall through to WAV conversion } // For lossless/PCM or if we couldn't read the original lossy file, // convert the f32 interleaved samples to WAV format bytes let wav_data = Self::encode_wav( audio_file.data(), audio_file.channels, audio_file.sample_rate ); let data_base64 = general_purpose::STANDARD.encode(&wav_data); EmbeddedAudioData { data_base64, format: "wav".to_string(), } } /// Encode f32 interleaved samples as WAV file bytes fn encode_wav(samples: &[f32], channels: u32, sample_rate: u32) -> Vec { let num_samples = samples.len(); let bytes_per_sample = 4; // 32-bit float let data_size = num_samples * bytes_per_sample; let file_size = 36 + data_size; let mut wav_data = Vec::with_capacity(44 + data_size); // RIFF header wav_data.extend_from_slice(b"RIFF"); wav_data.extend_from_slice(&(file_size as u32).to_le_bytes()); wav_data.extend_from_slice(b"WAVE"); // fmt chunk wav_data.extend_from_slice(b"fmt "); wav_data.extend_from_slice(&16u32.to_le_bytes()); // chunk size wav_data.extend_from_slice(&3u16.to_le_bytes()); // format code (3 = IEEE float) wav_data.extend_from_slice(&(channels as u16).to_le_bytes()); wav_data.extend_from_slice(&sample_rate.to_le_bytes()); wav_data.extend_from_slice(&(sample_rate * channels * bytes_per_sample as u32).to_le_bytes()); // byte rate wav_data.extend_from_slice(&((channels * bytes_per_sample as u32) as u16).to_le_bytes()); // block align wav_data.extend_from_slice(&32u16.to_le_bytes()); // bits per sample // data chunk wav_data.extend_from_slice(b"data"); wav_data.extend_from_slice(&(data_size as u32).to_le_bytes()); // Write samples as little-endian f32 for &sample in samples { wav_data.extend_from_slice(&sample.to_le_bytes()); } wav_data } /// Load audio pool from serialized entries /// /// Returns a list of pool indices that failed to load (missing files). /// The caller should present these to the user for resolution. pub fn load_from_serialized( &mut self, entries: Vec, project_path: &Path, ) -> Result, String> { let fn_start = std::time::Instant::now(); eprintln!("📊 [LOAD_SERIALIZED] Starting load_from_serialized with {} entries...", entries.len()); let project_dir = project_path.parent() .ok_or_else(|| "Project path has no parent directory".to_string())?; let mut missing_indices = Vec::new(); // Clear existing pool let clear_start = std::time::Instant::now(); self.files.clear(); eprintln!("📊 [LOAD_SERIALIZED] Clear pool took {:.2}ms", clear_start.elapsed().as_secs_f64() * 1000.0); // Find the maximum pool index to determine required size let max_index = entries.iter() .map(|e| e.pool_index) .max() .unwrap_or(0); // Ensure we have space for all entries let resize_start = std::time::Instant::now(); self.files.resize(max_index + 1, AudioFile::new(PathBuf::new(), Vec::new(), 2, 44100)); eprintln!("📊 [LOAD_SERIALIZED] Resize pool to {} took {:.2}ms", max_index + 1, resize_start.elapsed().as_secs_f64() * 1000.0); for (i, entry) in entries.iter().enumerate() { let entry_start = std::time::Instant::now(); eprintln!("📊 [LOAD_SERIALIZED] Processing entry {}/{}: '{}'", i + 1, entries.len(), entry.name); let success = if let Some(ref embedded) = entry.embedded_data { // Load from embedded data eprintln!("📊 [LOAD_SERIALIZED] Entry has embedded data (format: {})", embedded.format); match Self::load_from_embedded_into_pool(self, entry.pool_index, embedded.clone(), &entry.name) { Ok(_) => { eprintln!("[AudioPool] Successfully loaded embedded audio: {}", entry.name); true } Err(e) => { eprintln!("[AudioPool] Failed to load embedded audio {}: {}", entry.name, e); false } } } else if let Some(ref rel_path) = entry.relative_path { // Load from file path eprintln!("📊 [LOAD_SERIALIZED] Entry has file path: {:?}", rel_path); let full_path = project_dir.join(&rel_path); if full_path.exists() { Self::load_file_into_pool(self, entry.pool_index, &full_path).is_ok() } else { eprintln!("[AudioPool] File not found: {:?}", full_path); false } } else { eprintln!("[AudioPool] Entry has neither embedded data nor path: {}", entry.name); false }; if !success { missing_indices.push(entry.pool_index); } eprintln!("📊 [LOAD_SERIALIZED] Entry {} took {:.2}ms (success: {})", i + 1, entry_start.elapsed().as_secs_f64() * 1000.0, success); } eprintln!("📊 [LOAD_SERIALIZED] ✅ Total load_from_serialized time: {:.2}ms", fn_start.elapsed().as_secs_f64() * 1000.0); Ok(missing_indices) } /// Load audio from embedded base64 data fn load_from_embedded_into_pool( &mut self, pool_index: usize, embedded: EmbeddedAudioData, name: &str, ) -> Result<(), String> { use base64::{Engine as _, engine::general_purpose}; let fn_start = std::time::Instant::now(); eprintln!("📊 [POOL] Loading embedded audio '{}'...", name); // Decode base64 let step1_start = std::time::Instant::now(); let data = general_purpose::STANDARD .decode(&embedded.data_base64) .map_err(|e| format!("Failed to decode base64: {}", e))?; eprintln!("📊 [POOL] Step 1: Decode base64 ({} bytes) took {:.2}ms", data.len(), step1_start.elapsed().as_secs_f64() * 1000.0); // Write to temporary file for symphonia to decode let step2_start = std::time::Instant::now(); let temp_dir = std::env::temp_dir(); let temp_path = temp_dir.join(format!("lightningbeam_embedded_{}.{}", pool_index, embedded.format)); std::fs::write(&temp_path, &data) .map_err(|e| format!("Failed to write temporary file: {}", e))?; eprintln!("📊 [POOL] Step 2: Write temp file took {:.2}ms", step2_start.elapsed().as_secs_f64() * 1000.0); // Load the temporary file using existing infrastructure let step3_start = std::time::Instant::now(); let result = Self::load_file_into_pool(self, pool_index, &temp_path); eprintln!("📊 [POOL] Step 3: Decode audio with Symphonia took {:.2}ms", step3_start.elapsed().as_secs_f64() * 1000.0); // Clean up temporary file let _ = std::fs::remove_file(&temp_path); // Update the path to reflect it was embedded, and preserve original bytes if result.is_ok() && pool_index < self.files.len() { self.files[pool_index].path = PathBuf::from(format!("", name)); // Preserve the original compressed/encoded bytes so re-save doesn't need to re-encode self.files[pool_index].original_bytes = Some(data); self.files[pool_index].original_format = Some(embedded.format.clone()); } eprintln!("📊 [POOL] ✅ Total load_from_embedded time: {:.2}ms", fn_start.elapsed().as_secs_f64() * 1000.0); result } /// Load an audio file into a specific pool index fn load_file_into_pool(&mut self, pool_index: usize, file_path: &Path) -> Result<(), String> { use symphonia::core::audio::SampleBuffer; use symphonia::core::codecs::{DecoderOptions, CODEC_TYPE_NULL}; use symphonia::core::formats::FormatOptions; use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; let file = std::fs::File::open(file_path) .map_err(|e| format!("Failed to open audio file: {}", e))?; let mss = MediaSourceStream::new(Box::new(file), Default::default()); let mut hint = Hint::new(); if let Some(ext) = file_path.extension() { hint.with_extension(&ext.to_string_lossy()); } let format_opts = FormatOptions::default(); let metadata_opts = MetadataOptions::default(); let decoder_opts = DecoderOptions::default(); let probed = symphonia::default::get_probe() .format(&hint, mss, &format_opts, &metadata_opts) .map_err(|e| format!("Failed to probe audio file: {}", e))?; let mut format = probed.format; let track = format .tracks() .iter() .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) .ok_or_else(|| "No audio track found".to_string())?; let mut decoder = symphonia::default::get_codecs() .make(&track.codec_params, &decoder_opts) .map_err(|e| format!("Failed to create decoder: {}", e))?; let track_id = track.id; let sample_rate = track.codec_params.sample_rate.unwrap_or(44100); let channels = track.codec_params.channels.map(|c| c.count()).unwrap_or(2) as u32; let mut samples = Vec::new(); let mut sample_buf = None; loop { let packet = match format.next_packet() { Ok(packet) => packet, Err(_) => break, }; if packet.track_id() != track_id { continue; } match decoder.decode(&packet) { Ok(decoded) => { if sample_buf.is_none() { let spec = *decoded.spec(); let duration = decoded.capacity() as u64; sample_buf = Some(SampleBuffer::::new(duration, spec)); } if let Some(ref mut buf) = sample_buf { buf.copy_interleaved_ref(decoded); samples.extend_from_slice(buf.samples()); } } Err(_) => continue, } } // Detect original format from file extension let original_format = file_path.extension() .and_then(|ext| ext.to_str()) .map(|s| s.to_lowercase()); let audio_file = AudioFile::with_format( file_path.to_path_buf(), samples, channels, sample_rate, original_format, ); if pool_index >= self.files.len() { return Err(format!("Pool index {} out of bounds", pool_index)); } self.files[pool_index] = audio_file; Ok(()) } /// Resolve a missing audio file by loading from a new path /// This is called from the UI when the user manually locates a missing file pub fn resolve_missing_file(&mut self, pool_index: usize, new_path: &Path) -> Result<(), String> { Self::load_file_into_pool(self, pool_index, new_path) } }