maolan 0.1.0

Rust DAW application for recording, editing, routing, automation, export, and plugin hosting
use super::{AudioClip, MIDIClip};
use crate::message::{MidiEditorViewMode, TrackAutomationMode, TrackAutomationTarget};
use iced::Point;
use serde::{Deserialize, Deserializer, Serialize};

pub use crate::consts::state_track::{
    TRACK_FOLDER_HEADER_HEIGHT, TRACK_SUBTRACK_GAP, TRACK_SUBTRACK_MIN_HEIGHT,
};

#[derive(Debug, Clone, Copy)]
pub struct TrackLaneLayout {
    pub header_height: f32,
    pub lane_height: f32,
    pub audio_lanes: usize,
    pub midi_lanes: usize,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AudioData {
    pub clips: Vec<AudioClip>,
    pub ins: usize,
    pub outs: usize,
}

impl AudioData {
    pub fn new(ins: usize, outs: usize) -> Self {
        Self {
            clips: vec![],
            ins,
            outs,
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MIDIData {
    pub clips: Vec<MIDIClip>,
    pub ins: usize,
    pub outs: usize,
    #[serde(default)]
    pub editor_view_mode: MidiEditorViewMode,
}

impl MIDIData {
    pub fn new(ins: usize, outs: usize) -> Self {
        Self {
            clips: vec![],
            ins,
            outs,
            editor_view_mode: MidiEditorViewMode::PianoRoll,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackAutomationPoint {
    pub sample: usize,
    pub value: f32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackAutomationLane {
    pub target: TrackAutomationTarget,
    pub visible: bool,
    pub points: Vec<TrackAutomationPoint>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)]
pub struct EditorMarker {
    pub sample: usize,
    #[serde(default)]
    pub name: String,
}

impl<'de> Deserialize<'de> for EditorMarker {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum LegacyOrCurrent {
            Legacy(usize),
            Current {
                sample: usize,
                #[serde(default)]
                name: String,
            },
        }

        match LegacyOrCurrent::deserialize(deserializer)? {
            LegacyOrCurrent::Legacy(sample) => Ok(Self {
                sample,
                name: String::new(),
            }),
            LegacyOrCurrent::Current { sample, name } => Ok(Self { sample, name }),
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(remote = "Point")]
struct PointDef {
    x: f32,
    y: f32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Track {
    id: usize,
    pub name: String,
    pub level: f32,
    pub balance: f32,
    #[serde(skip, default)]
    pub meter_out_db: Vec<f32>,
    pub armed: bool,
    pub muted: bool,
    pub phase_inverted: bool,
    pub soloed: bool,
    pub input_monitor: bool,
    pub disk_monitor: bool,
    #[serde(default)]
    pub midi_learn_volume: Option<maolan_engine::message::MidiLearnBinding>,
    #[serde(default)]
    pub midi_learn_balance: Option<maolan_engine::message::MidiLearnBinding>,
    #[serde(default)]
    pub midi_learn_mute: Option<maolan_engine::message::MidiLearnBinding>,
    #[serde(default)]
    pub midi_learn_solo: Option<maolan_engine::message::MidiLearnBinding>,
    #[serde(default)]
    pub midi_learn_arm: Option<maolan_engine::message::MidiLearnBinding>,
    #[serde(default)]
    pub midi_learn_input_monitor: Option<maolan_engine::message::MidiLearnBinding>,
    #[serde(default)]
    pub midi_learn_disk_monitor: Option<maolan_engine::message::MidiLearnBinding>,
    #[serde(default)]
    pub vca_master: Option<String>,
    #[serde(default)]
    pub frozen: bool,
    pub height: f32,
    #[serde(default)]
    pub primary_audio_ins: usize,
    #[serde(default)]
    pub primary_audio_outs: usize,
    pub audio: AudioData,
    pub midi: MIDIData,
    #[serde(default)]
    pub midi_lane_channels: Vec<Option<u8>>,
    #[serde(default)]
    pub frozen_audio_backup: Vec<AudioClip>,
    #[serde(default)]
    pub frozen_midi_backup: Vec<MIDIClip>,
    #[serde(default)]
    pub frozen_render_clip: Option<String>,
    #[serde(default)]
    pub automation_lanes: Vec<TrackAutomationLane>,
    #[serde(default)]
    pub editor_markers: Vec<EditorMarker>,
    #[serde(default = "default_automation_mode")]
    pub automation_mode: TrackAutomationMode,
    #[serde(with = "PointDef")]
    pub position: Point,
}

fn default_automation_mode() -> TrackAutomationMode {
    TrackAutomationMode::Read
}

impl Track {
    pub fn new(
        name: String,
        level: f32,
        audio_ins: usize,
        audio_outs: usize,
        midi_ins: usize,
        midi_outs: usize,
    ) -> Self {
        let mut track = Self {
            id: 0,
            name,
            level,
            balance: 0.0,
            meter_out_db: vec![-90.0; audio_outs],
            armed: false,
            muted: false,
            phase_inverted: false,
            soloed: false,
            input_monitor: false,
            disk_monitor: true,
            midi_learn_volume: None,
            midi_learn_balance: None,
            midi_learn_mute: None,
            midi_learn_solo: None,
            midi_learn_arm: None,
            midi_learn_input_monitor: None,
            midi_learn_disk_monitor: None,
            vca_master: None,
            frozen: false,
            audio: AudioData::new(audio_ins, audio_outs),
            midi: MIDIData::new(midi_ins, midi_outs),
            midi_lane_channels: vec![None; midi_ins],
            primary_audio_ins: audio_ins,
            primary_audio_outs: audio_outs,
            frozen_audio_backup: vec![],
            frozen_midi_backup: vec![],
            frozen_render_clip: None,
            automation_lanes: vec![],
            editor_markers: vec![],
            automation_mode: TrackAutomationMode::Read,
            height: 60.0,
            position: Point::new(100.0, 100.0),
        };
        track.height = track.min_height_for_layout().max(60.0);
        track
    }

    pub fn audio_lane_count(&self) -> usize {
        // Always return 1 audio lane if we have any audio inputs
        if self.audio.ins > 0 { 1 } else { 0 }
    }

    pub fn primary_audio_ins(&self) -> usize {
        if self.primary_audio_ins == 0 && self.audio.ins > 0 {
            self.audio.ins
        } else {
            self.primary_audio_ins.min(self.audio.ins)
        }
    }

    pub fn primary_audio_outs(&self) -> usize {
        if self.primary_audio_outs == 0 && self.audio.outs > 0 {
            self.audio.outs
        } else {
            self.primary_audio_outs.min(self.audio.outs)
        }
    }

    pub fn return_count(&self) -> usize {
        self.audio.ins.saturating_sub(self.primary_audio_ins())
    }

    pub fn send_count(&self) -> usize {
        self.audio.outs.saturating_sub(self.primary_audio_outs())
    }

    pub fn midi_lane_count(&self) -> usize {
        self.midi.ins
    }

    pub fn automation_lane_count(&self) -> usize {
        self.automation_lanes
            .iter()
            .filter(|lane| lane.visible)
            .count()
    }

    pub fn total_lane_count(&self) -> usize {
        self.audio_lane_count()
            .saturating_add(self.midi_lane_count())
            .saturating_add(self.automation_lane_count())
    }

    pub fn min_height_for_layout(&self) -> f32 {
        let lanes = self.total_lane_count().max(1);
        TRACK_FOLDER_HEADER_HEIGHT
            + (lanes as f32 * TRACK_SUBTRACK_MIN_HEIGHT)
            + ((lanes.saturating_sub(1)) as f32 * TRACK_SUBTRACK_GAP)
            + 8.0
    }

    pub fn lane_layout(&self) -> TrackLaneLayout {
        let total_lanes = self.total_lane_count().max(1);
        let available = (self.height - TRACK_FOLDER_HEADER_HEIGHT - 8.0).max(0.0);
        let gaps = (total_lanes.saturating_sub(1)) as f32 * TRACK_SUBTRACK_GAP;
        let lane_height = ((available - gaps) / total_lanes as f32).max(TRACK_SUBTRACK_MIN_HEIGHT);
        TrackLaneLayout {
            header_height: TRACK_FOLDER_HEADER_HEIGHT,
            lane_height,
            audio_lanes: self.audio_lane_count(),
            midi_lanes: self.midi_lane_count(),
        }
    }

    pub fn lane_top(&self, kind: maolan_engine::kind::Kind, lane: usize) -> f32 {
        let layout = self.lane_layout();
        let mut y = layout.header_height;
        match kind {
            maolan_engine::kind::Kind::Audio => {
                y + lane.min(layout.audio_lanes.saturating_sub(1)) as f32
                    * (layout.lane_height + TRACK_SUBTRACK_GAP)
            }
            maolan_engine::kind::Kind::MIDI => {
                y += layout.audio_lanes as f32 * (layout.lane_height + TRACK_SUBTRACK_GAP);
                y + lane.min(layout.midi_lanes.saturating_sub(1)) as f32
                    * (layout.lane_height + TRACK_SUBTRACK_GAP)
            }
        }
    }

    pub fn lane_index_at_y(&self, kind: maolan_engine::kind::Kind, y: f32) -> usize {
        let layout = self.lane_layout();
        let lane_span = layout.lane_height + TRACK_SUBTRACK_GAP;
        let local = (y - layout.header_height).max(0.0);
        match kind {
            maolan_engine::kind::Kind::Audio => {
                if layout.audio_lanes == 0 {
                    0
                } else {
                    ((local / lane_span).floor() as usize).min(layout.audio_lanes - 1)
                }
            }
            maolan_engine::kind::Kind::MIDI => {
                let midi_local = local - (layout.audio_lanes as f32 * lane_span);
                if layout.midi_lanes == 0 {
                    0
                } else {
                    ((midi_local.max(0.0) / lane_span).floor() as usize).min(layout.midi_lanes - 1)
                }
            }
        }
    }

    pub fn automation_lane_top(&self, lane: usize) -> f32 {
        let layout = self.lane_layout();
        let lane_span = layout.lane_height + TRACK_SUBTRACK_GAP;
        layout.header_height + (layout.audio_lanes + layout.midi_lanes + lane) as f32 * lane_span
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn audio_data_new_creates_empty() {
        let data = AudioData::new(2, 2);
        assert!(data.clips.is_empty());
        assert_eq!(data.ins, 2);
        assert_eq!(data.outs, 2);
    }

    #[test]
    fn midi_data_new_creates_empty() {
        let data = MIDIData::new(1, 1);
        assert!(data.clips.is_empty());
        assert_eq!(data.ins, 1);
        assert_eq!(data.outs, 1);
    }

    #[test]
    fn track_automation_point_creation() {
        let point = TrackAutomationPoint {
            sample: 100,
            value: 0.5,
        };
        assert_eq!(point.sample, 100);
        assert!((point.value - 0.5).abs() < f32::EPSILON);
    }

    #[test]
    fn track_automation_lane_default() {
        use crate::message::TrackAutomationTarget;
        let lane = TrackAutomationLane {
            target: TrackAutomationTarget::Volume,
            visible: true,
            points: vec![],
        };
        assert!(lane.visible);
        assert!(lane.points.is_empty());
    }

    #[test]
    fn editor_marker_creation() {
        let marker = EditorMarker {
            sample: 48000,
            name: "Verse".to_string(),
        };
        assert_eq!(marker.sample, 48000);
        assert_eq!(marker.name, "Verse");
    }

    #[test]
    fn track_lane_layout_creation() {
        let layout = TrackLaneLayout {
            header_height: 24.0,
            lane_height: 80.0,
            audio_lanes: 2,
            midi_lanes: 1,
        };
        assert_eq!(layout.header_height, 24.0);
        assert_eq!(layout.audio_lanes, 2);
    }
}