обновление
This commit is contained in:
163
src/audio.rs
163
src/audio.rs
@@ -1,14 +1,140 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink};
|
||||
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()
|
||||
}
|
||||
// ВАЖНО: пробрасываем try_seek, иначе Sink::try_seek вернёт NotSupported
|
||||
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 {
|
||||
@@ -20,15 +146,21 @@ impl AudioPlayer {
|
||||
_stream: stream_handle,
|
||||
sink,
|
||||
paused_pos: None,
|
||||
spectrum: SpectrumBus::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn play(&mut self, path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.sink.stop();
|
||||
self.paused_pos = None;
|
||||
let file = BufReader::new(File::open(path)?);
|
||||
let source = Decoder::new(file)?;
|
||||
self.sink.append(source);
|
||||
let file = File::open(path)?;
|
||||
// Decoder::try_from автоматически настраивает seekable и byte_len[citation:1][citation:19]
|
||||
let decoder = Decoder::try_from(file)?;
|
||||
let tapped = TapSource {
|
||||
inner: decoder,
|
||||
bus: self.spectrum.clone(),
|
||||
};
|
||||
self.sink.append(tapped);
|
||||
self.sink.play();
|
||||
Ok(())
|
||||
}
|
||||
@@ -41,6 +173,10 @@ impl AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_paused(&self) -> bool {
|
||||
self.sink.is_paused()
|
||||
}
|
||||
|
||||
pub fn toggle_pause(&mut self) {
|
||||
if self.sink.is_paused() {
|
||||
self.sink.play();
|
||||
@@ -64,11 +200,23 @@ impl AudioPlayer {
|
||||
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,
|
||||
@@ -80,10 +228,7 @@ impl AudioPlayer {
|
||||
if !total.is_zero() && new_pos >= total {
|
||||
new_pos = total.saturating_sub(Duration::from_millis(100));
|
||||
}
|
||||
self.sink.try_seek(new_pos)?;
|
||||
if self.sink.is_paused() {
|
||||
self.paused_pos = Some(new_pos);
|
||||
}
|
||||
self.seek_to(new_pos)?;
|
||||
Ok(new_pos)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user