Skip to main content

sim_lib_music_serial/
render.rs

1//! Canonical music-core rendering for serial realizations.
2
3use std::collections::BTreeMap;
4
5use sim_lib_music_core::{
6    AmbiguousConversionPolicy, Music, ObjectId, PianoRoll, Score, ScoreForm, ScoreFormKind, Staff,
7    StaffNote, StaffVoice, convert_score,
8};
9
10use crate::{SerialRealization, StrictRealizationError};
11
12/// Score metadata used when wrapping a rendered serial realization.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct SerialRenderOptions {
15    /// Tempo in beats per minute.
16    pub tempo_bpm: u32,
17    /// Time signature numerator and denominator.
18    pub time_signature: (u8, u8),
19    /// Optional key label.
20    pub key: Option<String>,
21}
22
23impl Default for SerialRenderOptions {
24    fn default() -> Self {
25        Self {
26            tempo_bpm: 60,
27            time_signature: (4, 4),
28            key: None,
29        }
30    }
31}
32
33/// Renders one realization to the canonical identity-bearing [`Staff`].
34pub fn render_serial_staff(
35    realization: &SerialRealization,
36) -> Result<Staff, StrictRealizationError> {
37    let mut voices = BTreeMap::<_, StaffVoice>::new();
38    for event in realization.events() {
39        let planned = realization
40            .plan()
41            .event(&event.event_id)
42            .expect("realization events must reference plan events");
43        let voice = voices
44            .entry(planned.voice.clone())
45            .or_insert_with(|| StaffVoice {
46                id: planned.voice.clone(),
47                name: planned.voice.as_str().to_owned(),
48                duration: event.onset + event.duration,
49                notes: Vec::new(),
50            });
51        voice.duration = voice.duration.max(event.onset + event.duration);
52    }
53    for note in realization.notes() {
54        let voice = voices
55            .entry(note.voice.clone())
56            .or_insert_with(|| StaffVoice {
57                id: note.voice.clone(),
58                name: note.voice.as_str().to_owned(),
59                duration: note.onset + note.note.duration,
60                notes: Vec::new(),
61            });
62        voice.duration = voice.duration.max(note.onset + note.note.duration);
63        voice.notes.push(StaffNote {
64            voice_id: note.voice.clone(),
65            note_id: ObjectId::new(format!(
66                "serial-note/{}/{}/{}",
67                note.event_id, note.note_index, note.origin.source_ordinal.ordinal
68            ))
69            .map_err(|error| StrictRealizationError::MusicCore(error.to_string()))?,
70            event_id: ObjectId::new(format!(
71                "serial-event/{}/{}",
72                note.event_id, note.note_index
73            ))
74            .map_err(|error| StrictRealizationError::MusicCore(error.to_string()))?,
75            onset: note.onset,
76            note: note.note.clone(),
77        });
78    }
79    Staff::new(voices.into_values().collect())
80        .map_err(|error| StrictRealizationError::MusicCore(error.to_string()))
81}
82
83/// Renders one realization to the canonical [`PianoRoll`] through [`Staff`].
84pub fn render_serial_piano_roll(
85    realization: &SerialRealization,
86) -> Result<PianoRoll, StrictRealizationError> {
87    let staff = render_serial_staff(realization)?;
88    let report = convert_score(
89        &ScoreForm::Staff(staff),
90        ScoreFormKind::PianoRoll,
91        AmbiguousConversionPolicy::Reject,
92    )
93    .map_err(|error| StrictRealizationError::MusicCore(error.to_string()))?;
94    let ScoreForm::PianoRoll(roll) = report.value else {
95        unreachable!("piano-roll conversion must return a piano roll");
96    };
97    Ok(roll)
98}
99
100/// Wraps the canonical piano roll in a music-core [`Score`].
101pub fn render_serial_score(
102    realization: &SerialRealization,
103    options: &SerialRenderOptions,
104) -> Result<Score, StrictRealizationError> {
105    Score::new(
106        options.tempo_bpm,
107        options.time_signature,
108        options.key.clone(),
109        Music::PianoRoll(render_serial_piano_roll(realization)?),
110    )
111    .map_err(|error| StrictRealizationError::MusicCore(error.to_string()))
112}