Skip to main content

sim_lib_music_core/
piano_roll.rs

1use num_rational::Ratio;
2use sim_kernel::{Expr, Result as KernelResult, Symbol};
3use sim_lib_midi_core::{Channel, MidiEvent, U7, U14};
4
5use crate::model::ensure_non_negative;
6use crate::{
7    Articulation, LaneId, LaneKind, MusicError, Note, NoteEvent, PerformanceTake, Pitch, Time,
8};
9
10/// Timing grid for a piano roll.
11///
12/// Couples a ticks-per-quarter resolution with a quantization step measured in
13/// whole-note fractions.
14///
15/// # Examples
16///
17/// ```
18/// use sim_lib_music_core::TimeGrid;
19///
20/// let grid = TimeGrid::default();
21/// assert_eq!(grid.tpq, 480);
22/// ```
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct TimeGrid {
25    /// Ticks per quarter note.
26    pub tpq: u32,
27    /// Quantization step, as a fraction of a whole note.
28    pub step: Time,
29}
30
31impl TimeGrid {
32    /// Builds a grid, rejecting a zero `tpq` or a non-positive `step`.
33    ///
34    /// Returns `MusicError::InvalidPianoRollGrid` when the inputs are invalid.
35    pub fn new(tpq: u32, step: Time) -> Result<Self, MusicError> {
36        if tpq == 0 || step <= Time::from_integer(0) {
37            return Err(MusicError::InvalidPianoRollGrid);
38        }
39        Ok(Self { tpq, step })
40    }
41}
42
43impl Default for TimeGrid {
44    fn default() -> Self {
45        Self {
46            tpq: 480,
47            step: Ratio::new(1, 16),
48        }
49    }
50}
51
52/// A note placed at an absolute onset time.
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct TimedNote {
55    /// Absolute onset time of the note.
56    pub onset: Time,
57    /// The note sounded at `onset`.
58    pub note: Note,
59}
60
61/// A drum hit cell addressed by MIDI key.
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct DrumCell {
64    /// Onset time of the hit.
65    pub onset: Time,
66    /// Sounding duration of the hit.
67    pub duration: Time,
68    /// MIDI key (drum voice) struck.
69    pub key: U7,
70    /// Strike velocity.
71    pub velocity: U7,
72    /// MIDI channel of the hit.
73    pub channel: Channel,
74}
75
76/// A note addressed by scale degree and octave rather than absolute pitch.
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct ScaleDegreeCell {
79    /// Onset time of the cell.
80    pub onset: Time,
81    /// Sounding duration of the cell.
82    pub duration: Time,
83    /// Scale degree, relative to the prevailing scale.
84    pub degree: i16,
85    /// Octave offset applied to the degree.
86    pub octave: i8,
87    /// Strike velocity.
88    pub velocity: U7,
89    /// MIDI channel of the cell.
90    pub channel: Channel,
91}
92
93/// A cell that places a named runtime object on a lane.
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct ObjectCell {
96    /// Onset time of the object.
97    pub onset: Time,
98    /// Duration the object occupies.
99    pub duration: Time,
100    /// Symbol naming the placed object.
101    pub object: Symbol,
102}
103
104/// An automation breakpoint targeting a named parameter.
105#[derive(Clone, Debug, PartialEq, Eq)]
106pub struct AutomationCell {
107    /// Time of the breakpoint.
108    pub time: Time,
109    /// Symbol naming the automation target.
110    pub target: Symbol,
111    /// Value applied at `time`.
112    pub value: i64,
113}
114
115/// A MIDI control-change cell.
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct ControlChangeCell {
118    /// Time of the control change.
119    pub time: Time,
120    /// MIDI channel affected.
121    pub channel: Channel,
122    /// Controller number.
123    pub controller: U7,
124    /// New controller value.
125    pub value: U7,
126}
127
128/// A MIDI pitch-bend cell.
129#[derive(Clone, Debug, PartialEq, Eq)]
130pub struct PitchBendCell {
131    /// Time of the bend.
132    pub time: Time,
133    /// MIDI channel affected.
134    pub channel: Channel,
135    /// 14-bit bend value.
136    pub value: U14,
137}
138
139/// A MIDI polyphonic key-pressure cell.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct PolyPressureCell {
142    /// Time of the pressure event.
143    pub time: Time,
144    /// MIDI channel affected.
145    pub channel: Channel,
146    /// Key the pressure applies to.
147    pub key: U7,
148    /// Pressure amount.
149    pub pressure: U7,
150}
151
152/// A MIDI channel-pressure (aftertouch) cell.
153#[derive(Clone, Debug, PartialEq, Eq)]
154pub struct ChannelPressureCell {
155    /// Time of the pressure event.
156    pub time: Time,
157    /// MIDI channel affected.
158    pub channel: Channel,
159    /// Pressure amount applied to the whole channel.
160    pub pressure: U7,
161}
162
163/// A single placed event in a piano roll lane.
164///
165/// Each variant carries the cell payload appropriate to its lane kind.
166#[derive(Clone, Debug, PartialEq, Eq)]
167pub enum PianoRollCell {
168    /// A pitched note.
169    Note(TimedNote),
170    /// A drum hit.
171    Drum(DrumCell),
172    /// A scale-degree note.
173    ScaleDegree(ScaleDegreeCell),
174    /// A placed runtime object.
175    Object(ObjectCell),
176    /// An automation breakpoint.
177    Automation(AutomationCell),
178    /// A MIDI control change.
179    ControlChange(ControlChangeCell),
180    /// A MIDI pitch bend.
181    PitchBend(PitchBendCell),
182    /// A MIDI polyphonic key pressure.
183    PolyPressure(PolyPressureCell),
184    /// A MIDI channel pressure.
185    ChannelPressure(ChannelPressureCell),
186    /// A raw MIDI event.
187    Midi(MidiEvent),
188}
189
190impl PianoRollCell {
191    /// Returns the cell's time position on its lane.
192    pub fn time(&self) -> Time {
193        match self {
194            Self::Note(cell) => cell.onset,
195            Self::Drum(cell) => cell.onset,
196            Self::ScaleDegree(cell) => cell.onset,
197            Self::Object(cell) => cell.onset,
198            Self::Automation(cell) => cell.time,
199            Self::ControlChange(cell) => cell.time,
200            Self::PitchBend(cell) => cell.time,
201            Self::PolyPressure(cell) => cell.time,
202            Self::ChannelPressure(cell) => cell.time,
203            Self::Midi(event) => tick_time_to_time(event.time),
204        }
205    }
206
207    /// Returns the lane kind this cell belongs on.
208    pub fn lane_kind(&self) -> LaneKind {
209        match self {
210            Self::Note(_) => LaneKind::Note,
211            Self::Drum(_) => LaneKind::Drum,
212            Self::ScaleDegree(_) => LaneKind::ScaleDegree,
213            Self::Object(_) => LaneKind::Object,
214            Self::Automation(_) => LaneKind::Automation,
215            Self::ControlChange(_)
216            | Self::PitchBend(_)
217            | Self::PolyPressure(_)
218            | Self::ChannelPressure(_) => LaneKind::Control,
219            Self::Midi(_) => LaneKind::Midi,
220        }
221    }
222
223    /// Returns the wire label naming this cell's variant.
224    pub fn kind_label(&self) -> &'static str {
225        match self {
226            Self::Note(_) => "note",
227            Self::Drum(_) => "drum",
228            Self::ScaleDegree(_) => "scale-degree",
229            Self::Object(_) => "object",
230            Self::Automation(_) => "automation",
231            Self::ControlChange(_) => "control-change",
232            Self::PitchBend(_) => "pitch-bend",
233            Self::PolyPressure(_) => "poly-pressure",
234            Self::ChannelPressure(_) => "channel-pressure",
235            Self::Midi(_) => "midi",
236        }
237    }
238
239    /// Renders the cell as its expression form for codecs and browse.
240    pub fn to_expr(&self) -> Expr {
241        match self {
242            Self::Note(cell) => map(vec![
243                ("kind", Expr::String("note".to_owned())),
244                ("onset", time_expr(cell.onset)),
245                ("duration", time_expr(cell.note.duration)),
246                ("pitch", Expr::String(pitch_label(cell.note.pitch))),
247                ("velocity", Expr::String(cell.note.velocity.to_string())),
248                ("channel", Expr::String(cell.note.channel.0.to_string())),
249            ]),
250            Self::Drum(cell) => map(vec![
251                ("kind", Expr::String("drum".to_owned())),
252                ("onset", time_expr(cell.onset)),
253                ("duration", time_expr(cell.duration)),
254                ("key", Expr::String(cell.key.0.to_string())),
255                ("velocity", Expr::String(cell.velocity.0.to_string())),
256                ("channel", Expr::String(cell.channel.0.to_string())),
257            ]),
258            Self::ScaleDegree(cell) => map(vec![
259                ("kind", Expr::String("scale-degree".to_owned())),
260                ("onset", time_expr(cell.onset)),
261                ("duration", time_expr(cell.duration)),
262                ("degree", Expr::String(cell.degree.to_string())),
263                ("octave", Expr::String(cell.octave.to_string())),
264                ("velocity", Expr::String(cell.velocity.0.to_string())),
265                ("channel", Expr::String(cell.channel.0.to_string())),
266            ]),
267            Self::Object(cell) => map(vec![
268                ("kind", Expr::String("object".to_owned())),
269                ("onset", time_expr(cell.onset)),
270                ("duration", time_expr(cell.duration)),
271                ("object", Expr::Symbol(cell.object.clone())),
272            ]),
273            Self::Automation(cell) => map(vec![
274                ("kind", Expr::String("automation".to_owned())),
275                ("time", time_expr(cell.time)),
276                ("target", Expr::Symbol(cell.target.clone())),
277                ("value", Expr::String(cell.value.to_string())),
278            ]),
279            Self::ControlChange(cell) => map(vec![
280                ("kind", Expr::String("control-change".to_owned())),
281                ("time", time_expr(cell.time)),
282                ("channel", Expr::String(cell.channel.0.to_string())),
283                ("controller", Expr::String(cell.controller.0.to_string())),
284                ("value", Expr::String(cell.value.0.to_string())),
285            ]),
286            Self::PitchBend(cell) => map(vec![
287                ("kind", Expr::String("pitch-bend".to_owned())),
288                ("time", time_expr(cell.time)),
289                ("channel", Expr::String(cell.channel.0.to_string())),
290                ("value", Expr::String(cell.value.0.to_string())),
291            ]),
292            Self::PolyPressure(cell) => map(vec![
293                ("kind", Expr::String("poly-pressure".to_owned())),
294                ("time", time_expr(cell.time)),
295                ("channel", Expr::String(cell.channel.0.to_string())),
296                ("key", Expr::String(cell.key.0.to_string())),
297                ("pressure", Expr::String(cell.pressure.0.to_string())),
298            ]),
299            Self::ChannelPressure(cell) => map(vec![
300                ("kind", Expr::String("channel-pressure".to_owned())),
301                ("time", time_expr(cell.time)),
302                ("channel", Expr::String(cell.channel.0.to_string())),
303                ("pressure", Expr::String(cell.pressure.0.to_string())),
304            ]),
305            Self::Midi(event) => map(vec![
306                ("kind", Expr::String("midi".to_owned())),
307                ("time", time_expr(tick_time_to_time(event.time))),
308                ("payload", Expr::String(format!("{:?}", event.payload))),
309            ]),
310        }
311    }
312}
313
314/// A single lane of a piano roll holding cells of one kind.
315#[derive(Clone, Debug, PartialEq, Eq)]
316pub struct PianoRollLane {
317    /// Identifier of the lane.
318    pub id: LaneId,
319    /// Kind of cell the lane carries.
320    pub kind: LaneKind,
321    /// Cells on the lane, kept in stable time order.
322    pub cells: Vec<PianoRollCell>,
323}
324
325impl PianoRollLane {
326    /// Builds a lane, validating cell kinds and times then ordering the cells.
327    ///
328    /// Every cell must match `kind` and carry non-negative timing; otherwise a
329    /// `MusicError` is returned. The cells are sorted into stable order.
330    pub fn new(
331        id: LaneId,
332        kind: LaneKind,
333        mut cells: Vec<PianoRollCell>,
334    ) -> Result<Self, MusicError> {
335        for cell in &cells {
336            if cell.lane_kind() != kind {
337                return Err(MusicError::PianoRollLaneCellMismatch {
338                    lane: id.0.clone(),
339                    lane_kind: kind.wire_label().to_owned(),
340                    cell_kind: cell.kind_label().to_owned(),
341                });
342            }
343            validate_cell_time(cell)?;
344        }
345        stable_cell_order(&mut cells);
346        Ok(Self { id, kind, cells })
347    }
348
349    /// Renders the lane and its cells as an expression.
350    pub fn to_expr(&self) -> Expr {
351        map(vec![
352            ("id", Expr::String(self.id.0.clone())),
353            ("kind", Expr::Symbol(self.kind.symbol())),
354            (
355                "cells",
356                Expr::List(self.cells.iter().map(PianoRollCell::to_expr).collect()),
357            ),
358        ])
359    }
360}
361
362/// A piano roll: a set of timed lanes over a shared timing grid.
363///
364/// Holds the note projection in `items` alongside the full `lanes`, all keyed
365/// to a single [`TimeGrid`].
366#[derive(Clone, Debug, PartialEq, Eq)]
367pub struct PianoRoll {
368    /// Note projection across all lanes, in stable order.
369    pub items: Vec<TimedNote>,
370    /// Lanes making up the roll, sorted by id then kind.
371    pub lanes: Vec<PianoRollLane>,
372    /// Timing grid shared by every lane.
373    pub time: TimeGrid,
374}
375
376impl PianoRoll {
377    /// Builds a roll from notes, placing them on a single note lane.
378    ///
379    /// Uses the default [`TimeGrid`]; an empty input yields a roll with no lanes.
380    pub fn new(items: Vec<TimedNote>) -> Result<Self, MusicError> {
381        let cells = items
382            .into_iter()
383            .map(PianoRollCell::Note)
384            .collect::<Vec<_>>();
385        let lanes = if cells.is_empty() {
386            Vec::new()
387        } else {
388            vec![PianoRollLane::new(
389                LaneId::new("notes"),
390                LaneKind::Note,
391                cells,
392            )?]
393        };
394        Self::from_lanes_with_time(lanes, TimeGrid::default())
395    }
396
397    /// Builds a roll from prepared lanes using the default [`TimeGrid`].
398    pub fn from_lanes(lanes: Vec<PianoRollLane>) -> Result<Self, MusicError> {
399        Self::from_lanes_with_time(lanes, TimeGrid::default())
400    }
401
402    /// Builds a roll from prepared lanes over an explicit timing grid.
403    ///
404    /// Validates `time`, sorts the lanes, and derives the note projection in
405    /// stable order.
406    pub fn from_lanes_with_time(
407        mut lanes: Vec<PianoRollLane>,
408        time: TimeGrid,
409    ) -> Result<Self, MusicError> {
410        TimeGrid::new(time.tpq, time.step)?;
411        lanes.sort_by(|left, right| {
412            left.id
413                .cmp(&right.id)
414                .then_with(|| left.kind.cmp(&right.kind))
415        });
416        let mut items = lanes
417            .iter()
418            .flat_map(|lane| lane.cells.iter())
419            .filter_map(cell_note)
420            .collect::<Vec<_>>();
421        stable_note_order(&mut items);
422        Ok(Self { items, lanes, time })
423    }
424
425    /// Builds a roll from note events on a single performance note lane.
426    ///
427    /// Converts each event's tick timing to grid time with normal articulation.
428    pub fn from_note_events(events: Vec<NoteEvent>) -> Result<Self, MusicError> {
429        let cells = events
430            .into_iter()
431            .map(|event| {
432                PianoRollCell::Note(TimedNote {
433                    onset: tick_time_to_time(event.time),
434                    note: Note {
435                        duration: tick_time_to_time(event.duration),
436                        pitch: event.pitch,
437                        velocity: event.velocity,
438                        channel: event.channel,
439                        articulation: Articulation::Normal,
440                    },
441                })
442            })
443            .collect::<Vec<_>>();
444        Self::from_lanes(vec![PianoRollLane::new(
445            LaneId::new("performance-notes"),
446            LaneKind::Note,
447            cells,
448        )?])
449    }
450
451    /// Builds a roll from a performance take's extracted note events.
452    ///
453    /// Surfaces extraction or validation failures as a kernel evaluation error.
454    pub fn from_performance_take(take: &PerformanceTake) -> KernelResult<Self> {
455        let note_events = take.note_events()?;
456        Self::from_note_events(note_events)
457            .map_err(|err| sim_kernel::Error::Eval(format!("invalid piano-roll take: {err}")))
458    }
459
460    /// Iterates over every cell across all lanes.
461    pub fn cells(&self) -> impl Iterator<Item = &PianoRollCell> {
462        self.lanes.iter().flat_map(|lane| lane.cells.iter())
463    }
464
465    /// Renders the roll, its grid, and its lanes as an expression.
466    pub fn to_expr(&self) -> Expr {
467        map(vec![
468            (
469                "object",
470                Expr::Symbol(Symbol::qualified("music", "PianoRoll")),
471            ),
472            ("tpq", Expr::String(self.time.tpq.to_string())),
473            ("step", time_expr(self.time.step)),
474            (
475                "lanes",
476                Expr::List(self.lanes.iter().map(PianoRollLane::to_expr).collect()),
477            ),
478        ])
479    }
480}
481
482fn stable_note_order(items: &mut [TimedNote]) {
483    items.sort_by(|left, right| {
484        left.onset
485            .cmp(&right.onset)
486            .then_with(|| left.note.pitch.semitone().cmp(&right.note.pitch.semitone()))
487            .then_with(|| left.note.channel.0.cmp(&right.note.channel.0))
488    });
489}
490
491fn stable_cell_order(cells: &mut [PianoRollCell]) {
492    cells.sort_by(|left, right| {
493        left.time()
494            .cmp(&right.time())
495            .then_with(|| left.kind_label().cmp(right.kind_label()))
496    });
497}
498
499fn validate_cell_time(cell: &PianoRollCell) -> Result<(), MusicError> {
500    ensure_non_negative(cell.time())?;
501    match cell {
502        PianoRollCell::Note(cell) => ensure_non_negative(cell.note.duration),
503        PianoRollCell::Drum(cell) => ensure_non_negative(cell.duration),
504        PianoRollCell::ScaleDegree(cell) => ensure_non_negative(cell.duration),
505        PianoRollCell::Object(cell) => ensure_non_negative(cell.duration),
506        PianoRollCell::Automation(_)
507        | PianoRollCell::ControlChange(_)
508        | PianoRollCell::PitchBend(_)
509        | PianoRollCell::PolyPressure(_)
510        | PianoRollCell::ChannelPressure(_)
511        | PianoRollCell::Midi(_) => Ok(()),
512    }
513}
514
515fn cell_note(cell: &PianoRollCell) -> Option<TimedNote> {
516    match cell {
517        PianoRollCell::Note(cell) => Some(cell.clone()),
518        PianoRollCell::Drum(cell) => Some(TimedNote {
519            onset: cell.onset,
520            note: Note {
521                duration: cell.duration,
522                pitch: Pitch::from_midi(cell.key.0),
523                velocity: cell.velocity.0.max(1),
524                channel: cell.channel,
525                articulation: Articulation::Normal,
526            },
527        }),
528        _ => None,
529    }
530}
531
532fn tick_time_to_time(time: sim_lib_midi_core::TickTime) -> Time {
533    Ratio::new(time.ticks, i64::from(time.tpq) * 4)
534}
535
536fn time_expr(time: Time) -> Expr {
537    map(vec![
538        ("numer", Expr::String(time.numer().to_string())),
539        ("denom", Expr::String(time.denom().to_string())),
540    ])
541}
542
543fn pitch_label(pitch: Pitch) -> String {
544    pitch
545        .to_midi()
546        .map(|midi| format!("midi:{midi}"))
547        .unwrap_or_else(|| format!("semitone:{}", pitch.semitone()))
548}
549
550fn map(entries: Vec<(&'static str, Expr)>) -> Expr {
551    Expr::Map(
552        entries
553            .into_iter()
554            .map(|(key, value)| (Expr::Symbol(Symbol::new(key)), value))
555            .collect(),
556    )
557}