use std::path::PathBuf;
use std::sync::Arc;
use playr_core::event::JobId;
use playr_core::samples::Plan;
use playr_core::wave::Peaks;
pub const MIN_FRAMES_PER_COLUMN: u64 = 2 * playr_core::wave::BUCKET;
pub use playr_app::Display;
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: bool,
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)
}
const EIGHTHS: [char; 9] = [
' ', '\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}',
'\u{2588}',
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fill {
Empty,
Rms,
Peak,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell {
pub glyph: char,
pub fg: Fill,
pub behind: Fill,
}
pub fn envelope_rows(columns: &[(f32, f32)], height: usize) -> Vec<Vec<Cell>> {
let eighths = |v: f32| (v.clamp(0.0, 1.0) * (8 * height) as f32).round() as usize;
let steps: Vec<(usize, usize)> = columns
.iter()
.map(|&(rms, peak)| {
let rms = eighths(rms);
(rms, eighths(peak).max(rms))
})
.collect();
(0..height)
.map(|row| {
let below = 8 * (height - 1 - row);
steps
.iter()
.map(|&(rms, peak)| {
let cell = |glyph, fg, behind| Cell { glyph, fg, behind };
if rms >= below + 8 {
cell(EIGHTHS[8], Fill::Rms, Fill::Empty)
} else if rms > below {
let behind = if peak >= below + 8 {
Fill::Peak
} else {
Fill::Empty
};
cell(EIGHTHS[rms - below], Fill::Rms, behind)
} else if peak > below {
cell(EIGHTHS[(peak - below).min(8)], Fill::Peak, Fill::Empty)
} else {
cell(' ', Fill::Empty, Fill::Empty)
}
})
.collect()
})
.collect()
}
pub fn braille_rows(extents: &[(f32, f32)], height: usize) -> Vec<String> {
let dots = 4 * height;
let row_of = |v: f32| ((1.0 - v.clamp(-1.0, 1.0)) / 2.0 * (dots - 1) as f32).round() as usize;
const BITS: [[u32; 4]; 2] = [[0x01, 0x02, 0x04, 0x40], [0x08, 0x10, 0x20, 0x80]];
let cells = extents.len().div_ceil(2);
let mut grid = vec![vec![0u32; cells]; height];
for (i, &(lo, hi)) in extents.iter().enumerate() {
if lo > hi {
continue;
}
for dot in row_of(hi)..=row_of(lo) {
grid[dot / 4][i / 2] |= BITS[i % 2][dot % 4];
}
}
grid.iter()
.map(|row| {
row.iter()
.map(|&bits| char::from_u32(0x2800 + bits).expect("in the Braille block"))
.collect()
})
.collect()
}
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)
}