sim_lib_music_consonance/model.rs
1use sim_lib_music_core::{Articulation, Channel, ObjectId, Pitch, Time};
2use sim_lib_pitch_dissonance::ContextualSonanceOptions;
3use sim_lib_pitch_namer::LabelContext;
4use sim_lib_sound_tuning::EqualTemperament;
5use thiserror::Error;
6
7/// Exact half-open musical span `[start, end)`.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct TimeSpan {
10 /// Inclusive start.
11 pub start: Time,
12 /// Exclusive end.
13 pub end: Time,
14}
15
16impl TimeSpan {
17 /// Builds a non-negative, ordered span.
18 pub fn new(start: Time, end: Time) -> Result<Self, ConsonanceError> {
19 let zero = Time::from_integer(0);
20 if start < zero || end < start {
21 return Err(ConsonanceError::InvalidSpan { start, end });
22 }
23 Ok(Self { start, end })
24 }
25
26 /// Returns the exact span length.
27 pub fn duration(&self) -> Time {
28 self.end - self.start
29 }
30}
31
32/// Kind of source from which a consonance report was derived.
33#[derive(Copy, Clone, Debug, PartialEq, Eq)]
34pub enum ProvenanceKind {
35 /// A canonical [`sim_lib_music_core::Score`].
36 Score,
37 /// An identity-bearing [`sim_lib_music_core::Staff`].
38 Staff,
39 /// A pedal- and overlap-realized MIDI timeline.
40 MidiTimeline,
41}
42
43/// Report-level source and identity evidence.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct Provenance {
46 /// Source representation.
47 pub kind: ProvenanceKind,
48 /// Stable source label.
49 pub source: String,
50 /// How source voice, note, and event identities were obtained.
51 pub identity_policy: String,
52 /// Exact conversion, realization, or source facts.
53 pub facts: Vec<String>,
54}
55
56/// One sounding note with full source identity and exact lifetime.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct SoundingNote {
59 /// Voice identity.
60 pub voice_id: ObjectId,
61 /// Logical note identity.
62 pub note_id: ObjectId,
63 /// Event identity.
64 pub event_id: ObjectId,
65 /// Octave-aware pitch.
66 pub pitch: Pitch,
67 /// Exact source onset.
68 pub onset: Time,
69 /// Exact half-open source release.
70 pub release: Time,
71 /// MIDI velocity, retained independently from acoustic amplitude policy.
72 pub velocity: u8,
73 /// MIDI channel.
74 pub channel: Channel,
75 /// Notated or realized articulation.
76 pub articulation: Articulation,
77 /// Note-local source facts.
78 pub provenance: Vec<String>,
79}
80
81/// One maximal exact interval with a constant sounding-note multiset.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct SoundingWindow {
84 /// Exact half-open span.
85 pub span: TimeSpan,
86 /// Every note sounding throughout the span, including equal-pitch events.
87 pub notes: Vec<SoundingNote>,
88}
89
90/// Independently inspectable output from one named metric model.
91#[derive(Clone, Debug, PartialEq)]
92pub struct MetricReport {
93 /// Stable model name.
94 pub model: String,
95 /// Total roughness or conflict mass before density normalization.
96 pub roughness_mass: f64,
97 /// Density normalized by the model's named opportunity policy.
98 pub normalized_density: f64,
99 /// Harmonic, commonality, ratio, or continuity context component.
100 pub harmonic_context: f64,
101 /// Named normalization policy.
102 pub normalization: String,
103 /// Named aggregation policy.
104 pub aggregation: String,
105 /// Model-specific provenance.
106 pub evidence: Vec<String>,
107}
108
109/// All metric families for one sounding window.
110#[derive(Clone, Debug, PartialEq)]
111pub struct WindowSonance {
112 /// Source window, including multiplicity and identities.
113 pub window: SoundingWindow,
114 /// Set-domain pitch models, each retained separately.
115 pub pitch: Vec<MetricReport>,
116 /// Frequency- and amplitude-domain models, each retained separately.
117 pub acoustic: Vec<MetricReport>,
118 /// Exact-ratio contextual model.
119 pub ratio: MetricReport,
120 /// Event-commonality contextual model.
121 pub commonality: MetricReport,
122 /// Voice-leading contextual model.
123 pub leading: MetricReport,
124}
125
126/// Complete consonance evaluation without an implicit aggregate score.
127#[derive(Clone, Debug, PartialEq)]
128pub struct ConsonanceReport {
129 /// Event-boundary windows and their separate metrics.
130 pub windows: Vec<WindowSonance>,
131 /// Source and identity evidence.
132 pub provenance: Provenance,
133}
134
135/// Explicit policy for score consonance evaluation.
136#[derive(Clone, Debug, PartialEq)]
137pub struct ConsonancePolicy {
138 /// Context for pitch-domain models.
139 pub pitch_context: LabelContext,
140 /// Duplicate, normalization, ratio, and voice policy for contextual models.
141 pub contextual: ContextualSonanceOptions,
142 /// Equal-temperament acoustic realization used before sound analysis.
143 pub tuning: EqualTemperament,
144 /// Requested pitch model names.
145 pub pitch_models: Vec<String>,
146 /// Requested acoustic model names.
147 pub acoustic_models: Vec<String>,
148}
149
150impl Default for ConsonancePolicy {
151 fn default() -> Self {
152 Self {
153 pitch_context: LabelContext::default(),
154 contextual: ContextualSonanceOptions::standard(),
155 tuning: EqualTemperament::default(),
156 pitch_models: [
157 "interval-vector",
158 "forte-complexity",
159 "tonal-function",
160 "tritone-density",
161 ]
162 .into_iter()
163 .map(str::to_owned)
164 .collect(),
165 acoustic_models: [
166 "harmonic-entropy",
167 "helmholtz-beating",
168 "plomp-levelt",
169 "sethares",
170 ]
171 .into_iter()
172 .map(str::to_owned)
173 .collect(),
174 }
175 }
176}
177
178/// Error raised by exact window construction or metric evaluation.
179#[derive(Debug, Error, Clone, PartialEq)]
180pub enum ConsonanceError {
181 /// A half-open span was negative or reversed.
182 #[error("invalid half-open span [{start}, {end})")]
183 InvalidSpan {
184 /// Invalid start.
185 start: Time,
186 /// Invalid end.
187 end: Time,
188 },
189 /// A requested model was not installed.
190 #[error("unknown {domain} consonance model {model}")]
191 UnknownModel {
192 /// Metric domain.
193 domain: &'static str,
194 /// Requested model.
195 model: String,
196 },
197 /// Raw MIDI score bodies require the realization-aware entry point.
198 #[error("raw MIDI score bodies must be realized before consonance evaluation")]
199 MidiRequiresRealization,
200 /// Existing exact score conversion rejected the source.
201 #[error("score conversion failed: {0}")]
202 ScoreConversion(String),
203 /// An identity derived from a source was invalid.
204 #[error("invalid consonance identity: {0}")]
205 Identity(String),
206 /// Acoustic analysis rejected the realized tones.
207 #[error("acoustic consonance evaluation failed: {0}")]
208 Acoustic(String),
209}