1use std::collections::BTreeMap;
4
5use sim_lib_music_core::{Articulation, Channel, Time};
6use sim_lib_pitch_serial::RowForm;
7use thiserror::Error;
8
9use crate::{
10 EventPlacement, PlannedSerialEvent, RowInstanceId, SerialEventId, SerialOrigin, SerialPlan,
11 SerialRole, StrictEventSpec, StructuralLicense, VoiceId,
12};
13
14#[derive(Copy, Clone, Debug, PartialEq, Eq)]
16pub enum CanonSymmetryRequirement {
17 None,
19 RetrogradeAnswer,
21 PalindromicVoiceOffsets,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct CanonOrchestration {
28 pub channel: Channel,
30 pub articulation: Articulation,
32 pub timbre: Option<String>,
34 pub orchestration: Option<String>,
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct CanonVoiceSpec {
41 pub row_id: RowInstanceId,
43 pub form: RowForm,
45 pub voice: VoiceId,
47 pub voice_offset: Time,
49 pub register: i8,
51 pub duration: Time,
53 pub orchestration: CanonOrchestration,
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct CanonSpec {
60 pub event_prefix: String,
62 pub onset: Time,
64 pub rationale: String,
66 pub license: StructuralLicense,
68 pub requirement: CanonSymmetryRequirement,
70 pub voices: Vec<CanonVoiceSpec>,
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct CanonRealizationEvent {
77 pub event_id: SerialEventId,
79 pub onset: Time,
81 pub spec: StrictEventSpec,
83}
84
85#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct CanonVoiceProfile {
88 pub voice: VoiceId,
90 pub form: RowForm,
92 pub voice_offset: Time,
94 pub orchestration: CanonOrchestration,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct CanonSymmetryCertificate {
101 pub requirement: CanonSymmetryRequirement,
103 pub satisfied: bool,
105 pub explanation: String,
107}
108
109#[derive(Clone, Debug, PartialEq, Eq)]
111pub struct CanonDeployment {
112 pub plan: SerialPlan,
114 pub realization: Vec<CanonRealizationEvent>,
116 pub voices: Vec<CanonVoiceProfile>,
118 pub symmetry: CanonSymmetryCertificate,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq, Error)]
124pub enum CanonError {
125 #[error("canon requires at least one voice")]
127 EmptyVoices,
128 #[error("canon voice {0} must use a strictly positive duration")]
130 NonPositiveDuration(VoiceId),
131 #[error("{0}")]
133 Symmetry(String),
134 #[error("canon plan failed: {0}")]
136 Plan(String),
137}
138
139pub fn build_canon(spec: CanonSpec) -> Result<CanonDeployment, CanonError> {
141 if spec.voices.is_empty() {
142 return Err(CanonError::EmptyVoices);
143 }
144 for voice in &spec.voices {
145 if voice.duration <= Time::from_integer(0) {
146 return Err(CanonError::NonPositiveDuration(voice.voice.clone()));
147 }
148 }
149
150 let symmetry = validate_symmetry(&spec)?;
151 if !symmetry.satisfied {
152 return Err(CanonError::Symmetry(symmetry.explanation.clone()));
153 }
154
155 let mut rows = BTreeMap::new();
156 let mut events = BTreeMap::new();
157 let mut precedence = Vec::new();
158 let mut realization = Vec::new();
159 let mut voices = Vec::with_capacity(spec.voices.len());
160
161 for voice_spec in &spec.voices {
162 rows.insert(voice_spec.row_id.clone(), voice_spec.form.clone());
163 voices.push(CanonVoiceProfile {
164 voice: voice_spec.voice.clone(),
165 form: voice_spec.form.clone(),
166 voice_offset: voice_spec.voice_offset,
167 orchestration: voice_spec.orchestration.clone(),
168 });
169 let mut previous = None::<SerialEventId>;
170 for ordinal in 0..12usize {
171 let event_id = SerialEventId::new(format!("{}/{}", spec.event_prefix, events.len()))
172 .map_err(|error| CanonError::Plan(error.to_string()))?;
173 let event = PlannedSerialEvent {
174 id: event_id.clone(),
175 ordinals: vec![crate::OrdinalRef::new(voice_spec.row_id.clone(), ordinal)],
176 role: SerialRole::Structural,
177 origin: SerialOrigin::Structural {
178 rationale: spec.rationale.clone(),
179 },
180 voice: voice_spec.voice.clone(),
181 placement: EventPlacement::independent(),
182 parents: Vec::new(),
183 licenses: vec![spec.license.clone()],
184 };
185 events.insert(event_id.clone(), event);
186 if let Some(previous_id) = previous.as_ref() {
187 precedence.push((previous_id.clone(), event_id.clone()));
188 }
189 previous = Some(event_id.clone());
190 realization.push(CanonRealizationEvent {
191 event_id,
192 onset: spec.onset
193 + voice_spec.voice_offset
194 + (voice_spec.duration * i64::try_from(ordinal).expect("ordinal fits i64")),
195 spec: StrictEventSpec::notes(
196 voice_spec.register,
197 voice_spec.duration,
198 88,
199 voice_spec.orchestration.channel,
200 voice_spec.orchestration.articulation,
201 ),
202 });
203 }
204 }
205
206 let plan = SerialPlan::try_new(rows, events, precedence)
207 .map_err(|error| CanonError::Plan(error.to_string()))?;
208 Ok(CanonDeployment {
209 plan,
210 realization,
211 voices,
212 symmetry,
213 })
214}
215
216fn validate_symmetry(spec: &CanonSpec) -> Result<CanonSymmetryCertificate, CanonError> {
217 let certificate = match spec.requirement {
218 CanonSymmetryRequirement::None => CanonSymmetryCertificate {
219 requirement: CanonSymmetryRequirement::None,
220 satisfied: true,
221 explanation: "no symmetry requirement requested".to_owned(),
222 },
223 CanonSymmetryRequirement::RetrogradeAnswer => {
224 let Some(subject) = spec.voices.first() else {
225 return Err(CanonError::EmptyVoices);
226 };
227 let satisfied = spec.voices.iter().skip(1).all(|voice| {
228 voice.form.classes().iter().copied().eq(subject
229 .form
230 .classes()
231 .iter()
232 .rev()
233 .copied())
234 });
235 CanonSymmetryCertificate {
236 requirement: CanonSymmetryRequirement::RetrogradeAnswer,
237 satisfied,
238 explanation: if satisfied {
239 "every answer voice preserves the first voice as an exact retrograde".to_owned()
240 } else {
241 "retrograde-answer requirement failed: at least one answer voice is not the subject retrograde".to_owned()
242 },
243 }
244 }
245 CanonSymmetryRequirement::PalindromicVoiceOffsets => {
246 let offsets = spec
247 .voices
248 .iter()
249 .map(|voice| voice.voice_offset)
250 .collect::<Vec<_>>();
251 let satisfied = offsets.iter().eq(offsets.iter().rev());
252 CanonSymmetryCertificate {
253 requirement: CanonSymmetryRequirement::PalindromicVoiceOffsets,
254 satisfied,
255 explanation: if satisfied {
256 "voice offsets form a palindrome".to_owned()
257 } else {
258 "palindromic-voice-offset requirement failed".to_owned()
259 },
260 }
261 }
262 };
263 Ok(certificate)
264}