polaris-mirror/src/app/peaks.rs
2024-09-02 13:57:25 -07:00

179 lines
4.5 KiB
Rust

use std::{
hash::{DefaultHasher, Hash, Hasher},
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use symphonia::core::{
audio::SampleBuffer,
codecs::{DecoderOptions, CODEC_TYPE_NULL},
formats::FormatOptions,
io::{MediaSourceStream, MediaSourceStreamOptions},
meta::MetadataOptions,
probe::Hint,
};
use tokio::{io::AsyncWriteExt, task::spawn_blocking};
use crate::app::Error;
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Peaks {
pub interleaved: Vec<u8>,
}
#[derive(Clone)]
pub struct Manager {
peaks_dir_path: PathBuf,
}
impl Manager {
pub fn new(peaks_dir_path: PathBuf) -> Self {
Self { peaks_dir_path }
}
pub async fn get_peaks(&self, audio_path: &Path) -> Result<Peaks, Error> {
match self.read_from_cache(audio_path).await {
Ok(Some(peaks)) => Ok(peaks),
_ => self.read_from_source(audio_path).await,
}
}
fn get_peaks_path(&self, audio_path: &Path) -> PathBuf {
let hash = Manager::hash(audio_path);
let mut peaks_path = self.peaks_dir_path.clone();
peaks_path.push(format!("{}.peaks", hash));
peaks_path
}
async fn read_from_cache(&self, audio_path: &Path) -> Result<Option<Peaks>, Error> {
let peaks_path = self.get_peaks_path(audio_path);
if peaks_path.exists() {
let serialized = tokio::fs::read(&peaks_path)
.await
.map_err(|e| Error::Io(peaks_path.clone(), e))?;
let peaks =
bitcode::deserialize::<Peaks>(&serialized).map_err(Error::PeaksDeserialization)?;
Ok(Some(peaks))
} else {
Ok(None)
}
}
async fn read_from_source(&self, audio_path: &Path) -> Result<Peaks, Error> {
let peaks = spawn_blocking({
let audio_path = audio_path.to_owned();
move || compute_peaks(&audio_path)
})
.await??;
let serialized = bitcode::serialize(&peaks).map_err(Error::PeaksSerialization)?;
tokio::fs::create_dir_all(&self.peaks_dir_path)
.await
.map_err(|e| Error::Io(self.peaks_dir_path.clone(), e))?;
let path = self.get_peaks_path(audio_path);
let mut out_file = tokio::fs::File::create(&path)
.await
.map_err(|e| Error::Io(path.clone(), e))?;
out_file
.write_all(&serialized)
.await
.map_err(|e| Error::Io(path.clone(), e))?;
Ok(peaks)
}
fn hash(path: &Path) -> u64 {
let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
hasher.finish()
}
}
fn compute_peaks(audio_path: &Path) -> Result<Peaks, Error> {
let peaks_per_minute = 4000;
let file =
std::fs::File::open(&audio_path).or_else(|e| Err(Error::Io(audio_path.to_owned(), e)))?;
let media_source = MediaSourceStream::new(Box::new(file), MediaSourceStreamOptions::default());
let mut peaks = Peaks::default();
peaks.interleaved.reserve(5 * peaks_per_minute);
let mut format = symphonia::default::get_probe()
.format(
&Hint::new(),
media_source,
&FormatOptions::default(),
&MetadataOptions::default(),
)
.map_err(Error::MediaProbeError)?
.format;
let track = format
.tracks()
.iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.ok_or_else(|| Error::MediaEmpty(audio_path.to_owned()))?;
let track_id = track.id;
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &DecoderOptions::default())
.map_err(Error::MediaDecoderError)?;
let (mut min, mut max) = (u8::MAX, u8::MIN);
let mut num_ingested = 0;
loop {
let packet = match format.next_packet() {
Ok(packet) => packet,
Err(symphonia::core::errors::Error::IoError(e))
if e.kind() == std::io::ErrorKind::UnexpectedEof =>
{
break;
}
Err(e) => return Err(Error::MediaPacketError(e)),
};
if packet.track_id() != track_id {
continue;
}
let decoded = match decoder.decode(&packet) {
Ok(d) => d,
Err(_) => continue,
};
let num_channels = decoded.spec().channels.count();
let sample_rate = decoded.spec().rate;
let num_samples_per_peak =
((sample_rate as f32) * 60.0 / (peaks_per_minute as f32)).round() as usize;
let mut buffer = SampleBuffer::<u8>::new(decoded.capacity() as u64, *decoded.spec());
buffer.copy_interleaved_ref(decoded);
for samples in buffer.samples().chunks_exact(num_channels) {
// Merge channels into mono signal
let mut mono: u32 = 0;
for sample in samples {
mono += *sample as u32;
}
mono /= samples.len() as u32;
min = u8::min(min, mono as u8);
max = u8::max(max, mono as u8);
num_ingested += 1;
if num_ingested >= num_samples_per_peak {
peaks.interleaved.push(min);
peaks.interleaved.push(max);
(min, max) = (u8::MAX, u8::MIN);
num_ingested = 0;
}
}
}
Ok(peaks)
}