use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use playr_core::event::JobId;
use playr_core::samples::Plan;
use playr_core::wave::Peaks;
pub use crate::Display;
pub const MIN_FRAMES_PER_COLUMN: u64 = 2 * playr_core::wave::BUCKET;
pub const DB_FLOOR: f32 = -48.0;
pub fn db_height(magnitude: f32) -> f32 {
if magnitude <= 0.0 {
return 0.0;
}
((20.0 * magnitude.log10() - DB_FLOOR) / -DB_FLOOR).clamp(0.0, 1.0)
}
#[derive(Debug, Clone, Default)]
pub enum Wave {
#[default]
None,
Reading {
path: PathBuf,
job: JobId,
},
Ready {
path: PathBuf,
peaks: Arc<Peaks>,
},
Failed {
path: PathBuf,
error: String,
},
}
impl Wave {
pub fn path(&self) -> Option<&PathBuf> {
match self {
Wave::None => None,
Wave::Reading { path, .. } | Wave::Ready { path, .. } | Wave::Failed { path, .. } => {
Some(path)
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Sampler {
pub wave: Wave,
pub display: Display,
pub zoom: u32,
pub planning: Option<JobId>,
pub pending: Option<Plan>,
}
pub fn window(frames: u64, width: u64, zoom: u32, at: u64) -> (u64, u64, u32) {
let bucket = playr_core::wave::BUCKET;
let width = width.max(1);
let fit = frames.div_ceil(width).max(1);
if fit < MIN_FRAMES_PER_COLUMN {
return (0, fit, 0);
}
let deepest = (0..63)
.find(|&z| fit >> z <= MIN_FRAMES_PER_COLUMN)
.unwrap_or(63);
let zoom = zoom.min(deepest);
let per_column = (fit >> zoom)
.max(MIN_FRAMES_PER_COLUMN)
.next_multiple_of(bucket);
let shown = per_column * width;
let start = if shown >= frames {
0
} else {
at.saturating_sub(shown / 2).min(frames - shown) / bucket * bucket
};
(start, per_column, zoom)
}
pub fn fmt_frames(frames: u64, rate: u32) -> String {
let ms = frames * 1000 / rate.max(1) as u64;
format!("{}:{:02}.{:03}", ms / 60_000, ms / 1000 % 60, ms % 1000)
}
pub fn peaks_of(sampler: &Sampler, playing: Option<&PathBuf>) -> Result<Arc<Peaks>, String> {
match (&sampler.wave, playing) {
(_, None) => Err("Nothing is playing. Play a track to see its waveform.".into()),
(Wave::Ready { path, peaks }, Some(playing)) if path == playing => Ok(peaks.clone()),
(Wave::Failed { error, .. }, _) => Err(format!("Cannot read the waveform: {error}")),
_ => Err("Reading the waveform...".into()),
}
}
pub fn plan_text(sampler: &Sampler) -> String {
match (&sampler.pending, sampler.planning) {
(_, Some(_)) => "planning slices".into(),
(Some(p), _) => format!(
"{} slices planned: enter writes, esc discards",
p.spans.len()
),
(None, _) => String::new(),
}
}
#[derive(Debug, Clone)]
pub struct Layout {
pub peaks: Arc<Peaks>,
pub rate: u32,
pub columns: u64,
pub start: u64,
pub per_column: u64,
pub zoom: u32,
pub at: u64,
pub marks: Vec<u64>,
pub region: (u64, u64),
pub loudest: f32,
}
impl Layout {
pub fn new(
peaks: Arc<Peaks>,
columns: u64,
zoom: u32,
position: Duration,
marks: &[Duration],
) -> Layout {
let rate = peaks.rate.max(1);
let frame_of = |d: Duration| (d.as_secs_f64() * rate as f64).round() as u64;
let at = frame_of(position);
let columns = columns.max(1);
let (start, per_column, zoom) = window(peaks.frames, columns, zoom, at);
let marks: Vec<u64> = marks.iter().map(|&d| frame_of(d)).collect();
let (region_start, region_end) = playr_core::samples::region(&marks, at);
let region = (region_start, region_end.unwrap_or(peaks.frames));
let loudest = peaks.loudest().max(f32::MIN_POSITIVE);
Layout {
peaks,
rate,
columns,
start,
per_column,
zoom,
at,
marks,
region,
loudest,
}
}
pub fn span_of(&self, c: usize) -> (u64, u64) {
let a = self.start + c as u64 * self.per_column;
(a, a + self.per_column)
}
pub fn column_of(&self, frame: u64) -> Option<usize> {
(frame >= self.start && frame < self.start + self.per_column * self.columns)
.then(|| ((frame - self.start) / self.per_column) as usize)
}
pub fn playhead(&self) -> Option<usize> {
self.column_of(self.at)
}
pub fn in_region(&self, c: usize) -> bool {
let (a, b) = self.span_of(c);
a < self.region.1 && b > self.region.0
}
pub fn end(&self) -> u64 {
(self.start + self.per_column * self.columns).min(self.peaks.frames)
}
pub fn heights(&self, display: Display, c: usize) -> (f32, f32) {
let height = |magnitude: f32| match display {
Display::Decibels => db_height(magnitude),
_ => magnitude / self.loudest,
};
let (a, b) = self.span_of(c);
self.peaks.range(a, b).map_or((0.0, 0.0), |e| {
(height(e.rms), height(e.min.abs().max(e.max.abs())))
})
}
pub fn extent(&self, a: u64, b: u64) -> (f32, f32) {
self.peaks.range(a, b).map_or((1.0, -1.0), |e| {
(e.min / self.loudest, e.max / self.loudest)
})
}
pub fn time_at(&self, columns: f32) -> Duration {
let frame = self.start as f64 + columns.max(0.0) as f64 * self.per_column as f64;
let frame = frame.min(self.peaks.frames as f64);
Duration::from_secs_f64(frame / self.rate as f64)
}
pub fn scale(&self) -> String {
let ms = self.per_column as f64 * 1000.0 / self.rate as f64;
if ms < 10.0 {
format!("{ms:.1} ms")
} else {
format!("{ms:.0} ms")
}
}
pub fn shown(&self) -> String {
format!(
"{}-{}",
fmt_frames(self.start, self.rate),
fmt_frames(self.end(), self.rate)
)
}
pub fn region_text(&self) -> String {
let (a, b) = self.region;
format!(
"region {}-{} ({:.3} s) marks {}",
fmt_frames(a, self.rate),
fmt_frames(b, self.rate),
(b - a) as f64 / self.rate as f64,
self.marks.len(),
)
}
}
pub fn edges(plan: &Plan) -> impl Iterator<Item = u64> + '_ {
plan.spans.iter().flat_map(|&(a, b)| [Some(a), b]).flatten()
}