sim_lib_music_serial/
render.rs1use 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#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct SerialRenderOptions {
15 pub tempo_bpm: u32,
17 pub time_signature: (u8, u8),
19 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
33pub 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
83pub 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
100pub 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}