sim-lib-view-daw 0.1.2

DAW timeline, mixer, plugin rack, and synth lenses for SIM Web.
Documentation
//! Piano-roll Scene descriptors for playable event streams.

use sim_kernel::{Expr, Symbol};
use sim_lib_scene::{data_map, node, sym};
use sim_value::build::{int, list, text, uint};

/// Stable lens id for the stream-backed piano-roll view.
pub const PIANO_ROLL_VIEW_ID: &str = "view:piano-roll";

/// Demo fixture name for the keyboard, rack, and piano-roll workbench.
pub const PIANO_ROLL_DEMO_FIXTURE: &str = "keyboard-rack-roll";

/// Editing actions exposed by the piano-roll scene.
pub const PIANO_ROLL_EDIT_ACTIONS: &[&str] = &[
    "draw",
    "move",
    "trim",
    "split",
    "delete",
    "duplicate",
    "quantize",
    "set-velocity",
    "set-pitch",
    "set-lane",
    "set-curve",
    "freeze",
];

/// Lane family in a piano-roll view.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PianoRollLaneKind {
    /// Pitched MIDI-style notes.
    Note,
    /// Drum rows keyed by drum or pad name.
    Drum,
    /// Scale-relative degree rows.
    ScaleDegree,
    /// Object rows carrying nested playable objects.
    Object,
    /// Automation or modulation curves.
    Automation,
}

impl PianoRollLaneKind {
    fn as_str(self) -> &'static str {
        match self {
            Self::Note => "note",
            Self::Drum => "drum",
            Self::ScaleDegree => "scale-degree",
            Self::Object => "object",
            Self::Automation => "automation",
        }
    }
}

/// One event block shown in a piano-roll lane.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PianoRollEvent {
    /// Stable event id.
    pub id: Symbol,
    /// Lane id that owns the event.
    pub lane: Symbol,
    /// Start tick.
    pub at: u64,
    /// Event length in ticks.
    pub len: u64,
    /// Pitch, drum key, scale degree, or object row.
    pub pitch: i32,
    /// MIDI-style velocity or normalized intensity.
    pub velocity: u8,
    /// Event family.
    pub event_kind: PianoRollLaneKind,
    /// Whether the event is produced by a downstream player.
    pub generated: bool,
    /// Whether the event is currently performed live.
    pub live: bool,
    /// Optional curve name for automation lanes.
    pub curve: Option<String>,
}

impl PianoRollEvent {
    /// Build a deterministic fixture event.
    pub fn new(
        id: Symbol,
        lane: Symbol,
        at: u64,
        len: u64,
        pitch: i32,
        velocity: u8,
        event_kind: PianoRollLaneKind,
    ) -> Self {
        Self {
            id,
            lane,
            at,
            len,
            pitch,
            velocity,
            event_kind,
            generated: false,
            live: false,
            curve: None,
        }
    }

    /// Mark the event as generated by a player.
    pub fn generated(mut self) -> Self {
        self.generated = true;
        self
    }

    /// Mark the event as currently held by a performance source.
    pub fn live(mut self) -> Self {
        self.live = true;
        self
    }

    /// Attach an automation curve name.
    pub fn with_curve(mut self, curve: impl Into<String>) -> Self {
        self.curve = Some(curve.into());
        self
    }
}

/// One visible piano-roll lane.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PianoRollLane {
    /// Stable lane id.
    pub id: Symbol,
    /// Display label.
    pub label: String,
    /// Lane family.
    pub lane_kind: PianoRollLaneKind,
    /// Lane events.
    pub events: Vec<PianoRollEvent>,
}

/// Complete piano-roll view descriptor.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PianoRollView {
    /// Intent target edited by piano-roll actions.
    pub target: Symbol,
    /// Performance source that supplies live notes.
    pub source: Symbol,
    /// Player chain whose generated output is shown.
    pub player_chain: Symbol,
    /// Visible lanes.
    pub lanes: Vec<PianoRollLane>,
    /// Current live performance notes.
    pub live_notes: Vec<PianoRollEvent>,
    /// Current generated notes.
    pub generated_notes: Vec<PianoRollEvent>,
}

/// Render a piano-roll descriptor as a `scene/piano-roll` node.
pub fn piano_roll_view(view: &PianoRollView) -> Expr {
    node(
        "piano-roll",
        vec![
            ("lens", sym(PIANO_ROLL_VIEW_ID)),
            ("role", sym("piano-roll")),
            ("target", Expr::Symbol(view.target.clone())),
            ("source", Expr::Symbol(view.source.clone())),
            ("player-chain", Expr::Symbol(view.player_chain.clone())),
            (
                "edit-actions",
                list(
                    PIANO_ROLL_EDIT_ACTIONS
                        .iter()
                        .map(|action| text(*action))
                        .collect(),
                ),
            ),
            (
                "lanes",
                list(view.lanes.iter().map(piano_roll_lane_expr).collect()),
            ),
            (
                "live-notes",
                list(view.live_notes.iter().map(piano_roll_event_expr).collect()),
            ),
            (
                "generated-notes",
                list(
                    view.generated_notes
                        .iter()
                        .map(piano_roll_event_expr)
                        .collect(),
                ),
            ),
        ],
    )
}

