136 lines
3.1 KiB
Rust
136 lines
3.1 KiB
Rust
use std::path::PathBuf;
|
||||
|
|
|
|||
|
|
#[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
|
|||
|
|
pub track: Option<PathBuf>,
|
|||
|
|
pub track_name: Option<String>,
|
|||
|
|
|
|||
|
|
// Transport
|
|||
|
|
pub play_state: PlayState,
|
|||
|
|
pub position: u64, // samples
|
|||
|
|
pub duration: u64, // samples
|
|||
|
|
pub sample_rate: u32,
|
|||
|
|
pub cue_point: Option<u64>,
|
|||
|
|
|
|||
|
|
// Per-deck mix controls
|
|||
|
|
/// 0.0 – 1.0, sent to wpctl on the deck's PipeWire stream node
|
|||
|
|
pub fader: f32,
|
|||
|
|
|
|||
|
|
// Effect chain parameters (sent to pw-cli / filter-chain node)
|
|||
|
|
/// Semitones -12.0 .. +12.0
|
|||
|
|
pub pitch: f32,
|
|||
|
|
/// Tempo multiplier 0.5 .. 2.0
|
|||
|
|
pub tempo: f32,
|
|||
|
|
|
|||
|
|
// VU (updated by audio engine)
|
|||
|
|
pub vu: (f32, f32),
|
|||
|
|
|
|||
|
|
// PipeWire object IDs (populated after graph setup)
|
|||
|
|
/// Object ID of the deck's playback stream node
|
|||
|
|
pub stream_node_id: Option<u32>,
|
|||
|
|
/// Object ID of the filter-chain node for this deck
|
|||
|
|
pub filter_node_id: Option<u32>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
impl Deck {
|
|||
|
|
pub fn new(id: DeckId) -> Self {
|
|||
|
|
Self {
|
|||
|
|
id,
|
|||
|
|
track: None,
|
|||
|
|
track_name: None,
|
|||
|
|
play_state: PlayState::Stopped,
|
|||
|
|
position: 0,
|
|||
|
|
duration: 0,
|
|||
|
|
sample_rate: 44100,
|
|||
|
|
cue_point: None,
|
|||
|
|
fader: 0.8,
|
|||
|
|
pitch: 0.0,
|
|||
|
|
tempo: 1.0,
|
|||
|
|
vu: (0.0, 0.0),
|
|||
|
|
stream_node_id: None,
|
|||
|
|
filter_node_id: None,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pub fn is_loaded(&self) -> bool {
|
|||
|
|
self.track.is_some()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
}
|
|||
|
|
}
|