///|
pub(all) enum VoiceRange {
Soprano
Alto
Tenor
Bass
CustomRange(low~ : Int, high~ : Int)
} derive(Debug, Eq, ToJson)
///|
pub(all) struct VoiceAssessment {
role : VoiceRole
range : VoiceRange
notes : Int
violations : Int
lowest : Int
highest : Int
} derive(Debug, Eq, ToJson)
///|
pub fn VoiceRange::low(self : VoiceRange) -> Int {
match self {
Soprano => 60
Alto => 48
Tenor => 40
Bass => 28
CustomRange(low~, ..) => low
}
}
///|
pub fn VoiceRange::high(self : VoiceRange) -> Int {
match self {
Soprano => 84
Alto => 72
Tenor => 67
Bass => 55
CustomRange(high~, ..) => high
}
}
///|
pub fn VoiceRange::contains(self : VoiceRange, midi : Int) -> Bool {
self.low() <= midi && midi <= self.high()
}
///|
pub fn VoiceRange::width(self : VoiceRange) -> Int {
self.high() - self.low()
}
///|
pub fn assess_voice(
notes : Array[Note],
role : VoiceRole,
range : VoiceRange,
) -> VoiceAssessment {
let mut lowest = 127
let mut highest = -1
let mut violations = 0
for note in notes {
if note.midi < lowest {
lowest = note.midi
}
if note.midi > highest {
highest = note.midi
}
if !range.contains(note.midi) {
violations += 1
}
}
{ role, range, notes: notes.length(), violations, lowest, highest }
}
///|
pub fn VoiceAssessment::passed(self : VoiceAssessment) -> Bool {
self.violations == 0
}
///|
pub fn VoiceAssessment::span(self : VoiceAssessment) -> Int {
if self.notes == 0 {
0
} else {
self.highest - self.lowest
}
}
///|
pub fn voice_crossing_at(lower : Note, upper : Note) -> Bool {
lower.midi >= upper.midi
}
///|
pub fn crossing_count(lower : Array[Note], upper : Array[Note]) -> Int {
let n = if lower.length() < upper.length() {
lower.length()
} else {
upper.length()
}
let mut total = 0
for i in 0.. Int {
let n = if lower.length() < upper.length() {
lower.length()
} else {
upper.length()
}
let mut total = 0
for i in 0.. Int {
if notes.length() == 0 {
return 0
}
let mut sum = 0
for note in notes {
sum += note.midi
}
sum / notes.length()
}
///|
pub fn register_distance(notes : Array[Note], target : Int) -> Int {
let mut sum = 0
for note in notes {
sum += if note.midi > target {
note.midi - target
} else {
target - note.midi
}
}
if notes.length() == 0 {
0
} else {
sum / notes.length()
}
}
///|
pub fn voice_role_name(role : VoiceRole) -> String {
match role {
Cantus => "cantus"
Counterpoint => "counterpoint"
Upper => "upper"
Lower => "lower"
}
}
///|
pub fn voice_range_name(range : VoiceRange) -> String {
match range {
Soprano => "soprano"
Alto => "alto"
Tenor => "tenor"
Bass => "bass"
CustomRange(..) => "custom"
}
}
///|
pub fn voice_assessment_status(assessment : VoiceAssessment) -> String {
if assessment.violations == 0 {
"pass"
} else if assessment.violations * 2 <= assessment.notes {
"review"
} else {
"fail"
}
}
///|
pub fn voice_assessment_violation_rate(assessment : VoiceAssessment) -> Int {
if assessment.notes == 0 {
0
} else {
assessment.violations * 100 / assessment.notes
}
}
///|
pub fn voice_assessment_repair_distance(assessment : VoiceAssessment) -> Int {
if assessment.notes == 0 {
0
} else {
let mut distance = 0
if assessment.lowest < assessment.range.low() {
distance += assessment.range.low() - assessment.lowest
}
if assessment.highest > assessment.range.high() {
distance += assessment.highest - assessment.range.high()
}
distance
}
}
///|
pub fn voice_assessment_summary(assessment : VoiceAssessment) -> String {
"role=\{voice_role_name(assessment.role)} range=\{voice_range_name(assessment.range)} status=\{voice_assessment_status(assessment)} notes=\{assessment.notes} violations=\{assessment.violations} rate=\{voice_assessment_violation_rate(assessment)}%"
}
///|
pub fn voice_assessment_markdown(assessment : VoiceAssessment) -> String {
let status = voice_assessment_status(assessment)
[
"## Voice register",
"",
"- Role: \{voice_role_name(assessment.role)}",
"- Range: \{voice_range_name(assessment.range)}",
"- Status: \{status}",
"- Violations: \{assessment.violations}/\{assessment.notes}",
"- Repair distance: \{voice_assessment_repair_distance(assessment)} semitones",
].join("\n")
}
///|
pub fn voices_fit(
lower : Array[Note],
upper : Array[Note],
lower_range : VoiceRange,
upper_range : VoiceRange,
) -> Bool {
let lower_assessment = assess_voice(lower, Lower, lower_range)
let upper_assessment = assess_voice(upper, Upper, upper_range)
lower_assessment.passed() &&
upper_assessment.passed() &&
crossing_count(lower, upper) == 0
}
///|
pub fn voice_assessment_is_extreme(assessment : VoiceAssessment) -> Bool {
assessment.notes > 0 &&
(
voice_assessment_violation_rate(assessment) >= 75 ||
voice_assessment_repair_distance(assessment) > assessment.range.width() / 2
)
}
///|
pub fn voice_assessment_pitch_center(assessment : VoiceAssessment) -> Int {
if assessment.notes == 0 {
0
} else {
(assessment.lowest + assessment.highest) / 2
}
}