2026-03-05 15:41:53 +01:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
2026-05-27 22:04:08 +02:00
|
|
|
// ─── Defaults ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
pub const PITCH_DEFAULT: f32 = 0.0;
|
|
|
|
|
pub const TEMPO_DEFAULT: f32 = 1.0;
|
|
|
|
|
pub const CUTOFF_DEFAULT: f32 = 0.0;
|
|
|
|
|
pub const REVERB_DEFAULT: f32 = 0.0;
|
|
|
|
|
pub const FADER_DEFAULT: f32 = 0.8;
|
|
|
|
|
|
2026-03-05 15:41:53 +01:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
|
|
|
pub enum DeckId {
|
|
|
|
|
A = 0,
|
|
|
|
|
Z = 1,
|
|
|
|
|
S = 2,
|
|
|
|
|
X = 3,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DeckId {
|
|
|
|
|
pub fn from_index(i: usize) -> Option<Self> {
|
|
|
|
|
match i {
|
|
|
|
|
0 => Some(Self::A),
|
|
|
|
|
1 => Some(Self::Z),
|
|
|
|
|
2 => Some(Self::S),
|
|
|
|
|
3 => Some(Self::X),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn label(self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
Self::A => "A",
|
|
|
|
|
Self::Z => "Z",
|
|
|
|
|
Self::S => "S",
|
|
|
|
|
Self::X => "X",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Lowercase node name suffix used in PipeWire node names
|
|
|
|
|
pub fn node_suffix(self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
Self::A => "a",
|
|
|
|
|
Self::Z => "z",
|
|
|
|
|
Self::S => "s",
|
|
|
|
|
Self::X => "x",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub enum PlayState {
|
|
|
|
|
Stopped,
|
|
|
|
|
Playing,
|
|
|
|
|
Paused,
|
|
|
|
|
Cued,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PlayState {
|
|
|
|
|
pub fn icon(&self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Playing => "▶",
|
|
|
|
|
Self::Paused => "⏸",
|
|
|
|
|
Self::Stopped => "⏹",
|
|
|
|
|
Self::Cued => "⏮",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct Deck {
|
|
|
|
|
pub id: DeckId,
|
|
|
|
|
|
|
|
|
|
// Track
|
2026-05-27 22:04:08 +02:00
|
|
|
pub track: Option<PathBuf>,
|
2026-03-05 15:41:53 +01:00
|
|
|
pub track_name: Option<String>,
|
2026-05-27 22:04:08 +02:00
|
|
|
pub artist: Option<String>,
|
|
|
|
|
pub album: Option<String>,
|
2026-03-05 15:41:53 +01:00
|
|
|
|
|
|
|
|
// Transport
|
2026-05-27 22:04:08 +02:00
|
|
|
pub play_state: PlayState,
|
|
|
|
|
pub position: u64,
|
|
|
|
|
pub duration: u64,
|
2026-03-05 15:41:53 +01:00
|
|
|
pub sample_rate: u32,
|
2026-05-27 22:04:08 +02:00
|
|
|
pub cue_point: Option<u64>,
|
2026-03-05 15:41:53 +01:00
|
|
|
|
2026-05-27 22:04:08 +02:00
|
|
|
// Mix / effect controls (mirror of the realtime DSP state in audio.rs)
|
|
|
|
|
pub fader: f32, // 0.0 .. 1.5
|
|
|
|
|
pub pitch: f32, // semitones, -12 .. +12
|
|
|
|
|
pub tempo: f32, // 0.5 .. 2.0
|
|
|
|
|
pub cutoff: f32, // < 0 lowpass Hz, > 0 highpass Hz, 0 flat
|
|
|
|
|
pub reverb: f32, // 0.0 .. 1.0
|
2026-03-05 15:41:53 +01:00
|
|
|
|
2026-05-27 22:04:08 +02:00
|
|
|
// VU (updated by audio engine at ~60 Hz)
|
2026-03-05 15:41:53 +01:00
|
|
|
pub vu: (f32, f32),
|
|
|
|
|
|
2026-05-27 22:04:08 +02:00
|
|
|
// Per-bucket band peaks for the waveform: [bass, mid, high], one entry per
|
|
|
|
|
// ~0.1 s of audio (PEAK_BUCKET frames). Sent in WaveformChunks during decode.
|
|
|
|
|
pub waveform_bands: Vec<[f32; 3]>,
|
|
|
|
|
|
|
|
|
|
/// BPM estimate from the decode-time analyzer; `None` until detected.
|
|
|
|
|
pub bpm: Option<f32>,
|
2026-03-05 15:41:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Deck {
|
|
|
|
|
pub fn new(id: DeckId) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
id,
|
2026-05-27 22:04:08 +02:00
|
|
|
track: None,
|
|
|
|
|
track_name: None,
|
|
|
|
|
artist: None,
|
|
|
|
|
album: None,
|
|
|
|
|
play_state: PlayState::Stopped,
|
|
|
|
|
position: 0,
|
|
|
|
|
duration: 0,
|
2026-03-05 15:41:53 +01:00
|
|
|
sample_rate: 44100,
|
2026-05-27 22:04:08 +02:00
|
|
|
cue_point: None,
|
|
|
|
|
fader: FADER_DEFAULT,
|
|
|
|
|
pitch: PITCH_DEFAULT,
|
|
|
|
|
tempo: TEMPO_DEFAULT,
|
|
|
|
|
cutoff: CUTOFF_DEFAULT,
|
|
|
|
|
reverb: REVERB_DEFAULT,
|
|
|
|
|
vu: (0.0, 0.0),
|
|
|
|
|
waveform_bands: Vec::new(),
|
|
|
|
|
bpm: None,
|
2026-03-05 15:41:53 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_loaded(&self) -> bool {
|
|
|
|
|
self.track.is_some()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 22:04:08 +02:00
|
|
|
pub fn peaks_empty(&self) -> bool {
|
|
|
|
|
self.waveform_bands.is_empty()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 15:41:53 +01:00
|
|
|
pub fn progress(&self) -> f32 {
|
|
|
|
|
if self.duration == 0 { 0.0 } else {
|
|
|
|
|
(self.position as f32 / self.duration as f32).clamp(0.0, 1.0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn elapsed(&self) -> (u64, u64) {
|
|
|
|
|
let s = self.position / self.sample_rate as u64;
|
|
|
|
|
(s / 60, s % 60)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn remaining(&self) -> (u64, u64) {
|
|
|
|
|
let s = self.duration.saturating_sub(self.position) / self.sample_rate as u64;
|
|
|
|
|
(s / 60, s % 60)
|
|
|
|
|
}
|
|
|
|
|
}
|