1use sim_lib_music_core::{Pitch, Time};
2use thiserror::Error;
3
4#[derive(Copy, Clone, Debug, PartialEq, Eq)]
6pub enum Species {
7 First,
9 Second,
11 Third,
13 Fourth,
15 Open,
17}
18
19#[derive(Copy, Clone, Debug, PartialEq, Eq)]
21pub struct PitchRange {
22 pub low: Pitch,
24 pub high: Pitch,
26}
27
28impl PitchRange {
29 pub fn midi() -> Self {
31 Self {
32 low: Pitch::from_midi(0),
33 high: Pitch::from_midi(127),
34 }
35 }
36
37 pub fn contains(self, pitch: Pitch) -> bool {
39 self.low <= pitch && pitch <= self.high
40 }
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct IntervalRules {
46 pub max_melodic_semitones: u8,
48 pub forbidden_melodic_semitones: Vec<u8>,
50 pub consonant_harmonic_classes: Vec<u8>,
52 pub perfect_harmonic_classes: Vec<u8>,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
58pub struct MotionRules {
59 pub forbid_parallel_perfects: bool,
61 pub forbid_direct_perfects: bool,
63 pub leap_threshold: u8,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct VoiceRules {
70 pub default_range: PitchRange,
72 pub ranges: Vec<PitchRange>,
74 pub highest_voice_first: bool,
76 pub allow_crossing: bool,
78 pub allow_overlap: bool,
80}
81
82#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct DurationRules {
85 pub pulse: Time,
87 pub allowed_pulse_ratios: Vec<Time>,
89}
90
91#[derive(Copy, Clone, Debug, PartialEq, Eq)]
93pub enum DissonanceContext {
94 Passing,
96 Neighbor,
98 Suspension,
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct DissonanceRules {
105 pub allowed_contexts: Vec<DissonanceContext>,
107 pub max_step_semitones: u8,
109 pub require_weak_attack: bool,
111}
112
113#[derive(Clone, Debug, PartialEq, Eq)]
115pub struct RuleSet {
116 pub id: String,
118 pub species: Species,
120 pub intervals: IntervalRules,
122 pub motion: MotionRules,
124 pub voices: VoiceRules,
126 pub durations: DurationRules,
128 pub dissonance: DissonanceRules,
130}
131
132#[derive(Clone, Debug, Error, PartialEq, Eq)]
134pub enum RuleError {
135 #[error("counterpoint rule-set id cannot be empty")]
137 EmptyId,
138 #[error("counterpoint duration pulse must be positive")]
140 InvalidPulse,
141 #[error("counterpoint pitch range {index} is reversed")]
143 ReversedRange {
144 index: usize,
146 },
147 #[error("counterpoint harmonic interval class {value} is outside 0..=6")]
149 InvalidIntervalClass {
150 value: u8,
152 },
153}
154
155impl RuleSet {
156 pub fn species_one(pulse: Time) -> Self {
158 species_rules(
159 "species-one",
160 Species::First,
161 pulse,
162 vec![Time::from_integer(1)],
163 vec![],
164 )
165 }
166
167 pub fn species_two(pulse: Time) -> Self {
169 species_rules(
170 "species-two",
171 Species::Second,
172 pulse,
173 vec![Time::new(1, 2), Time::from_integer(1)],
174 vec![DissonanceContext::Passing, DissonanceContext::Neighbor],
175 )
176 }
177
178 pub fn species_three(pulse: Time) -> Self {
180 species_rules(
181 "species-three",
182 Species::Third,
183 pulse,
184 vec![Time::new(1, 4), Time::new(1, 2), Time::from_integer(1)],
185 vec![DissonanceContext::Passing, DissonanceContext::Neighbor],
186 )
187 }
188
189 pub fn species_four(pulse: Time) -> Self {
191 species_rules(
192 "species-four",
193 Species::Fourth,
194 pulse,
195 vec![Time::new(1, 2), Time::from_integer(1)],
196 vec![DissonanceContext::Suspension],
197 )
198 }
199
200 pub fn open() -> Self {
202 Self {
203 id: "open".to_owned(),
204 species: Species::Open,
205 intervals: IntervalRules {
206 max_melodic_semitones: u8::MAX,
207 forbidden_melodic_semitones: Vec::new(),
208 consonant_harmonic_classes: (0..=6).collect(),
209 perfect_harmonic_classes: vec![0, 5],
210 },
211 motion: MotionRules {
212 forbid_parallel_perfects: false,
213 forbid_direct_perfects: false,
214 leap_threshold: u8::MAX,
215 },
216 voices: VoiceRules {
217 default_range: PitchRange::midi(),
218 ranges: Vec::new(),
219 highest_voice_first: true,
220 allow_crossing: true,
221 allow_overlap: true,
222 },
223 durations: DurationRules {
224 pulse: Time::from_integer(1),
225 allowed_pulse_ratios: Vec::new(),
226 },
227 dissonance: DissonanceRules {
228 allowed_contexts: Vec::new(),
229 max_step_semitones: 2,
230 require_weak_attack: false,
231 },
232 }
233 }
234
235 pub fn validate(&self) -> Result<(), RuleError> {
237 if self.id.trim().is_empty() {
238 return Err(RuleError::EmptyId);
239 }
240 if self.durations.pulse <= Time::from_integer(0) {
241 return Err(RuleError::InvalidPulse);
242 }
243 for (index, range) in std::iter::once(&self.voices.default_range)
244 .chain(&self.voices.ranges)
245 .enumerate()
246 {
247 if range.low > range.high {
248 return Err(RuleError::ReversedRange { index });
249 }
250 }
251 for value in self
252 .intervals
253 .consonant_harmonic_classes
254 .iter()
255 .chain(&self.intervals.perfect_harmonic_classes)
256 {
257 if *value > 6 {
258 return Err(RuleError::InvalidIntervalClass { value: *value });
259 }
260 }
261 Ok(())
262 }
263
264 pub fn range_for_voice(&self, index: usize) -> PitchRange {
266 self.voices
267 .ranges
268 .get(index)
269 .copied()
270 .unwrap_or(self.voices.default_range)
271 }
272}
273
274fn species_rules(
275 id: &str,
276 species: Species,
277 pulse: Time,
278 allowed_pulse_ratios: Vec<Time>,
279 allowed_contexts: Vec<DissonanceContext>,
280) -> RuleSet {
281 RuleSet {
282 id: id.to_owned(),
283 species,
284 intervals: IntervalRules {
285 max_melodic_semitones: 12,
286 forbidden_melodic_semitones: vec![6, 10, 11],
287 consonant_harmonic_classes: vec![0, 3, 4, 5],
288 perfect_harmonic_classes: vec![0, 5],
289 },
290 motion: MotionRules {
291 forbid_parallel_perfects: true,
292 forbid_direct_perfects: true,
293 leap_threshold: 2,
294 },
295 voices: VoiceRules {
296 default_range: PitchRange::midi(),
297 ranges: Vec::new(),
298 highest_voice_first: true,
299 allow_crossing: false,
300 allow_overlap: false,
301 },
302 durations: DurationRules {
303 pulse,
304 allowed_pulse_ratios,
305 },
306 dissonance: DissonanceRules {
307 allowed_contexts,
308 max_step_semitones: 2,
309 require_weak_attack: !matches!(species, Species::Fourth),
310 },
311 }
312}