use std::collections::BTreeMap;
use sim_lib_music_core::{Articulation, Channel, Time};
use sim_lib_pitch_serial::RowForm;
use thiserror::Error;
use crate::{
EventPlacement, PlannedSerialEvent, RowInstanceId, SerialEventId, SerialOrigin, SerialPlan,
SerialRole, StrictEventSpec, StructuralLicense, VoiceId,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CanonSymmetryRequirement {
None,
RetrogradeAnswer,
PalindromicVoiceOffsets,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CanonOrchestration {
pub channel: Channel,
pub articulation: Articulation,
pub timbre: Option<String>,
pub orchestration: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CanonVoiceSpec {
pub row_id: RowInstanceId,
pub form: RowForm,
pub voice: VoiceId,
pub voice_offset: Time,
pub register: i8,
pub duration: Time,
pub orchestration: CanonOrchestration,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CanonSpec {
pub event_prefix: String,
pub onset: Time,
pub rationale: String,
pub license: StructuralLicense,
pub requirement: CanonSymmetryRequirement,
pub voices: Vec<CanonVoiceSpec>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CanonRealizationEvent {
pub event_id: SerialEventId,
pub onset: Time,
pub spec: StrictEventSpec,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CanonVoiceProfile {
pub voice: VoiceId,
pub form: RowForm,
pub voice_offset: Time,
pub orchestration: CanonOrchestration,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CanonSymmetryCertificate {
pub requirement: CanonSymmetryRequirement,
pub satisfied: bool,
pub explanation: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CanonDeployment {
pub plan: SerialPlan,
pub realization: Vec<CanonRealizationEvent>,
pub voices: Vec<CanonVoiceProfile>,
pub symmetry: CanonSymmetryCertificate,
}
#[derive(Clone, Debug, PartialEq, Eq, Error)]
pub enum CanonError {
#[error("canon requires at least one voice")]
EmptyVoices,
#[error("canon voice {0} must use a strictly positive duration")]
NonPositiveDuration(VoiceId),
#[error("{0}")]
Symmetry(String),
#[error("canon plan failed: {0}")]
Plan(String),
}
pub fn build_canon(spec: CanonSpec) -> Result<CanonDeployment, CanonError> {
if spec.voices.is_empty() {
return Err(CanonError::EmptyVoices);
}
for voice in &spec.voices {
if voice.duration <= Time::from_integer(0) {
return Err(CanonError::NonPositiveDuration(voice.voice.clone()));
}
}
let symmetry = validate_symmetry(&spec)?;
if !symmetry.satisfied {
return Err(CanonError::Symmetry(symmetry.explanation.clone()));
}
let mut rows = BTreeMap::new();
let mut events = BTreeMap::new();
let mut precedence = Vec::new();
let mut realization = Vec::new();
let mut voices = Vec::with_capacity(spec.voices.len());
for voice_spec in &spec.voices {
rows.insert(voice_spec.row_id.clone(), voice_spec.form.clone());
voices.push(CanonVoiceProfile {
voice: voice_spec.voice.clone(),
form: voice_spec.form.clone(),
voice_offset: voice_spec.voice_offset,
orchestration: voice_spec.orchestration.clone(),
});
let mut previous = None::<SerialEventId>;
for ordinal in 0..12usize {
let event_id = SerialEventId::new(format!("{}/{}", spec.event_prefix, events.len()))
.map_err(|error| CanonError::Plan(error.to_string()))?;
let event = PlannedSerialEvent {
id: event_id.clone(),
ordinals: vec![crate::OrdinalRef::new(voice_spec.row_id.clone(), ordinal)],
role: SerialRole::Structural,
origin: SerialOrigin::Structural {
rationale: spec.rationale.clone(),
},
voice: voice_spec.voice.clone(),
placement: EventPlacement::independent(),
parents: Vec::new(),
licenses: vec![spec.license.clone()],
};
events.insert(event_id.clone(), event);
if let Some(previous_id) = previous.as_ref() {
precedence.push((previous_id.clone(), event_id.clone()));
}
previous = Some(event_id.clone());
realization.push(CanonRealizationEvent {
event_id,
onset: spec.onset
+ voice_spec.voice_offset
+ (voice_spec.duration * i64::try_from(ordinal).expect("ordinal fits i64")),
spec: StrictEventSpec::notes(
voice_spec.register,
voice_spec.duration,
88,
voice_spec.orchestration.channel,
voice_spec.orchestration.articulation,
),
});
}
}
let plan = SerialPlan::try_new(rows, events, precedence)
.map_err(|error| CanonError::Plan(error.to_string()))?;
Ok(CanonDeployment {
plan,
realization,
voices,
symmetry,
})
}
fn validate_symmetry(spec: &CanonSpec) -> Result<CanonSymmetryCertificate, CanonError> {
let certificate = match spec.requirement {
CanonSymmetryRequirement::None => CanonSymmetryCertificate {
requirement: CanonSymmetryRequirement::None,
satisfied: true,
explanation: "no symmetry requirement requested".to_owned(),
},
CanonSymmetryRequirement::RetrogradeAnswer => {
let Some(subject) = spec.voices.first() else {
return Err(CanonError::EmptyVoices);
};
let satisfied = spec.voices.iter().skip(1).all(|voice| {
voice.form.classes().iter().copied().eq(subject
.form
.classes()
.iter()
.rev()
.copied())
});
CanonSymmetryCertificate {
requirement: CanonSymmetryRequirement::RetrogradeAnswer,
satisfied,
explanation: if satisfied {
"every answer voice preserves the first voice as an exact retrograde".to_owned()
} else {
"retrograde-answer requirement failed: at least one answer voice is not the subject retrograde".to_owned()
},
}
}
CanonSymmetryRequirement::PalindromicVoiceOffsets => {
let offsets = spec
.voices
.iter()
.map(|voice| voice.voice_offset)
.collect::<Vec<_>>();
let satisfied = offsets.iter().eq(offsets.iter().rev());
CanonSymmetryCertificate {
requirement: CanonSymmetryRequirement::PalindromicVoiceOffsets,
satisfied,
explanation: if satisfied {
"voice offsets form a palindrome".to_owned()
} else {
"palindromic-voice-offset requirement failed".to_owned()
},
}
}
};
Ok(certificate)
}