record-player 0.1.0

Standalone Bitneedle record player, acoustic scratch engine, and browser command protocol
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DeckId { A, B }

impl DeckId {
    pub const ALL: [Self; 2] = [Self::A, Self::B];
    pub const fn index(self) -> usize { match self { Self::A => 0, Self::B => 1 } }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LoadStatus { Empty, Loading, Ready, Failed }

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SurfaceRegion { Programme, LeadIn, Deadwax }

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlaybackState {
    pub load_status: LoadStatus,
    pub duration_seconds: f64,
    pub current_seconds: f64,
    pub playing: bool,
    pub pending_play_when_ready: bool,
    pub suspended_at_seconds: Option<f64>,
    pub playback_rate: f32,
}

impl Default for PlaybackState {
    fn default() -> Self {
        Self { load_status: LoadStatus::Empty, duration_seconds: 0.0, current_seconds: 0.0, playing: false, pending_play_when_ready: false, suspended_at_seconds: None, playback_rate: 1.0 }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TransportState { pub motor_on: bool }
impl Default for TransportState { fn default() -> Self { Self { motor_on: false } } }

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NeedleState { pub lifted: bool }
impl Default for NeedleState { fn default() -> Self { Self { lifted: true } } }

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MixerState {
    pub channel_gain: f32,
    pub crossfader: f32,
}
impl Default for MixerState { fn default() -> Self { Self { channel_gain: 1.0, crossfader: 0.5 } } }

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScratchState {
    pub active: bool,
    pub pointer_id: Option<i32>,
    pub was_playing: bool,
    pub started_at_seconds: f64,
    pub target_position_frames: f64,
    pub rendered_position_frames: f64,
    pub target_rate: f32,
    pub base_rotation_degrees: f64,
}
impl Default for ScratchState {
    fn default() -> Self { Self { active: false, pointer_id: None, was_playing: false, started_at_seconds: 0.0, target_position_frames: 0.0, rendered_position_frames: 0.0, target_rate: 0.0, base_rotation_degrees: 0.0 } }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeckState {
    pub loaded: bool,
    pub transport: TransportState,
    pub needle: NeedleState,
    pub playback: PlaybackState,
    pub mixer: MixerState,
    pub scratch: ScratchState,
}
impl Default for DeckState {
    fn default() -> Self { Self { loaded: false, transport: TransportState::default(), needle: NeedleState::default(), playback: PlaybackState::default(), mixer: MixerState::default(), scratch: ScratchState::default() } }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TimedRegionState {
    pub active: bool,
    pub completed: bool,
    pub started_at_ms: f64,
    pub duration_ms: f64,
}
impl Default for TimedRegionState {
    fn default() -> Self { Self { active: false, completed: false, started_at_ms: 0.0, duration_ms: 0.0 } }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClipLoopState { pub active: bool, pub clip_id: Option<String> }
impl Default for ClipLoopState { fn default() -> Self { Self { active: false, clip_id: None } } }

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlayerConfig {
    pub sample_rate: f64,
    pub sharp_crossfader_width: f32,
}
impl Default for PlayerConfig { fn default() -> Self { Self { sample_rate: 48_000.0, sharp_crossfader_width: 0.08 } } }

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlayerState {
    pub decks: [DeckState; 2],
    pub active_deck: DeckId,
    pub lead_in: TimedRegionState,
    pub deadwax: TimedRegionState,
    pub clip_loop: ClipLoopState,
}
impl Default for PlayerState {
    fn default() -> Self { Self { decks: [DeckState::default(), DeckState::default()], active_deck: DeckId::A, lead_in: TimedRegionState::default(), deadwax: TimedRegionState::default(), clip_loop: ClipLoopState::default() } }
}