247 lines
6.7 KiB
Rust
247 lines
6.7 KiB
Rust
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<Mutex<VecDeque<f32>>>,
|
|
bars: Arc<Mutex<[f32; NUM_BARS]>>,
|
|
}
|
|
|
|
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<f32> = 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<Complex<f32>> = 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::<f32>::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<S> {
|
|
inner: S,
|
|
bus: SpectrumBus,
|
|
}
|
|
|
|
impl<S> Iterator for TapSource<S>
|
|
where
|
|
S: Source<Item = f32>,
|
|
{
|
|
type Item = f32;
|
|
fn next(&mut self) -> Option<f32> {
|
|
let s = self.inner.next()?;
|
|
self.bus.push_sample(s);
|
|
Some(s)
|
|
}
|
|
}
|
|
|
|
impl<S> Source for TapSource<S>
|
|
where
|
|
S: Source<Item = f32>,
|
|
{
|
|
fn current_span_len(&self) -> Option<usize> {
|
|
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<Duration> {
|
|
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<Duration>,
|
|
pub spectrum: SpectrumBus,
|
|
}
|
|
|
|
impl AudioPlayer {
|
|
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
|
|
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<Duration>,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<Duration, Box<dyn std::error::Error>> {
|
|
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)
|
|
}
|
|
} |