Lightningbeam/daw-backend/src/audio/waveform_cache.rs

291 lines
9.6 KiB
Rust

//! Waveform chunk cache for scalable multi-resolution waveform generation
//!
//! This module provides a chunk-based waveform caching system that generates
//! waveform data progressively at multiple detail levels, avoiding the limitations
//! of the old fixed 20,000-peak approach.
use crate::io::{WaveformChunk, WaveformChunkKey, WaveformPeak};
use crate::audio::pool::AudioFile;
use std::collections::HashMap;
/// Detail levels for multi-resolution waveform storage
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetailLevel {
Overview = 0, // 1 peak per second
Low = 1, // 10 peaks per second
Medium = 2, // 100 peaks per second
High = 3, // 1000 peaks per second
Max = 4, // Full resolution (sample-accurate)
}
impl DetailLevel {
/// Get peaks per second for this detail level
pub fn peaks_per_second(self) -> usize {
match self {
DetailLevel::Overview => 1,
DetailLevel::Low => 10,
DetailLevel::Medium => 100,
DetailLevel::High => 1000,
DetailLevel::Max => 48000, // Approximate max for sample-accurate
}
}
/// Create from u8 value
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(DetailLevel::Overview),
1 => Some(DetailLevel::Low),
2 => Some(DetailLevel::Medium),
3 => Some(DetailLevel::High),
4 => Some(DetailLevel::Max),
_ => None,
}
}
}
/// Priority for chunk generation
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChunkPriority {
Low = 0, // Background generation
Medium = 1, // Precache adjacent to viewport
High = 2, // Visible in current viewport
}
/// Chunk generation request
#[derive(Debug, Clone)]
pub struct ChunkGenerationRequest {
pub key: WaveformChunkKey,
pub priority: ChunkPriority,
}
/// Waveform chunk cache with multi-resolution support
pub struct WaveformCache {
/// Cached chunks indexed by key
chunks: HashMap<WaveformChunkKey, Vec<WaveformPeak>>,
/// Maximum memory usage in MB (for future LRU eviction)
_max_memory_mb: usize,
/// Current memory usage estimate in bytes
current_memory_bytes: usize,
}
impl WaveformCache {
/// Create a new waveform cache with the specified memory limit
pub fn new(max_memory_mb: usize) -> Self {
Self {
chunks: HashMap::new(),
_max_memory_mb: max_memory_mb,
current_memory_bytes: 0,
}
}
/// Get a chunk from the cache
pub fn get_chunk(&self, key: &WaveformChunkKey) -> Option<&Vec<WaveformPeak>> {
self.chunks.get(key)
}
/// Store a chunk in the cache
pub fn store_chunk(&mut self, key: WaveformChunkKey, peaks: Vec<WaveformPeak>) {
let chunk_size = peaks.len() * std::mem::size_of::<WaveformPeak>();
self.current_memory_bytes += chunk_size;
self.chunks.insert(key, peaks);
// TODO: Implement LRU eviction if memory exceeds limit
}
/// Check if a chunk exists in the cache
pub fn has_chunk(&self, key: &WaveformChunkKey) -> bool {
self.chunks.contains_key(key)
}
/// Clear all chunks for a specific pool index (when file is unloaded)
pub fn clear_pool(&mut self, pool_index: usize) {
self.chunks.retain(|key, peaks| {
if key.pool_index == pool_index {
let chunk_size = peaks.len() * std::mem::size_of::<WaveformPeak>();
self.current_memory_bytes = self.current_memory_bytes.saturating_sub(chunk_size);
false
} else {
true
}
});
}
/// Generate a single waveform chunk for an audio file
///
/// This generates peaks for a specific time range at a specific detail level.
/// The chunk covers a time range based on the detail level and chunk index.
pub fn generate_chunk(
audio_file: &AudioFile,
detail_level: u8,
chunk_index: u32,
) -> Option<WaveformChunk> {
let level = DetailLevel::from_u8(detail_level)?;
let peaks_per_second = level.peaks_per_second();
// Calculate time range for this chunk based on detail level
// Each chunk covers a varying amount of time depending on detail level
let chunk_duration_seconds = match level {
DetailLevel::Overview => 60.0, // 60 seconds per chunk (60 peaks)
DetailLevel::Low => 30.0, // 30 seconds per chunk (300 peaks)
DetailLevel::Medium => 10.0, // 10 seconds per chunk (1000 peaks)
DetailLevel::High => 5.0, // 5 seconds per chunk (5000 peaks)
DetailLevel::Max => 1.0, // 1 second per chunk (48000 peaks)
};
let start_time = chunk_index as f64 * chunk_duration_seconds;
let end_time = start_time + chunk_duration_seconds;
// Check if this chunk is within the audio file duration
let audio_duration = audio_file.duration_seconds();
if start_time >= audio_duration {
return None; // Chunk is completely beyond file end
}
// Clamp end_time to file duration
let end_time = end_time.min(audio_duration);
// Calculate frame range
let start_frame = (start_time * audio_file.sample_rate as f64) as usize;
let end_frame = (end_time * audio_file.sample_rate as f64) as usize;
// Calculate number of peaks for this time range
let duration = end_time - start_time;
let target_peaks = (duration * peaks_per_second as f64).ceil() as usize;
if target_peaks == 0 {
return None;
}
// Generate peaks using the existing method
let peaks = audio_file.generate_waveform_overview_range(
start_frame,
end_frame,
target_peaks,
);
Some(WaveformChunk {
audio_pool_index: 0, // Will be set by caller
detail_level,
chunk_index,
time_range: (start_time, end_time),
peaks,
})
}
/// Generate multiple chunks for an audio file
///
/// This is a convenience method for generating several chunks at once.
pub fn generate_chunks(
audio_file: &AudioFile,
pool_index: usize,
detail_level: u8,
chunk_indices: &[u32],
) -> Vec<WaveformChunk> {
chunk_indices
.iter()
.filter_map(|&chunk_index| {
let mut chunk = Self::generate_chunk(audio_file, detail_level, chunk_index)?;
chunk.audio_pool_index = pool_index;
Some(chunk)
})
.collect()
}
/// Calculate how many chunks are needed for a file at a given detail level
pub fn calculate_chunk_count(duration_seconds: f64, detail_level: u8) -> u32 {
let level = match DetailLevel::from_u8(detail_level) {
Some(l) => l,
None => return 0,
};
let chunk_duration_seconds = match level {
DetailLevel::Overview => 60.0,
DetailLevel::Low => 30.0,
DetailLevel::Medium => 10.0,
DetailLevel::High => 5.0,
DetailLevel::Max => 1.0,
};
((duration_seconds / chunk_duration_seconds).ceil() as u32).max(1)
}
/// Generate all Level 0 (overview) chunks for a file
///
/// This should be called immediately when a file is imported to provide
/// instant thumbnail display.
pub fn generate_overview_chunks(
&mut self,
audio_file: &AudioFile,
pool_index: usize,
) -> Vec<WaveformChunk> {
let duration = audio_file.duration_seconds();
let chunk_count = Self::calculate_chunk_count(duration, 0);
let chunk_indices: Vec<u32> = (0..chunk_count).collect();
let chunks = Self::generate_chunks(audio_file, pool_index, 0, &chunk_indices);
// Store chunks in cache
for chunk in &chunks {
let key = WaveformChunkKey {
pool_index,
detail_level: chunk.detail_level,
chunk_index: chunk.chunk_index,
};
self.store_chunk(key, chunk.peaks.clone());
}
chunks
}
/// Get current memory usage in bytes
pub fn memory_usage_bytes(&self) -> usize {
self.current_memory_bytes
}
/// Get current memory usage in megabytes
pub fn memory_usage_mb(&self) -> f64 {
self.current_memory_bytes as f64 / 1_000_000.0
}
/// Get number of cached chunks
pub fn chunk_count(&self) -> usize {
self.chunks.len()
}
}
impl Default for WaveformCache {
fn default() -> Self {
Self::new(100) // Default 100MB cache
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detail_level_peaks_per_second() {
assert_eq!(DetailLevel::Overview.peaks_per_second(), 1);
assert_eq!(DetailLevel::Low.peaks_per_second(), 10);
assert_eq!(DetailLevel::Medium.peaks_per_second(), 100);
assert_eq!(DetailLevel::High.peaks_per_second(), 1000);
}
#[test]
fn test_chunk_count_calculation() {
// 60 second file, Overview level (60s chunks) = 1 chunk
assert_eq!(WaveformCache::calculate_chunk_count(60.0, 0), 1);
// 120 second file, Overview level (60s chunks) = 2 chunks
assert_eq!(WaveformCache::calculate_chunk_count(120.0, 0), 2);
// 10 second file, Medium level (10s chunks) = 1 chunk
assert_eq!(WaveformCache::calculate_chunk_count(10.0, 2), 1);
// 25 second file, Medium level (10s chunks) = 3 chunks
assert_eq!(WaveformCache::calculate_chunk_count(25.0, 2), 3);
}
}