1use std::collections::{BTreeMap, BTreeSet};
4
5use sim_lib_music_core::{Pitch, Time};
6use sim_lib_pitch_dissonance::ContextualSonanceReport;
7use sim_lib_pitch_scale::PlayerScale;
8
9use crate::pitch_map::MapWitness;
10use crate::{OrdinalRef, RealizerId, SerialEventId};
11
12#[derive(Clone, Debug, PartialEq, Eq)]
14pub enum SerialSpineKind {
15 DegreeCycle,
17 NearestScaleTone,
19 MarkedChromaticInflection,
21 NonPitchSpine,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum SerialSpineLabel {
28 Degree(usize),
30 LandedPitch(Pitch),
32 ChromaticInflection {
34 degree: usize,
36 semitone_delta: i16,
38 },
39 OrdinalToken {
41 ordinal: OrdinalRef,
43 note_index: usize,
45 },
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct SerialSpineEntry {
51 pub event_id: SerialEventId,
53 pub ordinal: OrdinalRef,
55 pub note_index: usize,
57 pub onset: Time,
59 pub source_pitch: Pitch,
61 pub landed_pitch: Pitch,
63 pub modal_degree: Option<usize>,
65 pub modal_member: bool,
67 pub witness: MapWitness,
69 pub label: SerialSpineLabel,
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct SerialSpineCollision {
76 pub landed_class: u8,
78 pub source_classes: Vec<u8>,
80}
81
82#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct SerialRepeatedDegree {
85 pub degree: usize,
87 pub events: Vec<SerialEventId>,
89}
90
91#[derive(Clone, Debug, PartialEq, Eq)]
93pub struct ChromaticAggregateIdentity {
94 pub source_classes: Vec<u8>,
96 pub landed_classes: Vec<u8>,
98 pub lost_source_classes: Vec<u8>,
100 pub preserved: bool,
102}
103
104#[derive(Clone, Debug, PartialEq)]
106pub struct SerialSonanceContext {
107 pub from_event: SerialEventId,
109 pub to_event: SerialEventId,
111 pub report: ContextualSonanceReport,
113}
114
115#[derive(Clone, Debug, PartialEq)]
117pub struct SerialSpineReport {
118 pub realizer_id: RealizerId,
120 pub kind: SerialSpineKind,
122 pub scale: PlayerScale,
124 pub entries: Vec<SerialSpineEntry>,
126 pub collisions: Vec<SerialSpineCollision>,
128 pub repeated_degrees: Vec<SerialRepeatedDegree>,
130 pub out_of_mode: Vec<SerialEventId>,
132 pub pitch_changes: Vec<SerialEventId>,
134 pub aggregate_identity: ChromaticAggregateIdentity,
136 pub ordinal_order: Vec<OrdinalRef>,
138 pub sonance_context: Vec<SerialSonanceContext>,
140}
141
142impl SerialSpineReport {
143 pub fn modal_membership(&self) -> Vec<(SerialEventId, bool, Option<usize>)> {
145 self.entries
146 .iter()
147 .map(|entry| {
148 (
149 entry.event_id.clone(),
150 entry.modal_member,
151 entry.modal_degree,
152 )
153 })
154 .collect()
155 }
156
157 pub fn pitch_identity(&self) -> Vec<(SerialEventId, Pitch, Pitch, MapWitness)> {
159 self.entries
160 .iter()
161 .map(|entry| {
162 (
163 entry.event_id.clone(),
164 entry.source_pitch,
165 entry.landed_pitch,
166 entry.witness.clone(),
167 )
168 })
169 .collect()
170 }
171
172 pub fn chromatic_aggregate_identity(&self) -> &ChromaticAggregateIdentity {
174 &self.aggregate_identity
175 }
176
177 pub fn ordinal_order(&self) -> &[OrdinalRef] {
179 &self.ordinal_order
180 }
181
182 pub fn sonance_context(&self) -> &[SerialSonanceContext] {
184 &self.sonance_context
185 }
186}
187
188pub(crate) fn collect_collisions(entries: &[SerialSpineEntry]) -> Vec<SerialSpineCollision> {
189 let mut by_target = BTreeMap::<u8, BTreeSet<u8>>::new();
190 for entry in entries {
191 by_target
192 .entry(entry.landed_pitch.class.value())
193 .or_default()
194 .insert(entry.source_pitch.class.value());
195 }
196 by_target
197 .into_iter()
198 .filter_map(|(landed_class, source_classes)| {
199 (source_classes.len() > 1).then(|| SerialSpineCollision {
200 landed_class,
201 source_classes: source_classes.into_iter().collect(),
202 })
203 })
204 .collect()
205}
206
207pub(crate) fn collect_repeated_degrees(entries: &[SerialSpineEntry]) -> Vec<SerialRepeatedDegree> {
208 let mut out = Vec::new();
209 let mut current_degree = None;
210 let mut current_events = Vec::<SerialEventId>::new();
211 for entry in entries {
212 if let Some(degree) = entry.modal_degree {
213 if current_degree == Some(degree) {
214 current_events.push(entry.event_id.clone());
215 } else {
216 if let Some(previous_degree) = current_degree.take()
217 && current_events.len() > 1
218 {
219 out.push(SerialRepeatedDegree {
220 degree: previous_degree,
221 events: current_events.clone(),
222 });
223 }
224 current_degree = Some(degree);
225 current_events = vec![entry.event_id.clone()];
226 }
227 }
228 }
229 if let Some(degree) = current_degree
230 && current_events.len() > 1
231 {
232 out.push(SerialRepeatedDegree {
233 degree,
234 events: current_events,
235 });
236 }
237 out
238}
239
240pub(crate) fn aggregate_identity(entries: &[SerialSpineEntry]) -> ChromaticAggregateIdentity {
241 let source = entries
242 .iter()
243 .map(|entry| entry.source_pitch.class.value())
244 .collect::<BTreeSet<_>>();
245 let landed = entries
246 .iter()
247 .map(|entry| entry.landed_pitch.class.value())
248 .collect::<BTreeSet<_>>();
249 let lost_source_classes = source.difference(&landed).copied().collect::<Vec<_>>();
250 ChromaticAggregateIdentity {
251 source_classes: source.iter().copied().collect(),
252 landed_classes: landed.iter().copied().collect(),
253 lost_source_classes: lost_source_classes.clone(),
254 preserved: lost_source_classes.is_empty() && source == landed,
255 }
256}