/// Deterministic piano-roll fixture covering every lane family.
pub fn piano_roll_demo_view() -> PianoRollView {
    let note_lane = Symbol::qualified("music/piano-roll-lane", "lead-notes");
    let drum_lane = Symbol::qualified("music/piano-roll-lane", "drums");
    let degree_lane = Symbol::qualified("music/piano-roll-lane", "degrees");
    let object_lane = Symbol::qualified("music/piano-roll-lane", "objects");
    let automation_lane = Symbol::qualified("music/piano-roll-lane", "automation");
    let live = PianoRollEvent::new(
        Symbol::qualified("music/piano-roll-event", "live-c4"),
        note_lane.clone(),
        96,
        96,
        60,
        108,
        PianoRollLaneKind::Note,
    )
    .live();
    let generated = PianoRollEvent::new(
        Symbol::qualified("music/piano-roll-event", "generated-g4"),
        note_lane.clone(),
        192,
        96,
        67,
        92,
        PianoRollLaneKind::Note,
    )
    .generated();
    PianoRollView {
        target: Symbol::qualified("music/piano-roll", "lead"),
        source: Symbol::qualified("music/performance-source", "keyboard"),
        player_chain: Symbol::qualified("music/player-chain", "onscreen-keyboard"),
        lanes: vec![
            PianoRollLane {
                id: note_lane.clone(),
                label: "Notes".to_owned(),
                lane_kind: PianoRollLaneKind::Note,
                events: vec![live.clone(), generated.clone()],
            },
            PianoRollLane {
                id: drum_lane.clone(),
                label: "Drums".to_owned(),
                lane_kind: PianoRollLaneKind::Drum,
                events: vec![PianoRollEvent::new(
                    Symbol::qualified("music/piano-roll-event", "kick"),
                    drum_lane,
                    0,
                    48,
                    36,
                    120,
                    PianoRollLaneKind::Drum,
                )],
            },
            PianoRollLane {
                id: degree_lane.clone(),
                label: "Scale degrees".to_owned(),
                lane_kind: PianoRollLaneKind::ScaleDegree,
                events: vec![PianoRollEvent::new(
                    Symbol::qualified("music/piano-roll-event", "degree-five"),
                    degree_lane,
                    288,
                    96,
                    5,
                    96,
                    PianoRollLaneKind::ScaleDegree,
                )],
            },
            PianoRollLane {
                id: object_lane.clone(),
                label: "Objects".to_owned(),
                lane_kind: PianoRollLaneKind::Object,
                events: vec![PianoRollEvent::new(
                    Symbol::qualified("music/piano-roll-event", "phrase-a"),
                    object_lane,
                    384,
                    192,
                    0,
                    100,
                    PianoRollLaneKind::Object,
                )],
            },
            PianoRollLane {
                id: automation_lane.clone(),
                label: "Automation".to_owned(),
                lane_kind: PianoRollLaneKind::Automation,
                events: vec![
                    PianoRollEvent::new(
                        Symbol::qualified("music/piano-roll-event", "cutoff-rise"),
                        automation_lane,
                        0,
                        384,
                        0,
                        80,
                        PianoRollLaneKind::Automation,
                    )
                    .with_curve("rise"),
                ],
            },
        ],
        live_notes: vec![live],
        generated_notes: vec![generated],
    }
}

/// Deterministic piano-roll demo scene.
pub fn piano_roll_demo_scene() -> Expr {
    piano_roll_view(&piano_roll_demo_view())
}

fn piano_roll_lane_expr(lane: &PianoRollLane) -> Expr {
    data_map(vec![
        ("id", Expr::Symbol(lane.id.clone())),
        ("label", text(lane.label.clone())),
        ("lane-kind", text(lane.lane_kind.as_str())),
        (
            "events",
            list(lane.events.iter().map(piano_roll_event_expr).collect()),
        ),
    ])
}

fn piano_roll_event_expr(event: &PianoRollEvent) -> Expr {
    let mut fields = vec![
        ("id", Expr::Symbol(event.id.clone())),
        ("lane", Expr::Symbol(event.lane.clone())),
        ("event-kind", text(event.event_kind.as_str())),
        ("at", uint(event.at)),
        ("len", uint(event.len)),
        ("pitch", int(i64::from(event.pitch))),
        ("velocity", uint(u64::from(event.velocity))),
        ("generated", Expr::Bool(event.generated)),
        ("live", Expr::Bool(event.live)),
    ];
    if let Some(curve) = &event.curve {
        fields.push(("curve", text(curve.clone())));
    }
    data_map(fields)
}