use std::collections::VecDeque; use std::fs::File; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::time::Duration; use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source}; const FFT_SIZE: usize = 1024; const NUM_BARS: usize = 64; #[derive(Clone)] pub struct SpectrumBus { buffer: Arc>>, bars: Arc>, } impl SpectrumBus { pub fn new() -> Self { Self { buffer: Arc::new(Mutex::new(VecDeque::with_capacity(FFT_SIZE * 2))), bars: Arc::new(Mutex::new([0.0; NUM_BARS])), } } fn push_sample(&self, s: f32) { if let Ok(mut buf) = self.buffer.lock() { buf.push_back(s); if buf.len() > FFT_SIZE * 2 { buf.pop_front(); } } } pub fn bars(&self) -> [f32; NUM_BARS] { self.bars.lock().map(|b| *b).unwrap_or([0.0; NUM_BARS]) } pub fn recompute(&self) { use rustfft::num_complex::Complex; use rustfft::FftPlanner; let samples: Vec = match self.buffer.lock() { Ok(buf) => buf.iter().rev().take(FFT_SIZE).rev().copied().collect(), Err(_) => return, }; if samples.len() < FFT_SIZE { return; } let mut data: Vec> = samples .iter() .enumerate() .map(|(i, &s)| { let w = 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / (FFT_SIZE - 1) as f32).cos()); Complex::new(s * w, 0.0) }) .collect(); let mut planner = FftPlanner::::new(); let fft = planner.plan_fft_forward(FFT_SIZE); fft.process(&mut data); let half = FFT_SIZE / 2; let mut bars = [0.0f32; NUM_BARS]; for i in 0..NUM_BARS { let lo = ((i as f32 / NUM_BARS as f32).powi(2) * half as f32) as usize; let hi = (((i + 1) as f32 / NUM_BARS as f32).powi(2) * half as f32) as usize; let hi = hi.max(lo + 1).min(half); let mut sum = 0.0; for k in lo..hi { sum += data[k].norm(); } let avg = sum / (hi - lo) as f32; let v = (avg / FFT_SIZE as f32).sqrt() * 4.0; bars[i] = v.clamp(0.0, 1.0); } if let Ok(mut current) = self.bars.lock() { for i in 0..NUM_BARS { if bars[i] > current[i] { current[i] = bars[i]; } else { current[i] = current[i] * 0.7 + bars[i] * 0.3; } } } } } struct TapSource { inner: S, bus: SpectrumBus, } impl Iterator for TapSource where S: Source, { type Item = f32; fn next(&mut self) -> Option { let s = self.inner.next()?; self.bus.push_sample(s); Some(s) } } impl Source for TapSource where S: Source, { fn current_span_len(&self) -> Option { self.inner.current_span_len() } fn channels(&self) -> u16 { self.inner.channels() } fn sample_rate(&self) -> u32 { self.inner.sample_rate() } fn total_duration(&self) -> Option { self.inner.total_duration() } fn try_seek(&mut self, pos: Duration) -> Result<(), rodio::source::SeekError> { self.inner.try_seek(pos) } } pub struct AudioPlayer { _stream: OutputStream, sink: Sink, paused_pos: Option, pub spectrum: SpectrumBus, } impl AudioPlayer { pub fn new() -> Result> { let stream_handle = OutputStreamBuilder::open_default_stream()?; let sink = Sink::connect_new(&stream_handle.mixer()); sink.set_volume(1.0); Ok(Self { _stream: stream_handle, sink, paused_pos: None, spectrum: SpectrumBus::new(), }) } /// Запускает трек, опционально перематывая в `start_pos` ДО начала воспроизведения. /// Перемотка делается на декодере — это предотвращает щелчок на старте. pub fn play( &mut self, path: &PathBuf, start_pos: Option, ) -> Result<(), Box> { self.sink.stop(); self.paused_pos = None; let file = File::open(path)?; let mut decoder = Decoder::try_from(file)?; if let Some(pos) = start_pos { if !pos.is_zero() { let _ = decoder.try_seek(pos); } } let tapped = TapSource { inner: decoder, bus: self.spectrum.clone(), }; self.sink.append(tapped); // ВАЖНО: явно снимаем паузу — sink.stop() не сбрасывает состояние pause. self.sink.play(); Ok(()) } pub fn position(&self) -> Duration { if self.sink.is_paused() { self.paused_pos.unwrap_or_else(|| self.sink.get_pos()) } else { self.sink.get_pos() } } pub fn is_paused(&self) -> bool { self.sink.is_paused() } pub fn toggle_pause(&mut self) { if self.sink.is_paused() { self.sink.play(); self.paused_pos = None; } else { self.paused_pos = Some(self.sink.get_pos()); self.sink.pause(); } } pub fn stop(&mut self) { self.sink.stop(); self.paused_pos = None; } pub fn is_finished(&self) -> bool { self.sink.empty() } pub fn volume(&self) -> f32 { self.sink.volume() } pub fn set_volume(&mut self, v: f32) { self.sink.set_volume(v.clamp(0.0, 2.0)); } pub fn change_volume(&mut self, delta: f32) { let v = (self.sink.volume() + delta).clamp(0.0, 2.0); self.sink.set_volume(v); } pub fn seek_to(&mut self, pos: Duration) -> Result<(), Box> { self.sink.try_seek(pos)?; if self.sink.is_paused() { self.paused_pos = Some(pos); } Ok(()) } pub fn seek_by( &mut self, delta: i64, total: Duration, ) -> Result> { let current = self.position(); let new_secs = (current.as_secs() as i64 + delta).max(0) as u64; let mut new_pos = Duration::from_secs(new_secs); if !total.is_zero() && new_pos >= total { new_pos = total.saturating_sub(Duration::from_millis(100)); } self.seek_to(new_pos)?; Ok(new_pos) } }