use sim_lib_music_core::{Pitch, Time};
use thiserror::Error;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Species {
First,
Second,
Third,
Fourth,
Open,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct PitchRange {
pub low: Pitch,
pub high: Pitch,
}
impl PitchRange {
pub fn midi() -> Self {
Self {
low: Pitch::from_midi(0),
high: Pitch::from_midi(127),
}
}
pub fn contains(self, pitch: Pitch) -> bool {
self.low <= pitch && pitch <= self.high
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IntervalRules {
pub max_melodic_semitones: u8,
pub forbidden_melodic_semitones: Vec<u8>,
pub consonant_harmonic_classes: Vec<u8>,
pub perfect_harmonic_classes: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MotionRules {
pub forbid_parallel_perfects: bool,
pub forbid_direct_perfects: bool,
pub leap_threshold: u8,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VoiceRules {
pub default_range: PitchRange,
pub ranges: Vec<PitchRange>,
pub highest_voice_first: bool,
pub allow_crossing: bool,
pub allow_overlap: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DurationRules {
pub pulse: Time,
pub allowed_pulse_ratios: Vec<Time>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DissonanceContext {
Passing,
Neighbor,
Suspension,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DissonanceRules {
pub allowed_contexts: Vec<DissonanceContext>,
pub max_step_semitones: u8,
pub require_weak_attack: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RuleSet {
pub id: String,
pub species: Species,
pub intervals: IntervalRules,
pub motion: MotionRules,
pub voices: VoiceRules,
pub durations: DurationRules,
pub dissonance: DissonanceRules,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum RuleError {
#[error("counterpoint rule-set id cannot be empty")]
EmptyId,
#[error("counterpoint duration pulse must be positive")]
InvalidPulse,
#[error("counterpoint pitch range {index} is reversed")]
ReversedRange {
index: usize,
},
#[error("counterpoint harmonic interval class {value} is outside 0..=6")]
InvalidIntervalClass {
value: u8,
},
}
impl RuleSet {
pub fn species_one(pulse: Time) -> Self {
species_rules(
"species-one",
Species::First,
pulse,
vec![Time::from_integer(1)],
vec![],
)
}
pub fn species_two(pulse: Time) -> Self {
species_rules(
"species-two",
Species::Second,
pulse,
vec![Time::new(1, 2), Time::from_integer(1)],
vec![DissonanceContext::Passing, DissonanceContext::Neighbor],
)
}
pub fn species_three(pulse: Time) -> Self {
species_rules(
"species-three",
Species::Third,
pulse,
vec![Time::new(1, 4), Time::new(1, 2), Time::from_integer(1)],
vec![DissonanceContext::Passing, DissonanceContext::Neighbor],
)
}
pub fn species_four(pulse: Time) -> Self {
species_rules(
"species-four",
Species::Fourth,
pulse,
vec![Time::new(1, 2), Time::from_integer(1)],
vec![DissonanceContext::Suspension],
)
}
pub fn open() -> Self {
Self {
id: "open".to_owned(),
species: Species::Open,
intervals: IntervalRules {
max_melodic_semitones: u8::MAX,
forbidden_melodic_semitones: Vec::new(),
consonant_harmonic_classes: (0..=6).collect(),
perfect_harmonic_classes: vec![0, 5],
},
motion: MotionRules {
forbid_parallel_perfects: false,
forbid_direct_perfects: false,
leap_threshold: u8::MAX,
},
voices: VoiceRules {
default_range: PitchRange::midi(),
ranges: Vec::new(),
highest_voice_first: true,
allow_crossing: true,
allow_overlap: true,
},
durations: DurationRules {
pulse: Time::from_integer(1),
allowed_pulse_ratios: Vec::new(),
},
dissonance: DissonanceRules {
allowed_contexts: Vec::new(),
max_step_semitones: 2,
require_weak_attack: false,
},
}
}
pub fn validate(&self) -> Result<(), RuleError> {
if self.id.trim().is_empty() {
return Err(RuleError::EmptyId);
}
if self.durations.pulse <= Time::from_integer(0) {
return Err(RuleError::InvalidPulse);
}
for (index, range) in std::iter::once(&self.voices.default_range)
.chain(&self.voices.ranges)
.enumerate()
{
if range.low > range.high {
return Err(RuleError::ReversedRange { index });
}
}
for value in self
.intervals
.consonant_harmonic_classes
.iter()
.chain(&self.intervals.perfect_harmonic_classes)
{
if *value > 6 {
return Err(RuleError::InvalidIntervalClass { value: *value });
}
}
Ok(())
}
pub fn range_for_voice(&self, index: usize) -> PitchRange {
self.voices
.ranges
.get(index)
.copied()
.unwrap_or(self.voices.default_range)
}
}
fn species_rules(
id: &str,
species: Species,
pulse: Time,
allowed_pulse_ratios: Vec<Time>,
allowed_contexts: Vec<DissonanceContext>,
) -> RuleSet {
RuleSet {
id: id.to_owned(),
species,
intervals: IntervalRules {
max_melodic_semitones: 12,
forbidden_melodic_semitones: vec![6, 10, 11],
consonant_harmonic_classes: vec![0, 3, 4, 5],
perfect_harmonic_classes: vec![0, 5],
},
motion: MotionRules {
forbid_parallel_perfects: true,
forbid_direct_perfects: true,
leap_threshold: 2,
},
voices: VoiceRules {
default_range: PitchRange::midi(),
ranges: Vec::new(),
highest_voice_first: true,
allow_crossing: false,
allow_overlap: false,
},
durations: DurationRules {
pulse,
allowed_pulse_ratios,
},
dissonance: DissonanceRules {
allowed_contexts,
max_step_semitones: 2,
require_weak_attack: !matches!(species, Species::Fourth),
},
}
}