use std::sync::Arc; use std::sync::atomic::{AtomicI16, AtomicU32, AtomicU64, AtomicUsize, Ordering}; use std::time::Duration; use rodio::source::SeekError; use rodio::{ChannelCount, Sample, SampleRate, Source}; const LEVEL_SCALE: f32 = 1_000_000.0; const SCOPE_SAMPLES: usize = 256; const SCOPE_SAMPLE_SCALE: f32 = i16::MAX as f32; const TARGET_ANALYSIS_HZ: f32 = 30.0; const TARGET_SCOPE_HZ: f32 = 240.0; #[derive(Debug, Clone, Default)] pub struct AudioAnalysisSnapshot { pub sequence: u64, pub energy: f64, pub bass: f64, pub mid: f64, pub treble: f64, pub beat: f64, pub scope: Vec, } #[derive(Debug)] pub struct AnalyzerShared { sequence: AtomicU64, energy: AtomicU32, bass: AtomicU32, mid: AtomicU32, treble: AtomicU32, beat: AtomicU32, scope_write: AtomicUsize, scope: Box<[AtomicI16]>, } impl Default for AnalyzerShared { fn default() -> Self { Self { sequence: AtomicU64::new(0), energy: AtomicU32::new(0), bass: AtomicU32::new(0), mid: AtomicU32::new(0), treble: AtomicU32::new(0), beat: AtomicU32::new(0), scope_write: AtomicUsize::new(0), scope: (0..SCOPE_SAMPLES) .map(|_| AtomicI16::new(0)) .collect::>() .into_boxed_slice(), } } } impl AnalyzerShared { pub fn clear(&self) { self.energy.store(0, Ordering::Relaxed); self.bass.store(0, Ordering::Relaxed); self.mid.store(0, Ordering::Relaxed); self.treble.store(0, Ordering::Relaxed); self.beat.store(0, Ordering::Relaxed); self.scope_write.store(0, Ordering::Relaxed); for sample in self.scope.iter() { sample.store(0, Ordering::Relaxed); } self.sequence.store(0, Ordering::Relaxed); } pub fn snapshot(&self) -> AudioAnalysisSnapshot { let write = self.scope_write.load(Ordering::Relaxed); let len = self.scope.len().max(1); let scope = (0..self.scope.len()) .map(|offset| { let index = (write + offset) % len; f64::from(self.scope[index].load(Ordering::Relaxed)) / f64::from(i16::MAX) }) .collect(); AudioAnalysisSnapshot { sequence: self.sequence.load(Ordering::Relaxed), energy: load_norm(&self.energy), bass: load_norm(&self.bass), mid: load_norm(&self.mid), treble: load_norm(&self.treble), beat: load_norm(&self.beat), scope, } } fn store_levels(&self, energy: f32, bass: f32, mid: f32, treble: f32, beat: f32) { store_norm(&self.energy, energy); store_norm(&self.bass, bass); store_norm(&self.mid, mid); store_norm(&self.treble, treble); store_norm(&self.beat, beat); self.sequence.fetch_add(1, Ordering::Relaxed); } fn push_scope(&self, sample: f32) { let index = self.scope_write.fetch_add(1, Ordering::Relaxed) % self.scope.len(); let value = (sample.clamp(-1.0, 1.0) * SCOPE_SAMPLE_SCALE).round() as i16; self.scope[index].store(value, Ordering::Relaxed); } } pub struct AnalyzedSource { input: S, shared: Arc, state: AnalyzerState, } impl AnalyzedSource where S: Source, { pub fn new(input: S, shared: Arc) -> Self { let channels = input.channels(); let sample_rate = input.sample_rate(); Self { input, shared, state: AnalyzerState::new(channels, sample_rate), } } } impl Iterator for AnalyzedSource where S: Source, { type Item = Sample; fn next(&mut self) -> Option { let sample = self.input.next()?; self.state .ensure_format(self.input.channels(), self.input.sample_rate()); self.state.accept_sample(sample as f32, &self.shared); Some(sample) } fn size_hint(&self) -> (usize, Option) { self.input.size_hint() } } impl Source for AnalyzedSource where S: Source, { fn current_span_len(&self) -> Option { self.input.current_span_len() } fn channels(&self) -> ChannelCount { self.input.channels() } fn sample_rate(&self) -> SampleRate { self.input.sample_rate() } fn total_duration(&self) -> Option { self.input.total_duration() } fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { let result = self.input.try_seek(pos); if result.is_ok() { self.shared.clear(); self.state.reset_filters(); } result } } #[derive(Debug)] struct AnalyzerState { channels: usize, sample_rate: f32, bass_alpha: f32, mid_alpha: f32, channel_index: usize, frame_sum: f32, frame_count: usize, window_frames: usize, scope_counter: usize, scope_stride: usize, low_bass: f32, low_mid: f32, full_sq: f32, bass_sq: f32, mid_sq: f32, treble_sq: f32, slow_energy: f32, smooth_energy: f32, smooth_bass: f32, smooth_mid: f32, smooth_treble: f32, smooth_beat: f32, } impl AnalyzerState { fn new(channels: ChannelCount, sample_rate: SampleRate) -> Self { let mut state = Self { channels: channels.get() as usize, sample_rate: sample_rate.get() as f32, bass_alpha: 0.0, mid_alpha: 0.0, channel_index: 0, frame_sum: 0.0, frame_count: 0, window_frames: 0, scope_counter: 0, scope_stride: 0, low_bass: 0.0, low_mid: 0.0, full_sq: 0.0, bass_sq: 0.0, mid_sq: 0.0, treble_sq: 0.0, slow_energy: 0.0, smooth_energy: 0.0, smooth_bass: 0.0, smooth_mid: 0.0, smooth_treble: 0.0, smooth_beat: 0.0, }; state.configure(); state } fn ensure_format(&mut self, channels: ChannelCount, sample_rate: SampleRate) { let channels = channels.get() as usize; let sample_rate = sample_rate.get() as f32; if self.channels != channels || (self.sample_rate - sample_rate).abs() >= 1.0 { self.channels = channels; self.sample_rate = sample_rate; self.configure(); self.reset_filters(); } } fn configure(&mut self) { self.channels = self.channels.max(1); self.sample_rate = self.sample_rate.max(1.0); self.bass_alpha = lowpass_alpha(180.0, self.sample_rate); self.mid_alpha = lowpass_alpha(2_400.0, self.sample_rate); self.window_frames = (self.sample_rate / TARGET_ANALYSIS_HZ).round().max(256.0) as usize; self.scope_stride = (self.sample_rate / TARGET_SCOPE_HZ).round().max(1.0) as usize; } fn reset_filters(&mut self) { self.channel_index = 0; self.frame_sum = 0.0; self.frame_count = 0; self.scope_counter = 0; self.low_bass = 0.0; self.low_mid = 0.0; self.full_sq = 0.0; self.bass_sq = 0.0; self.mid_sq = 0.0; self.treble_sq = 0.0; self.slow_energy = 0.0; self.smooth_energy = 0.0; self.smooth_bass = 0.0; self.smooth_mid = 0.0; self.smooth_treble = 0.0; self.smooth_beat = 0.0; } fn accept_sample(&mut self, sample: f32, shared: &AnalyzerShared) { let sample = if sample.is_finite() { sample.clamp(-1.5, 1.5) } else { 0.0 }; self.frame_sum += sample; self.channel_index += 1; if self.channel_index < self.channels { return; } let mono = self.frame_sum / self.channels as f32; self.channel_index = 0; self.frame_sum = 0.0; self.accept_frame(mono, shared); } fn accept_frame(&mut self, sample: f32, shared: &AnalyzerShared) { self.low_bass += self.bass_alpha * (sample - self.low_bass); self.low_mid += self.mid_alpha * (sample - self.low_mid); let bass = self.low_bass; let mid = self.low_mid - self.low_bass; let treble = sample - self.low_mid; self.full_sq += sample * sample; self.bass_sq += bass * bass; self.mid_sq += mid * mid; self.treble_sq += treble * treble; self.frame_count += 1; self.scope_counter += 1; if self.scope_counter >= self.scope_stride { self.scope_counter = 0; shared.push_scope(sample); } if self.frame_count >= self.window_frames { self.publish_window(shared); } } fn publish_window(&mut self, shared: &AnalyzerShared) { let frames = self.frame_count.max(1) as f32; let energy = compress_rms((self.full_sq / frames).sqrt(), 7.5); let bass = compress_rms((self.bass_sq / frames).sqrt(), 11.0); let mid = compress_rms((self.mid_sq / frames).sqrt(), 15.0); let treble = compress_rms((self.treble_sq / frames).sqrt(), 22.0); if self.slow_energy == 0.0 { self.slow_energy = energy; } else { self.slow_energy = self.slow_energy * 0.94 + energy * 0.06; } let beat_raw = ((energy - self.slow_energy * 1.18) * 5.5).clamp(0.0, 1.0); self.smooth_energy = smooth_level(self.smooth_energy, energy, 0.35, 0.82); self.smooth_bass = smooth_level(self.smooth_bass, bass, 0.30, 0.80); self.smooth_mid = smooth_level(self.smooth_mid, mid, 0.35, 0.82); self.smooth_treble = smooth_level(self.smooth_treble, treble, 0.28, 0.76); self.smooth_beat = smooth_level(self.smooth_beat, beat_raw, 0.18, 0.70); shared.store_levels( self.smooth_energy, self.smooth_bass, self.smooth_mid, self.smooth_treble, self.smooth_beat, ); self.full_sq = 0.0; self.bass_sq = 0.0; self.mid_sq = 0.0; self.treble_sq = 0.0; self.frame_count = 0; } } fn lowpass_alpha(cutoff_hz: f32, sample_rate: f32) -> f32 { 1.0 - (-std::f32::consts::TAU * cutoff_hz / sample_rate.max(1.0)).exp() } fn compress_rms(rms: f32, scale: f32) -> f32 { (1.0 - (-rms.max(0.0) * scale).exp()).clamp(0.0, 1.0) } fn smooth_level(previous: f32, next: f32, attack: f32, release: f32) -> f32 { let keep = if next > previous { attack } else { release }; previous * keep + next * (1.0 - keep) } fn store_norm(target: &AtomicU32, value: f32) { target.store( (value.clamp(0.0, 1.0) * LEVEL_SCALE).round() as u32, Ordering::Relaxed, ); } fn load_norm(source: &AtomicU32) -> f64 { f64::from(source.load(Ordering::Relaxed)) / f64::from(LEVEL_SCALE) } #[cfg(test)] mod tests { use super::*; struct TestSource { samples: Vec, cursor: usize, channels: ChannelCount, sample_rate: SampleRate, } impl TestSource { fn sine(frames: usize, channels: u16, sample_rate: u32, frequency: f32) -> Self { let channels_count = channels.max(1); let mut samples = Vec::with_capacity(frames * usize::from(channels_count)); for frame in 0..frames { let time = frame as f32 / sample_rate as f32; let sample = (std::f32::consts::TAU * frequency * time).sin() * 0.5; for _ in 0..channels_count { samples.push(sample as Sample); } } Self { samples, cursor: 0, channels: ChannelCount::new(channels_count).unwrap(), sample_rate: SampleRate::new(sample_rate).unwrap(), } } } impl Iterator for TestSource { type Item = Sample; fn next(&mut self) -> Option { let sample = self.samples.get(self.cursor).copied()?; self.cursor += 1; Some(sample) } fn size_hint(&self) -> (usize, Option) { let remaining = self.samples.len().saturating_sub(self.cursor); (remaining, Some(remaining)) } } impl Source for TestSource { fn current_span_len(&self) -> Option { Some(self.samples.len().saturating_sub(self.cursor)) } fn channels(&self) -> ChannelCount { self.channels } fn sample_rate(&self) -> SampleRate { self.sample_rate } fn total_duration(&self) -> Option { let frames = self.samples.len() / usize::from(self.channels.get()); Some(Duration::from_secs_f64( frames as f64 / f64::from(self.sample_rate.get()), )) } } #[test] fn analyzed_source_publishes_levels_and_scope() { let shared = Arc::new(AnalyzerShared::default()); let source = TestSource::sine(4_096, 2, 48_000, 110.0); let analyzed = AnalyzedSource::new(source, Arc::clone(&shared)); for _ in analyzed {} let snapshot = shared.snapshot(); assert!(snapshot.sequence > 0); assert!(snapshot.energy > 0.0); assert!(snapshot.bass > 0.0); assert!(snapshot.scope.iter().any(|sample| sample.abs() > 0.001)); } }