Files
rumus/src/audio.rs
2026-10-15 15:54:31 +02:00

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)
}
}