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/// Stable identity of one note occurrence inside a piano-roll lane.
62///
63/// Equal pitches remain distinct because identity is the lane plus cell index,
64/// not a pitch or pitch-class mask.
65#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
66pub struct NoteOccurrence {
67    /// Lane containing the note cell.
68    pub lane: LaneId,
69    /// Stable index within that lane's canonical cell order.
70    pub cell_index: usize,
71}
72
73/// One identity-bearing note present in a [`NoteSlice`].
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct SlicedNote {
76    /// Piano-roll occurrence identity.
77    pub occurrence: NoteOccurrence,
78    /// Exact onset and note payload.
79    pub timed: TimedNote,
80}
81
82/// Exact half-open piano-roll window with every sounding note occurrence.
83///
84/// The `notes` vector deliberately preserves duplicate pitches and unisons for
85/// later consonance, orchestration, and voice-aware analysis.
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct NoteSlice {
88    /// Inclusive start of the window.
89    pub at: Time,
90    /// Exclusive end of the window.
91    pub until: Time,
92    /// Notes sounding throughout `[at, until)`, in stable occurrence order.
93    pub notes: Vec<SlicedNote>,
94}
95
96/// A drum hit cell addressed by MIDI key.
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct DrumCell {
99    /// Onset time of the hit.
100    pub onset: Time,
101    /// Sounding duration of the hit.
102    pub duration: Time,
103    /// MIDI key (drum voice) struck.
104    pub key: U7,
105    /// Strike velocity.
106    pub velocity: U7,
107    /// MIDI channel of the hit.
108    pub channel: Channel,
109}
110
111/// A note addressed by scale degree and octave rather than absolute pitch.
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct ScaleDegreeCell {
114    /// Onset time of the cell.
115    pub onset: Time,
116    /// Sounding duration of the cell.
117    pub duration: Time,
118    /// Scale degree, relative to the prevailing scale.
119    pub degree: i16,
120    /// Octave offset applied to the degree.
121    pub octave: i8,
122    /// Strike velocity.
123    pub velocity: U7,
124    /// MIDI channel of the cell.
125    pub channel: Channel,
126}
127
128/// A cell that places a named runtime object on a lane.
129#[derive(Clone, Debug, PartialEq, Eq)]
130pub struct ObjectCell {
131    /// Onset time of the object.
132    pub onset: Time,
133    /// Duration the object occupies.
134    pub duration: Time,
135    /// Symbol naming the placed object.
136    pub object: Symbol,
137}
138
139/// An automation breakpoint targeting a named parameter.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct AutomationCell {
142    /// Time of the breakpoint.
143    pub time: Time,
144    /// Symbol naming the automation target.
145    pub target: Symbol,
146    /// Value applied at `time`.
147    pub value: i64,
148}
149
150/// A MIDI control-change cell.
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct ControlChangeCell {
153    /// Time of the control change.
154    pub time: Time,
155    /// MIDI channel affected.
156    pub channel: Channel,
157    /// Controller number.
158    pub controller: U7,
159    /// New controller value.
160    pub value: U7,
161}
162
163/// A MIDI pitch-bend cell.
164#[derive(Clone, Debug, PartialEq, Eq)]
165pub struct PitchBendCell {
166    /// Time of the bend.
167    pub time: Time,
168    /// MIDI channel affected.
169    pub channel: Channel,
170    /// 14-bit bend value.
171    pub value: U14,
172}
173
174/// A MIDI polyphonic key-pressure cell.
175#[derive(Clone, Debug, PartialEq, Eq)]
176pub struct PolyPressureCell {
177    /// Time of the pressure event.
178    pub time: Time,
179    /// MIDI channel affected.
180    pub channel: Channel,
181    /// Key the pressure applies to.
182    pub key: U7,
183    /// Pressure amount.
184    pub pressure: U7,
185}
186
187/// A MIDI channel-pressure (aftertouch) cell.
188#[derive(Clone, Debug, PartialEq, Eq)]
189pub struct ChannelPressureCell {
190    /// Time of the pressure event.
191    pub time: Time,
192    /// MIDI channel affected.
193    pub channel: Channel,
194    /// Pressure amount applied to the whole channel.
195    pub pressure: U7,
196}
197
198/// A single placed event in a piano roll lane.
199///
200/// Each variant carries the cell payload appropriate to its lane kind.
201#[derive(Clone, Debug, PartialEq, Eq)]
202pub enum PianoRollCell {
203    /// A pitched note.
204    Note(TimedNote),
205    /// A drum hit.
206    Drum(DrumCell),
207    /// A scale-degree note.
208    ScaleDegree(ScaleDegreeCell),
209    /// A placed runtime object.
210    Object(ObjectCell),
211    /// An automation breakpoint.
212    Automation(AutomationCell),
213    /// A MIDI control change.
214    ControlChange(ControlChangeCell),
215    /// A MIDI pitch bend.
216    PitchBend(PitchBendCell),
217    /// A MIDI polyphonic key pressure.
218    PolyPressure(PolyPressureCell),
219    /// A MIDI channel pressure.
220    ChannelPressure(ChannelPressureCell),
221    /// A raw MIDI event.
222    Midi(MidiEvent),
223}
224
225impl PianoRollCell {
226    /// Returns the cell's time position on its lane.
227    pub fn time(&self) -> Time {
228        match self {
229            Self::Note(cell) => cell.onset,
230            Self::Drum(cell) => cell.onset,
231            Self::ScaleDegree(cell) => cell.onset,
232            Self::Object(cell) => cell.onset,
233            Self::Automation(cell) => cell.time,
234            Self::ControlChange(cell) => cell.time,
235            Self::PitchBend(cell) => cell.time,
236            Self::PolyPressure(cell) => cell.time,
237            Self::ChannelPressure(cell) => cell.time,
238            Self::Midi(event) => tick_time_to_time(event.time),
239        }
240    }
241
242    /// Returns the lane kind this cell belongs on.
243    pub fn lane_kind(&self) -> LaneKind {
244        match self {
245            Self::Note(_) => LaneKind::Note,
246            Self::Drum(_) => LaneKind::Drum,
247            Self::ScaleDegree(_) => LaneKind::ScaleDegree,
248            Self::Object(_) => LaneKind::Object,
249            Self::Automation(_) => LaneKind::Automation,
250            Self::ControlChange(_)
251            | Self::PitchBend(_)
252            | Self::PolyPressure(_)
253            | Self::ChannelPressure(_) => LaneKind::Control,
254            Self::Midi(_) => LaneKind::Midi,
255        }
256    }
257
258    /// Returns the wire label naming this cell's variant.
259    pub fn kind_label(&self) -> &'static str {
260        match self {
261            Self::Note(_) => "note",
262            Self::Drum(_) => "drum",
263            Self::ScaleDegree(_) => "scale-degree",
264            Self::Object(_) => "object",
265            Self::Automation(_) => "automation",
266            Self::ControlChange(_) => "control-change",
267            Self::PitchBend(_) => "pitch-bend",
268            Self::PolyPressure(_) => "poly-pressure",
269            Self::ChannelPressure(_) => "channel-pressure",
270            Self::Midi(_) => "midi",
271        }
272    }
273
274    /// Renders the cell as its expression form for codecs and browse.
275    pub fn to_expr(&self) -> Expr {
276        match self {
277            Self::Note(cell) => map(vec![
278                ("kind", Expr::String("note".to_owned())),
279                ("onset", time_expr(cell.onset)),
280                ("duration", time_expr(cell.note.duration)),
281                ("pitch", Expr::String(pitch_label(cell.note.pitch))),
282                ("velocity", Expr::String(cell.note.velocity.to_string())),
283                ("channel", Expr::String(cell.note.channel.0.to_string())),
284            ]),
285            Self::Drum(cell) => map(vec![
286                ("kind", Expr::String("drum".to_owned())),
287                ("onset", time_expr(cell.onset)),
288                ("duration", time_expr(cell.duration)),
289                ("key", Expr::String(cell.key.0.to_string())),
290                ("velocity", Expr::String(cell.velocity.0.to_string())),
291                ("channel", Expr::String(cell.channel.0.to_string())),
292            ]),
293            Self::ScaleDegree(cell) => map(vec![
294                ("kind", Expr::String("scale-degree".to_owned())),
295                ("onset", time_expr(cell.onset)),
296                ("duration", time_expr(cell.duration)),
297                ("degree", Expr::String(cell.degree.to_string())),
298                ("octave", Expr::String(cell.octave.to_string())),
299                ("velocity", Expr::String(cell.velocity.0.to_string())),
300                ("channel", Expr::String(cell.channel.0.to_string())),
301            ]),
302            Self::Object(cell) => map(vec![
303                ("kind", Expr::String("object".to_owned())),
304                ("onset", time_expr(cell.onset)),
305                ("duration", time_expr(cell.duration)),
306                ("object", Expr::Symbol(cell.object.clone())),
307            ]),
308            Self::Automation(cell) => map(vec![
309                ("kind", Expr::String("automation".to_owned())),
310                ("time", time_expr(cell.time)),
311                ("target", Expr::Symbol(cell.target.clone())),
312                ("value", Expr::String(cell.value.to_string())),
313            ]),
314            Self::ControlChange(cell) => map(vec![
315                ("kind", Expr::String("control-change".to_owned())),
316                ("time", time_expr(cell.time)),
317                ("channel", Expr::String(cell.channel.0.to_string())),
318                ("controller", Expr::String(cell.controller.0.to_string())),
319                ("value", Expr::String(cell.value.0.to_string())),
320            ]),
321            Self::PitchBend(cell) => map(vec![
322                ("kind", Expr::String("pitch-bend".to_owned())),
323                ("time", time_expr(cell.time)),
324                ("channel", Expr::String(cell.channel.0.to_string())),
325                ("value", Expr::String(cell.value.0.to_string())),
326            ]),
327            Self::PolyPressure(cell) => map(vec![
328                ("kind", Expr::String("poly-pressure".to_owned())),
329                ("time", time_expr(cell.time)),
330                ("channel", Expr::String(cell.channel.0.to_string())),
331                ("key", Expr::String(cell.key.0.to_string())),
332                ("pressure", Expr::String(cell.pressure.0.to_string())),
333            ]),
334            Self::ChannelPressure(cell) => map(vec![
335                ("kind", Expr::String("channel-pressure".to_owned())),
336                ("time", time_expr(cell.time)),
337                ("channel", Expr::String(cell.channel.0.to_string())),
338                ("pressure", Expr::String(cell.pressure.0.to_string())),
339            ]),
340            Self::Midi(event) => map(vec![
341                ("kind", Expr::String("midi".to_owned())),
342                ("time", time_expr(tick_time_to_time(event.time))),
343                ("payload", Expr::String(format!("{:?}", event.payload))),
344            ]),
345        }
346    }
347}
348
349/// A single lane of a piano roll holding cells of one kind.
350#[derive(Clone, Debug, PartialEq, Eq)]
351pub struct PianoRollLane {
352    /// Identifier of the lane.
353    pub id: LaneId,
354    /// Kind of cell the lane carries.
355    pub kind: LaneKind,
356    /// Cells on the lane, kept in stable time order.
357    pub cells: Vec<PianoRollCell>,
358}
359
360impl PianoRollLane {
361    /// Builds a lane, validating cell kinds and times then ordering the cells.
362    ///
363    /// Every cell must match `kind` and carry non-negative timing; otherwise a
364    /// `MusicError` is returned. The cells are sorted into stable order.
365    pub fn new(
366        id: LaneId,
367        kind: LaneKind,
368        mut cells: Vec<PianoRollCell>,
369    ) -> Result<Self, MusicError> {
370        for cell in &cells {
371            if cell.lane_kind() != kind {
372                return Err(MusicError::PianoRollLaneCellMismatch {
373                    lane: id.0.clone(),
374                    lane_kind: kind.wire_label().to_owned(),
375                    cell_kind: cell.kind_label().to_owned(),
376                });
377            }
378            validate_cell_time(cell)?;
379        }
380        stable_cell_order(&mut cells);
381        Ok(Self { id, kind, cells })
382    }
383
384    /// Renders the lane and its cells as an expression.
385    pub fn to_expr(&self) -> Expr {
386        map(vec![
387            ("id", Expr::String(self.id.0.clone())),
388            ("kind", Expr::Symbol(self.kind.symbol())),
389            (
390                "cells",
391                Expr::List(self.cells.iter().map(PianoRollCell::to_expr).collect()),
392            ),
393        ])
394    }
395}
396
397/// A piano roll: a set of timed lanes over a shared timing grid.
398///
399/// Holds the note projection in `items` alongside the full `lanes`, all keyed
400/// to a single [`TimeGrid`].
401#[derive(Clone, Debug, PartialEq, Eq)]
402pub struct PianoRoll {
403    /// Note projection across all lanes, in stable order.
404    pub items: Vec<TimedNote>,
405    /// Lanes making up the roll, sorted by id then kind.
406    pub lanes: Vec<PianoRollLane>,
407    /// Timing grid shared by every lane.
408    pub time: TimeGrid,
409}
410
411impl PianoRoll {
412    /// Builds a roll from notes, placing them on a single note lane.
413    ///
414    /// Uses the default [`TimeGrid`]; an empty input yields a roll with no lanes.
415    pub fn new(items: Vec<TimedNote>) -> Result<Self, MusicError> {
416        let cells = items
417            .into_iter()
418            .map(PianoRollCell::Note)
419            .collect::<Vec<_>>();
420        let lanes = if cells.is_empty() {
421            Vec::new()
422        } else {
423            vec![PianoRollLane::new(
424                LaneId::new("notes"),
425                LaneKind::Note,
426                cells,
427            )?]
428        };
429        Self::from_lanes_with_time(lanes, TimeGrid::default())
430    }
431
432    /// Builds a roll from prepared lanes using the default [`TimeGrid`].
433    pub fn from_lanes(lanes: Vec<PianoRollLane>) -> Result<Self, MusicError> {
434        Self::from_lanes_with_time(lanes, TimeGrid::default())
435    }
436
437    /// Builds a roll from prepared lanes over an explicit timing grid.
438    ///
439    /// Validates `time`, sorts the lanes, and derives the note projection in
440    /// stable order.
441    pub fn from_lanes_with_time(
442        mut lanes: Vec<PianoRollLane>,
443        time: TimeGrid,
444    ) -> Result<Self, MusicError> {
445        TimeGrid::new(time.tpq, time.step)?;
446        lanes.sort_by(|left, right| {
447            left.id
448                .cmp(&right.id)
449                .then_with(|| left.kind.cmp(&right.kind))
450        });
451        let mut items = lanes
452            .iter()
453            .flat_map(|lane| lane.cells.iter())
454            .filter_map(cell_note)
455            .collect::<Vec<_>>();
456        stable_note_order(&mut items);
457        Ok(Self { items, lanes, time })
458    }
459
460    /// Builds a roll from note events on a single performance note lane.
461    ///
462    /// Converts each event's tick timing to grid time with normal articulation.
463    pub fn from_note_events(events: Vec<NoteEvent>) -> Result<Self, MusicError> {
464        let cells = events
465            .into_iter()
466            .map(|event| {
467                PianoRollCell::Note(TimedNote {
468                    onset: tick_time_to_time(event.time),
469                    note: Note {
470                        duration: tick_time_to_time(event.duration),
471                        pitch: event.pitch,
472                        velocity: event.velocity,
473                        channel: event.channel,
474                        articulation: Articulation::Normal,
475                    },
476                })
477            })
478            .collect::<Vec<_>>();
479        Self::from_lanes(vec![PianoRollLane::new(
480            LaneId::new("performance-notes"),
481            LaneKind::Note,
482            cells,
483        )?])
484    }
485
486    /// Builds a roll from a performance take's extracted note events.
487    ///
488    /// Surfaces extraction or validation failures as a kernel evaluation error.
489    pub fn from_performance_take(take: &PerformanceTake) -> KernelResult<Self> {
490        let note_events = take.note_events()?;
491        Self::from_note_events(note_events)
492            .map_err(|err| sim_kernel::Error::Eval(format!("invalid piano-roll take: {err}")))
493    }
494
495    /// Iterates over every cell across all lanes.
496    pub fn cells(&self) -> impl Iterator<Item = &PianoRollCell> {
497        self.lanes.iter().flat_map(|lane| lane.cells.iter())
498    }
499
500    /// Splits the roll at every note onset and release into exact sounding
501    /// windows.
502    ///
503    /// Silent spans and zero-duration notes do not produce slices. Equal
504    /// pitches in different cells remain separate [`SlicedNote`] entries.
505    pub fn note_slices(&self) -> Vec<NoteSlice> {
506        let occurrences = self
507            .lanes
508            .iter()
509            .flat_map(|lane| {
510                lane.cells
511                    .iter()
512                    .enumerate()
513                    .filter_map(|(cell_index, cell)| {
514                        cell_note(cell).map(|timed| SlicedNote {
515                            occurrence: NoteOccurrence {
516                                lane: lane.id.clone(),
517                                cell_index,
518                            },
519                            timed,
520                        })
521                    })
522            })
523            .collect::<Vec<_>>();
524        let mut boundaries = occurrences
525            .iter()
526            .flat_map(|note| {
527                [
528                    note.timed.onset,
529                    note.timed.onset + note.timed.note.duration,
530                ]
531            })
532            .collect::<Vec<_>>();
533        boundaries.sort();
534        boundaries.dedup();
535        boundaries
536            .windows(2)
537            .filter_map(|pair| {
538                let at = pair[0];
539                let until = pair[1];
540                let notes = occurrences
541                    .iter()
542                    .filter(|note| {
543                        note.timed.onset <= at && at < note.timed.onset + note.timed.note.duration
544                    })
545                    .cloned()
546                    .collect::<Vec<_>>();
547                (!notes.is_empty() && at < until).then_some(NoteSlice { at, until, notes })
548            })
549            .collect()
550    }
551
552    /// Renders the roll, its grid, and its lanes as an expression.
553    pub fn to_expr(&self) -> Expr {
554        map(vec![
555            (
556                "object",
557                Expr::Symbol(Symbol::qualified("music", "PianoRoll")),
558            ),
559            ("tpq", Expr::String(self.time.tpq.to_string())),
560            ("step", time_expr(self.time.step)),
561            (
562                "lanes",
563                Expr::List(self.lanes.iter().map(PianoRollLane::to_expr).collect()),
564            ),
565        ])
566    }
567}
568
569fn stable_note_order(items: &mut [TimedNote]) {
570    items.sort_by(|left, right| {
571        left.onset
572            .cmp(&right.onset)
573            .then_with(|| left.note.pitch.semitone().cmp(&right.note.pitch.semitone()))
574            .then_with(|| left.note.channel.0.cmp(&right.note.channel.0))
575    });
576}
577
578fn stable_cell_order(cells: &mut [PianoRollCell]) {
579    cells.sort_by(|left, right| {
580        left.time()
581            .cmp(&right.time())
582            .then_with(|| left.kind_label().cmp(right.kind_label()))
583    });
584}
585
586fn validate_cell_time(cell: &PianoRollCell) -> Result<(), MusicError> {
587    ensure_non_negative(cell.time())?;
588    match cell {
589        PianoRollCell::Note(cell) => ensure_non_negative(cell.note.duration),
590        PianoRollCell::Drum(cell) => ensure_non_negative(cell.duration),
591        PianoRollCell::ScaleDegree(cell) => ensure_non_negative(cell.duration),
592        PianoRollCell::Object(cell) => ensure_non_negative(cell.duration),
593        PianoRollCell::Automation(_)
594        | PianoRollCell::ControlChange(_)
595        | PianoRollCell::PitchBend(_)
596        | PianoRollCell::PolyPressure(_)
597        | PianoRollCell::ChannelPressure(_)
598        | PianoRollCell::Midi(_) => Ok(()),
599    }
600}
601
602fn cell_note(cell: &PianoRollCell) -> Option<TimedNote> {
603    match cell {
604        PianoRollCell::Note(cell) => Some(cell.clone()),
605        PianoRollCell::Drum(cell) => Some(TimedNote {
606            onset: cell.onset,
607            note: Note {
608                duration: cell.duration,
609                pitch: Pitch::from_midi(cell.key.0),
610                velocity: cell.velocity.0.max(1),
611                channel: cell.channel,
612                articulation: Articulation::Normal,
613            },
614        }),
615        _ => None,
616    }
617}
618
619fn tick_time_to_time(time: sim_lib_midi_core::TickTime) -> Time {
620    Ratio::new(time.ticks, i64::from(time.tpq) * 4)
621}
622
623fn time_expr(time: Time) -> Expr {
624    map(vec![
625        ("numer", Expr::String(time.numer().to_string())),
626        ("denom", Expr::String(time.denom().to_string())),
627    ])
628}
629
630fn pitch_label(pitch: Pitch) -> String {
631    pitch
632        .to_midi()
633        .map(|midi| format!("midi:{midi}"))
634        .unwrap_or_else(|| format!("semitone:{}", pitch.semitone()))
635}
636
637fn map(entries: Vec<(&'static str, Expr)>) -> Expr {
638    Expr::Map(
639        entries
640            .into_iter()
641            .map(|(key, value)| (Expr::Symbol(Symbol::new(key)), value))
642            .collect(),
643    )
644}