291 lines
9.6 KiB
Rust
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);
|
|
}
|
|
}
|