use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use crate::action::Nudge;
use playr_core::event::JobId;
use playr_core::samples::Plan;
use playr_core::spectrum::BANDS;
use playr_core::wave::{Detail, Extent, Peaks};
pub use crate::Display;
pub const DETAIL_BELOW: u64 = 2 * playr_core::wave::BUCKET;
pub const DETAIL_MARGIN: Duration = Duration::from_secs(2);
pub const SNAP_WITHIN: Duration = Duration::from_millis(10);
pub const DB_FLOOR: f32 = -48.0;
pub const SPECTRUM_RANGE_DB: f32 = 90.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 snap: bool,
pub scale: Option<Scale>,
pub range: Option<Range>,
pub edge: Edge,
pub detail: DetailRead,
pub cursor: Option<u64>,
}
#[derive(Debug, Clone, Default)]
pub enum DetailRead {
#[default]
None,
Reading {
path: PathBuf,
job: JobId,
start: u64,
end: u64,
},
Ready {
path: PathBuf,
detail: Arc<Detail>,
},
Failed {
path: PathBuf,
start: u64,
end: u64,
},
}
impl DetailRead {
pub fn path(&self) -> Option<&PathBuf> {
match self {
DetailRead::None => None,
DetailRead::Reading { path, .. }
| DetailRead::Ready { path, .. }
| DetailRead::Failed { path, .. } => Some(path),
}
}
pub fn answers(&self, path: &PathBuf, a: u64, b: u64) -> bool {
match self {
DetailRead::Reading {
path: p,
start,
end,
..
}
| DetailRead::Failed {
path: p,
start,
end,
} => p == path && a >= *start && b <= *end,
DetailRead::Ready { path: p, detail } => p == path && detail.covers(a, b),
DetailRead::None => false,
}
}
}
impl Sampler {
pub fn detail(&self, playing: Option<&PathBuf>) -> Option<Arc<Detail>> {
match &self.detail {
DetailRead::Ready { path, detail } if Some(path) == playing => Some(detail.clone()),
_ => None,
}
}
pub fn range_ends(&self, playing: Option<&PathBuf>) -> (Option<u64>, Option<u64>) {
match &self.range {
Some(r) if Some(&r.path) == playing => (r.start, r.end),
_ => (None, None),
}
}
pub fn range(&self, playing: Option<&PathBuf>) -> Option<(u64, u64)> {
match self.range_ends(playing) {
(Some(a), Some(b)) => Some((a, b)),
_ => None,
}
}
pub fn set_range_start(&mut self, path: &PathBuf, frame: u64) {
let (_, end) = self.range_ends(Some(path));
self.range = Some(Range {
path: path.clone(),
start: Some(frame),
end: end.filter(|&e| e > frame),
});
}
pub fn set_range_end(&mut self, path: &PathBuf, frame: u64) {
let (start, _) = self.range_ends(Some(path));
self.range = Some(Range {
path: path.clone(),
start: start.filter(|&s| s < frame),
end: Some(frame),
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Edge {
#[default]
Start,
End,
}
impl Edge {
pub fn name(self) -> &'static str {
match self {
Edge::Start => "start",
Edge::End => "end",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Range {
pub path: PathBuf,
pub start: Option<u64>,
pub end: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Scale {
pub start: u64,
pub per_column: u64,
pub per_frame: u64,
pub columns: u64,
}
impl Scale {
pub fn frames(self, nudge: Nudge) -> i64 {
let frames = |columns: i64| {
let f = columns * self.per_column as i64 / self.per_frame.max(1) as i64;
f.max(1)
};
match nudge {
Nudge::Columns(n) => n.signum() * frames(n.abs()),
Nudge::Percent(p) => {
let shown = self.columns.max(1) as i64;
let columns = (shown * p.abs() / 100).clamp(1, shown);
p.signum() * frames(columns)
}
}
}
pub fn shown(self) -> (u64, u64) {
(
self.start,
self.start + frames_shown(self.columns, self.per_column, self.per_frame),
)
}
pub fn needs_detail(self) -> bool {
self.per_column < DETAIL_BELOW
}
}
fn frames_shown(columns: u64, per_column: u64, per_frame: u64) -> u64 {
if per_frame > 1 {
columns.div_ceil(per_frame)
} else {
per_column * columns
}
}
pub fn frame_of(time: Duration, rate: u32) -> u64 {
(time.as_secs_f64() * rate as f64).round() as u64
}
pub fn time_of(frame: u64, rate: u32) -> Duration {
Duration::from_secs_f64(frame as f64 / rate.max(1) as f64)
}
pub fn snap(peaks: &Peaks, frame: u64) -> u64 {
let within = frame_of(SNAP_WITHIN, peaks.rate);
peaks
.crossing(frame.saturating_sub(within), frame + within, frame)
.unwrap_or(frame)
}
pub fn nudge(peaks: &Peaks, from: u64, step: i64, snap: bool) -> u64 {
let to = from
.saturating_add_signed(step)
.min(peaks.frames.saturating_sub(1));
if !snap || step == 0 {
return to;
}
let within = frame_of(SNAP_WITHIN, peaks.rate);
let (lo, hi) = if step > 0 {
((from + 1).max(to.saturating_sub(within)), to + within)
} else {
(
to.saturating_sub(within),
(to + within).min(from.saturating_sub(1)),
)
};
peaks.crossing(lo, hi, to).unwrap_or(to)
}
pub fn window(
frames: u64,
width: u64,
zoom: u32,
at: u64,
most_per_frame: u64,
) -> (u64, u64, u64, u32) {
let bucket = playr_core::wave::BUCKET;
let width = width.max(1);
let fit = frames.div_ceil(width).max(1);
let to_one = fit.ilog2();
let zoom = zoom.min(to_one + most_per_frame.max(1).ilog2());
let (per_column, per_frame) = match zoom.checked_sub(to_one) {
Some(past) if past > 0 => (1, 1 << past),
_ => match fit >> zoom {
fine if fine < DETAIL_BELOW => (fine, 1),
coarse => (coarse.next_multiple_of(bucket), 1),
},
};
let shown = frames_shown(width, per_column, per_frame);
let start = if shown >= frames {
0
} else {
let start = at.saturating_sub(shown / 2).min(frames - shown);
if per_column >= DETAIL_BELOW {
start / bucket * bucket
} else {
start
}
};
(start, per_column, per_frame, 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 per_frame: u64,
pub zoom: u32,
pub at: u64,
pub marks: Vec<u64>,
pub region: (u64, u64),
pub range: (Option<u64>, Option<u64>),
pub loudest: f32,
pub detail: Option<Arc<Detail>>,
}
impl Layout {
pub fn new(
peaks: Arc<Peaks>,
columns: u64,
zoom: u32,
position: Duration,
marks: &[Duration],
most_per_frame: u64,
) -> Layout {
let rate = peaks.rate.max(1);
let frame_of = |d: Duration| frame_of(d, rate);
let at = frame_of(position);
let columns = columns.max(1);
let (start, per_column, per_frame, zoom) =
window(peaks.frames, columns, zoom, at, most_per_frame);
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,
per_frame,
zoom,
at,
marks,
region,
range: (None, None),
loudest,
detail: None,
}
}
pub fn with_detail(mut self, detail: Option<Arc<Detail>>) -> Layout {
self.detail = detail;
self
}
pub fn with_range(mut self, (start, end): (Option<u64>, Option<u64>)) -> Layout {
self.range = (start, end);
if let (Some(a), Some(b)) = (start, end) {
self.region = (a, b);
}
self
}
pub fn columns(&self) -> Scale {
Scale {
start: self.start,
per_column: self.per_column,
per_frame: self.per_frame,
columns: self.columns,
}
}
pub fn span_of(&self, c: usize) -> (u64, u64) {
if self.per_frame > 1 {
let f = self.start + c as u64 / self.per_frame;
return (f, f + 1);
}
let a = self.start + c as u64 * self.per_column;
(a, a + self.per_column)
}
pub fn column_of(&self, frame: u64) -> Option<usize> {
let shown = frames_shown(self.columns, self.per_column, self.per_frame);
(frame >= self.start && frame < self.start + shown)
.then(|| ((frame - self.start) * self.per_frame / 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 {
let shown = frames_shown(self.columns, self.per_column, self.per_frame);
(self.start + shown).min(self.peaks.frames)
}
fn range(&self, a: u64, b: u64) -> Option<Extent> {
match &self.detail {
Some(d) if self.per_column < DETAIL_BELOW && d.covers(a, b) => d.range(a, b),
_ => self.peaks.range(a, b),
}
}
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.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.range(a, b).map_or((1.0, -1.0), |e| {
(e.min / self.loudest, e.max / self.loudest)
})
}
pub fn spectrum(&self, c: usize, rows: usize) -> Vec<f32> {
let spectrum = &self.peaks.spectrum;
let (a, b) = self.span_of(c);
let Some(bands) = spectrum.column(a, b.min(self.peaks.frames)) else {
return Vec::new();
};
let top = spectrum.loudest();
(0..rows)
.map(|r| {
let lo = r * BANDS / rows;
let hi = ((r + 1) * BANDS / rows).max(lo + 1);
let db = bands[lo..hi].iter().copied().fold(f32::MIN, f32::max);
((db - top) / SPECTRUM_RANGE_DB + 1.0).clamp(0.0, 1.0)
})
.collect()
}
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 / self.per_frame 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 self.per_frame > 1 {
format!("1/{} frame", self.per_frame)
} else if self.per_column == 1 {
"1 frame".into()
} else if self.per_column < DETAIL_BELOW {
format!("{} frames", self.per_column)
} else 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;
let name = match self.range {
(Some(_), Some(_)) => "range",
_ => "region",
};
format!(
"{name} {}-{} ({:.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()
}