use sim_lib_discrete_graph::Graph;
use sim_lib_discrete_search::{SearchReceipt, SearchStatus};
use sim_lib_music_consonance::{ConsonancePatch, PatchError};
use sim_lib_music_core::{
ConversionError, Counterpoint, Melody, MusicError, ObjectId, Pitch, Staff, Time,
};
use thiserror::Error;
use crate::RuleSet;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct TimeSpan {
pub start: Time,
pub end: Time,
}
impl TimeSpan {
pub fn new(start: Time, end: Time) -> Self {
Self { start, end }
}
pub fn duration(&self) -> Time {
self.end - self.start
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct VoiceEvidence {
pub index: usize,
pub id: ObjectId,
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NoteEvidence {
pub voice: VoiceEvidence,
pub index: usize,
pub note_id: ObjectId,
pub event_id: ObjectId,
pub span: TimeSpan,
pub pitch: Pitch,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AlignmentWindow {
pub span: TimeSpan,
pub notes: Vec<NoteEvidence>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum MotionDirection {
Down,
Static,
Up,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Motion {
pub voices: [VoiceEvidence; 2],
pub notes: [NoteEvidence; 4],
pub span: TimeSpan,
pub first: MotionDirection,
pub second: MotionDirection,
pub interval_before: i32,
pub interval_after: i32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MetricEvidence {
pub metric: String,
pub observed: String,
pub expected: String,
pub unit: String,
pub facts: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Violation {
pub rule: String,
pub message: String,
pub voices: Vec<VoiceEvidence>,
pub notes: Vec<NoteEvidence>,
pub span: TimeSpan,
pub metric: MetricEvidence,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AnalysisProvenance {
pub mode: String,
pub rule_set: String,
pub facts: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CounterpointReport {
pub alignment: Vec<AlignmentWindow>,
pub motions: Vec<Motion>,
pub violations: Vec<Violation>,
pub provenance: AnalysisProvenance,
}
impl CounterpointReport {
pub fn is_legal(&self) -> bool {
self.violations.is_empty()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CadencePolicy {
Open,
PerfectFinal,
PerfectEndpoints,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiversityPolicy {
pub minimum_pitch_changes: usize,
}
impl Default for DiversityPolicy {
fn default() -> Self {
Self {
minimum_pitch_changes: 1,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CounterpointGenerationPolicy {
pub voices: usize,
pub cadence: CadencePolicy,
pub diversity: DiversityPolicy,
pub velocity: u8,
}
impl Default for CounterpointGenerationPolicy {
fn default() -> Self {
Self {
voices: 1,
cadence: CadencePolicy::PerfectEndpoints,
diversity: DiversityPolicy::default(),
velocity: 96,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CounterpointVariable {
pub index: usize,
pub voice: usize,
pub slot: usize,
pub onset: Time,
pub duration: Time,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CounterpointDomain {
pub variable: CounterpointVariable,
pub pitches: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CounterpointCsp {
pub variables: Vec<CounterpointVariable>,
pub domains: Vec<CounterpointDomain>,
pub rhythm: Time,
pub rule_set: String,
pub facts: Vec<String>,
}
impl CounterpointCsp {
pub fn slots(&self) -> usize {
self.variables
.iter()
.map(|variable| variable.slot)
.max()
.map_or(0, |slot| slot + 1)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CounterpointGenerationResult {
pub counterpoint: Counterpoint,
pub completed: Staff,
pub patch: ConsonancePatch,
pub analysis: CounterpointReport,
pub score: i64,
pub fingerprint: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CounterpointGenerationReceipt {
pub search: SearchReceipt,
pub raw_result_count: usize,
pub selected_result_count: usize,
pub diversity_rejected: usize,
pub facts: Vec<String>,
}
impl CounterpointGenerationReceipt {
pub fn status(&self) -> &SearchStatus {
&self.search.status
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CounterpointGeneration {
pub csp: CounterpointCsp,
pub results: Vec<CounterpointGenerationResult>,
pub receipt: CounterpointGenerationReceipt,
}
#[derive(Debug, Error)]
pub enum GenerationError {
#[error("invalid counterpoint generation policy: {0}")]
InvalidPolicy(String),
#[error(transparent)]
Rules(#[from] crate::RuleError),
#[error(transparent)]
Music(#[from] MusicError),
#[error(transparent)]
Conversion(#[from] ConversionError),
#[error(transparent)]
Patch(#[from] PatchError),
#[error("counterpoint generation invariant failed: {0}")]
Invariant(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrettoFusion {
pub counterpoint: Counterpoint,
pub entry_ids: Vec<usize>,
pub mode: String,
pub provenance: Vec<String>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ContrapuntalForm {
Original,
Retrograde,
Inversion {
axis: Pitch,
},
RetrogradeInversion {
axis: Pitch,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrettoTransform {
pub form: ContrapuntalForm,
pub transposition: i32,
pub duration_factor: Time,
}
impl StrettoTransform {
pub fn original(transposition: i32) -> Self {
Self {
form: ContrapuntalForm::Original,
transposition,
duration_factor: Time::from_integer(1),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrettoPolicy {
pub delays: Vec<Time>,
pub transforms: Vec<StrettoTransform>,
pub minimum_overlap: Time,
pub compatibility_rules: RuleSet,
pub max_entries: usize,
pub minimum_cluster_voices: usize,
pub max_clusters: usize,
pub max_chain_length: usize,
}
impl Default for StrettoPolicy {
fn default() -> Self {
let mut compatibility_rules = RuleSet::open();
compatibility_rules.id = "stretto-default".to_owned();
compatibility_rules.intervals.consonant_harmonic_classes = vec![0, 3, 4, 5];
Self {
delays: vec![Time::new(1, 4), Time::new(1, 2), Time::new(3, 4)],
transforms: (0..12).map(StrettoTransform::original).collect(),
minimum_overlap: Time::new(1, 4),
compatibility_rules,
max_entries: 64,
minimum_cluster_voices: 3,
max_clusters: 128,
max_chain_length: 8,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrettoEntry {
pub id: usize,
pub delay: Time,
pub transform: StrettoTransform,
pub melody: Melody,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OverlapEvidence {
pub span: TimeSpan,
pub simultaneous_windows: usize,
pub interval_classes: Vec<(u8, usize)>,
pub facts: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrettoCompatibility {
pub overlap: OverlapEvidence,
pub violation_count: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrettoCouple {
pub leader: usize,
pub follower: usize,
pub compatibility: StrettoCompatibility,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrettoRejection {
pub first: usize,
pub second: usize,
pub overlap: OverlapEvidence,
pub violations: Vec<Violation>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrettoCluster {
pub entries: Vec<usize>,
pub edge_ids: Vec<usize>,
pub fusion: StrettoFusion,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrettoChain {
pub clusters: Vec<usize>,
pub overlaps: Vec<usize>,
pub fused_entries: Vec<usize>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct StrettoGraph {
pub compatibility: Graph<StrettoEntry, StrettoCompatibility>,
pub couples: Vec<StrettoCouple>,
pub rejections: Vec<StrettoRejection>,
pub components: Vec<Vec<usize>>,
pub clusters: Vec<StrettoCluster>,
pub chain_graph: Graph<usize, usize>,
pub chains: Vec<StrettoChain>,
pub provenance: Vec<String>,
}
#[derive(Debug, Error)]
pub enum StrettoError {
#[error("invalid stretto policy: {0}")]
InvalidPolicy(String),
#[error(transparent)]
Transform(#[from] sim_lib_music_transform::TransformError),
#[error("transformed stretto entry is not monophonic: {0}")]
NonMonophonic(String),
#[error(transparent)]
Graph(#[from] sim_lib_discrete_graph::GraphError),
#[error(transparent)]
Music(#[from] sim_lib_music_core::MusicError),
}