Skip to main content

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}