1use std::collections::BTreeMap;
4
5use sim_lib_music_core::{
6 AmbiguousConversionPolicy, Music, ObjectId, Score, ScoreForm, ScoreFormKind, Staff, StaffNote,
7 StaffVoice, Time, convert_score,
8};
9use sim_lib_music_serial::{
10 RealizedSerialOrigin, SerialEventId, SerialRealization, SerialRenderOptions, VoiceId,
11 render_serial_staff,
12};
13use sim_lib_pitch_core::PitchClass;
14use sim_lib_pitch_serial::RowOperation;
15
16use crate::{RetrogradeMode, TransformError};
17
18#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum SerialProvenanceStatus {
21 Preserved,
23 Invalidated {
25 reason: &'static str,
27 },
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct SerialNoteEvidence {
33 pub note_id: ObjectId,
35 pub event_id: SerialEventId,
37 pub origin: RealizedSerialOrigin,
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct SerialTransformProvenance {
44 pub notes: Vec<SerialNoteEvidence>,
46 pub ordinal_order: SerialProvenanceStatus,
48 pub row_forms: SerialProvenanceStatus,
50 pub voices: SerialProvenanceStatus,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct SerialStaffTransform {
57 pub staff: Staff,
59 pub provenance: SerialTransformProvenance,
61}
62
63pub fn apply_serial_row_operation(
65 realization: &SerialRealization,
66 operation: RowOperation,
67) -> Result<SerialStaffTransform, TransformError> {
68 let mut transformed = map_serial_staff(realization, |note| {
69 let mut next = note.clone();
70 let class = next.note.pitch.class;
71 let class = if matches!(
72 operation.family,
73 sim_lib_pitch_serial::RowFamily::I | sim_lib_pitch_serial::RowFamily::RI
74 ) {
75 class.invert(PitchClass::C)
76 } else {
77 class
78 };
79 next.note.pitch.class = class.transpose(i32::from(operation.addend));
80 next
81 })?;
82 if matches!(
83 operation.family,
84 sim_lib_pitch_serial::RowFamily::R | sim_lib_pitch_serial::RowFamily::RI
85 ) {
86 transformed = retrograde_staff(
87 transformed,
88 RetrogradeMode::Cutout,
89 SerialProvenanceStatus::Invalidated {
90 reason: "retrograde reverses serial chronology",
91 },
92 SerialProvenanceStatus::Preserved,
93 )?;
94 }
95 Ok(transformed)
96}
97
98pub fn transpose_serial(
100 realization: &SerialRealization,
101 semitones: i32,
102) -> Result<SerialStaffTransform, TransformError> {
103 map_serial_staff(realization, |note| {
104 let mut next = note.clone();
105 next.note.pitch = next.note.pitch.transpose(semitones);
106 next
107 })
108}
109
110pub fn invert_serial(
112 realization: &SerialRealization,
113 axis: PitchClass,
114) -> Result<SerialStaffTransform, TransformError> {
115 let mut transformed = map_serial_staff(realization, |note| {
116 let mut next = note.clone();
117 next.note.pitch.class = next.note.pitch.class.invert(axis);
118 next
119 })?;
120 transformed.provenance.row_forms = SerialProvenanceStatus::Invalidated {
121 reason: "axis inversion is not the original row-form witness",
122 };
123 Ok(transformed)
124}
125
126pub fn retrograde_serial(
128 realization: &SerialRealization,
129 mode: RetrogradeMode,
130) -> Result<SerialStaffTransform, TransformError> {
131 retrograde_staff(
132 base_transform(realization)?,
133 mode,
134 SerialProvenanceStatus::Invalidated {
135 reason: "retrograde reverses serial chronology",
136 },
137 SerialProvenanceStatus::Invalidated {
138 reason: "retrograde no longer states the original row order",
139 },
140 )
141}
142
143pub fn scale_serial_time(
145 realization: &SerialRealization,
146 factor: Time,
147) -> Result<SerialStaffTransform, TransformError> {
148 if factor <= Time::from_integer(0) {
149 return Err(TransformError::InvalidFactor);
150 }
151 map_serial_staff(realization, |note| {
152 let mut next = note.clone();
153 next.onset *= factor;
154 next.note.duration *= factor;
155 next
156 })
157}
158
159pub fn quantize_serial(
161 realization: &SerialRealization,
162 grid: Time,
163) -> Result<SerialStaffTransform, TransformError> {
164 if grid <= Time::from_integer(0) {
165 return Err(TransformError::InvalidFactor);
166 }
167 let transformed = base_transform(realization)?;
168 for note in transformed.staff.notes() {
169 if !is_exact_multiple(note.onset, grid) || !is_exact_multiple(note.note.duration, grid) {
170 return Err(TransformError::InvalidTransformOutput {
171 transform: "serial-quantize",
172 reason: "quantize would alter exact serial timing",
173 });
174 }
175 }
176 Ok(transformed)
177}
178
179pub fn remap_serial_voices(
181 realization: &SerialRealization,
182 mapping: &BTreeMap<VoiceId, VoiceId>,
183) -> Result<SerialStaffTransform, TransformError> {
184 let source = serial_staff(realization)?;
185 let voices = source
186 .voices
187 .iter()
188 .map(|voice| {
189 let target_id = mapping
190 .get(&voice.id)
191 .cloned()
192 .unwrap_or_else(|| voice.id.clone());
193 let notes = voice
194 .notes
195 .iter()
196 .map(|note| {
197 let mut next = note.clone();
198 next.voice_id = target_id.clone();
199 next
200 })
201 .collect::<Vec<_>>();
202 StaffVoice {
203 id: target_id.clone(),
204 name: target_id.as_str().to_owned(),
205 duration: notes
206 .iter()
207 .map(StaffNote::end)
208 .max()
209 .unwrap_or(voice.duration),
210 notes,
211 }
212 })
213 .collect::<Vec<_>>();
214 let staff = Staff::new(voices)?;
215 Ok(SerialStaffTransform {
216 staff,
217 provenance: SerialTransformProvenance {
218 notes: note_evidence(realization),
219 ordinal_order: SerialProvenanceStatus::Preserved,
220 row_forms: SerialProvenanceStatus::Preserved,
221 voices: SerialProvenanceStatus::Invalidated {
222 reason: "voice remap changes declared voice identity",
223 },
224 },
225 })
226}
227
228pub fn render_serial_notation_score(
230 realization: &SerialRealization,
231 options: &SerialRenderOptions,
232) -> Result<Score, TransformError> {
233 notation_score(realization, options)
234}
235
236fn base_transform(realization: &SerialRealization) -> Result<SerialStaffTransform, TransformError> {
237 Ok(SerialStaffTransform {
238 staff: serial_staff(realization)?,
239 provenance: SerialTransformProvenance {
240 notes: note_evidence(realization),
241 ordinal_order: SerialProvenanceStatus::Preserved,
242 row_forms: SerialProvenanceStatus::Preserved,
243 voices: SerialProvenanceStatus::Preserved,
244 },
245 })
246}
247
248fn map_serial_staff(
249 realization: &SerialRealization,
250 mut map: impl FnMut(&StaffNote) -> StaffNote,
251) -> Result<SerialStaffTransform, TransformError> {
252 let source = serial_staff(realization)?;
253 let voices = source
254 .voices
255 .iter()
256 .map(|voice| {
257 let notes = voice.notes.iter().map(&mut map).collect::<Vec<_>>();
258 StaffVoice {
259 id: voice.id.clone(),
260 name: voice.name.clone(),
261 duration: notes
262 .iter()
263 .map(StaffNote::end)
264 .max()
265 .unwrap_or(voice.duration),
266 notes,
267 }
268 })
269 .collect::<Vec<_>>();
270 Ok(SerialStaffTransform {
271 staff: Staff::new(voices)?,
272 provenance: SerialTransformProvenance {
273 notes: note_evidence(realization),
274 ordinal_order: SerialProvenanceStatus::Preserved,
275 row_forms: SerialProvenanceStatus::Preserved,
276 voices: SerialProvenanceStatus::Preserved,
277 },
278 })
279}
280
281fn retrograde_staff(
282 source: SerialStaffTransform,
283 mode: RetrogradeMode,
284 ordinal_order: SerialProvenanceStatus,
285 row_forms: SerialProvenanceStatus,
286) -> Result<SerialStaffTransform, TransformError> {
287 let total = source.staff.duration();
288 let voices = source
289 .staff
290 .voices
291 .iter()
292 .map(|voice| {
293 let mut notes = voice.notes.clone();
294 match mode {
295 RetrogradeMode::Cutout => {
296 for note in &mut notes {
297 note.onset = total - note.onset - note.note.duration;
298 }
299 }
300 RetrogradeMode::PinnedNoteOn => {
301 let mut onsets = notes.iter().map(|note| note.onset).collect::<Vec<_>>();
302 onsets.sort();
303 let payloads = notes
304 .iter()
305 .rev()
306 .map(|note| note.note.clone())
307 .collect::<Vec<_>>();
308 for (index, note) in notes.iter_mut().enumerate() {
309 note.onset = onsets[index];
310 note.note = payloads[index].clone();
311 }
312 }
313 }
314 StaffVoice {
315 id: voice.id.clone(),
316 name: voice.name.clone(),
317 duration: total,
318 notes,
319 }
320 })
321 .collect::<Vec<_>>();
322 Ok(SerialStaffTransform {
323 staff: Staff::new(voices)?,
324 provenance: SerialTransformProvenance {
325 notes: source.provenance.notes,
326 ordinal_order,
327 row_forms,
328 voices: source.provenance.voices,
329 },
330 })
331}
332
333fn notation_score(
334 realization: &SerialRealization,
335 options: &SerialRenderOptions,
336) -> Result<Score, TransformError> {
337 let staff = serial_staff(realization)?;
338 let report = convert_score(
339 &ScoreForm::Staff(staff),
340 ScoreFormKind::Counterpoint,
341 AmbiguousConversionPolicy::Reject,
342 )?;
343 let ScoreForm::Counterpoint(counterpoint) = report.value else {
344 unreachable!("counterpoint conversion returns counterpoint");
345 };
346 Score::new(
347 options.tempo_bpm,
348 options.time_signature,
349 options.key.clone(),
350 Music::Counterpoint(counterpoint),
351 )
352 .map_err(TransformError::from)
353}
354
355fn note_evidence(realization: &SerialRealization) -> Vec<SerialNoteEvidence> {
356 realization
357 .notes()
358 .iter()
359 .map(|note| SerialNoteEvidence {
360 note_id: ObjectId::new(format!(
361 "serial-note/{}/{}/{}",
362 note.event_id, note.note_index, note.origin.source_ordinal.ordinal
363 ))
364 .expect("rendered serial note id"),
365 event_id: note.event_id.clone(),
366 origin: note.origin.clone(),
367 })
368 .collect()
369}
370
371fn is_exact_multiple(value: Time, step: Time) -> bool {
372 let ratio = value / step;
373 *ratio.denom() == 1
374}
375
376fn serial_staff(realization: &SerialRealization) -> Result<Staff, TransformError> {
377 render_serial_staff(realization).map_err(|_| TransformError::InvalidTransformOutput {
378 transform: "serial-staff",
379 reason: "serial realization could not render to staff",
380 })
381}