///|
pub(all) struct MotionConstraint {
name : String
allow_parallel : Bool
allow_similar : Bool
allow_contrary : Bool
allow_oblique : Bool
max_leap : Int
} derive(Debug, Eq, ToJson)
///|
pub(all) struct MotionResult {
motion : MotionKind
accepted : Bool
cost : Int
message : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct VoiceLeadingSummary {
transitions : Int
accepted : Int
rejected : Int
contrary : Int
parallel : Int
similar : Int
oblique : Int
total_cost : Int
} derive(Debug, Eq, ToJson)
///|
pub fn MotionConstraint::classroom() -> MotionConstraint {
{
name: "classroom",
allow_parallel: false,
allow_similar: true,
allow_contrary: true,
allow_oblique: true,
max_leap: 12,
}
}
///|
pub fn MotionConstraint::strict() -> MotionConstraint {
{
name: "strict",
allow_parallel: false,
allow_similar: false,
allow_contrary: true,
allow_oblique: true,
max_leap: 9,
}
}
///|
pub fn MotionConstraint::allows(
self : MotionConstraint,
motion_value : MotionKind,
) -> Bool {
match motion_value {
Parallel => self.allow_parallel
Similar => self.allow_similar
Contrary => self.allow_contrary
Oblique => self.allow_oblique
Static => true
}
}
///|
pub fn motion_cost(value : MotionKind) -> Int {
match value {
Contrary => 0
Oblique => 1
Similar => 2
Parallel => 5
Static => 1
}
}
///|
pub fn evaluate_motion(
lower_before : Note,
lower_after : Note,
upper_before : Note,
upper_after : Note,
constraint : MotionConstraint,
) -> MotionResult {
let movement = motion(lower_before, lower_after, upper_before, upper_after)
let leap = absolute_melodic_interval(upper_before, upper_after)
let leap_ok = leap <= constraint.max_leap
let accepted = constraint.allows(movement) && leap_ok
{
motion: movement,
accepted,
cost: motion_cost(movement) + (if leap_ok { 0 } else { 10 }),
message: if accepted {
"motion satisfies the selected constraint"
} else {
"motion should be revised for the selected constraint"
},
}
}
///|
pub fn evaluate_voice_leading(
lower : Array[Note],
upper : Array[Note],
constraint : MotionConstraint,
) -> Array[MotionResult] {
let result : Array[MotionResult] = []
let n = if lower.length() < upper.length() {
lower.length()
} else {
upper.length()
}
if n < 2 {
return result
}
for i in 1.. VoiceLeadingSummary {
let results = evaluate_voice_leading(lower, upper, constraint)
let mut accepted = 0
let mut rejected = 0
let mut contrary = 0
let mut parallel = 0
let mut similar = 0
let mut oblique = 0
let mut total_cost = 0
for result in results {
if result.accepted {
accepted += 1
} else {
rejected += 1
}
total_cost += result.cost
match result.motion {
Contrary => contrary += 1
Parallel => parallel += 1
Similar => similar += 1
Oblique => oblique += 1
Static => ()
}
}
{
transitions: results.length(),
accepted,
rejected,
contrary,
parallel,
similar,
oblique,
total_cost,
}
}
///|
pub fn motion_acceptance_rate(summary : VoiceLeadingSummary) -> Int {
if summary.transitions == 0 {
0
} else {
summary.accepted * 100 / summary.transitions
}
}
///|
pub fn motion_rejection_indices(
lower : Array[Note],
upper : Array[Note],
constraint : MotionConstraint,
) -> Array[Int] {
let result : Array[Int] = []
for i, item in evaluate_voice_leading(lower, upper, constraint) {
if !item.accepted {
result.push(i + 1)
}
}
result
}
///|
pub fn contrary_motion_ratio(lower : Array[Note], upper : Array[Note]) -> Int {
let summary = pair_statistics(lower, upper)
let total = summary.contrary +
summary.parallel +
summary.similar +
summary.oblique +
summary.static_motion
if total == 0 {
0
} else {
summary.contrary * 100 / total
}
}
///|
pub fn parallel_perfect_indices(
lower : Array[Note],
upper : Array[Note],
) -> Array[Int] {
let result : Array[Int] = []
let n = if lower.length() < upper.length() {
lower.length()
} else {
upper.length()
}
if n < 2 {
return result
}
for i in 1.. Array[Int] {
let result : Array[Int] = []
let n = if lower.length() < upper.length() {
lower.length()
} else {
upper.length()
}
if n < 2 {
return result
}
for i in 1.. Int {
let summary = summarize_voice_leading(
lower,
upper,
MotionConstraint::classroom(),
)
100 - summary.rejected * 20 - summary.parallel * 5
}
///|
pub fn cap_quality(value : Int) -> Int {
if value < 0 {
0
} else if value > 100 {
100
} else {
value
}
}
///|
pub fn constrained_quality(
lower : Array[Note],
upper : Array[Note],
constraint : MotionConstraint,
) -> Int {
let summary = summarize_voice_leading(lower, upper, constraint)
cap_quality(100 - summary.rejected * 20 - summary.total_cost)
}