///|
pub(all) enum PhraseRole {
Opening
Continuation
Preparation
Cadence
Extension
} derive(Debug, Eq, ToJson)
///|
pub(all) struct PhraseSegment {
start : Int
end : Int
role : PhraseRole
notes : Int
pitch_range : Int
density : Int
} derive(Debug, Eq, ToJson)
///|
pub(all) struct CadencePlan {
penultimate : Int
final_pitch : Int
final_interval : Int
approach : Direction
valid : Bool
message : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct PhrasePlan {
segments : Array[PhraseSegment]
cadence : CadencePlan
total_notes : Int
balanced : Bool
} derive(Debug, Eq, ToJson)
///|
pub fn PhraseRole::label(self : PhraseRole) -> String {
match self {
Opening => "opening"
Continuation => "continuation"
Preparation => "preparation"
Cadence => "cadence"
Extension => "extension"
}
}
///|
pub fn split_points(notes : Array[Note], target : Int) -> Array[Int] {
let result : Array[Int] = []
if target <= 0 || notes.length() == 0 {
return result
}
let mut index = target
while index < notes.length() {
result.push(index)
index += target
}
result
}
///|
pub fn role_for_segment(index : Int, total : Int) -> PhraseRole {
if total <= 1 {
Opening
} else if index == 0 {
Opening
} else if index == total - 1 {
Cadence
} else if index == 1 {
Continuation
} else {
Preparation
}
}
///|
pub fn make_segment(
notes : Array[Note],
start : Int,
end : Int,
role : PhraseRole,
) -> PhraseSegment {
let safe_start = if start < 0 { 0 } else { start }
let safe_end = if end > notes.length() {
notes.length()
} else if end < safe_start {
safe_start
} else {
end
}
let selected : Array[Note] = []
for i in safe_start.. Array[PhraseSegment] {
let points = split_points(notes, target)
let segments : Array[PhraseSegment] = []
let mut start = 0
for end in points {
segments.push(
make_segment(
notes,
start,
end,
role_for_segment(segments.length(), points.length() + 1),
),
)
start = end
}
if start <= notes.length() {
segments.push(
make_segment(
notes,
start,
notes.length(),
role_for_segment(segments.length(), points.length() + 1),
),
)
}
segments
}
///|
pub fn cadence_plan(cantus : Array[Note], counter : Array[Note]) -> CadencePlan {
let n = if cantus.length() < counter.length() {
cantus.length()
} else {
counter.length()
}
if n < 2 {
return {
penultimate: 0,
final_pitch: 0,
final_interval: 0,
approach: Same,
valid: false,
message: "a cadence needs at least two aligned events",
}
}
let final_interval = interval_class(
harmonic_interval(cantus[n - 1], counter[n - 1]),
)
let approach = direction(counter[n - 2], counter[n - 1])
let valid_interval = final_interval == 0 || final_interval == 7
{
penultimate: counter[n - 2].midi,
final_pitch: counter[n - 1].midi,
final_interval,
approach,
valid: valid_interval,
message: if valid_interval {
"cadence sonority is consonant"
} else {
"choose a unison, octave, or fifth at the final event"
},
}
}
///|
pub fn cadence_candidates(
cantus : Note,
final_pitch : Int,
policy : CandidatePolicy,
) -> Array[PitchCandidate] {
let source = Note::new(
midi=final_pitch,
onset=cantus.onset,
duration=cantus.duration,
)
let candidates = generate_candidates(cantus, None, policy)
let result : Array[PitchCandidate] = []
for candidate in candidates {
let interval = interval_class(harmonic_interval(cantus, candidate.note))
if interval == 0 || interval == 7 {
result.push({
..candidate,
note: { ..candidate.note, midi: candidate.note.midi },
})
}
}
ignore(source)
result
}
///|
pub fn plan_phrase(
cantus : Array[Note],
counter : Array[Note],
segment_size : Int,
) -> PhrasePlan {
let segments = segment_phrase(counter, segment_size)
let cadence = cadence_plan(cantus, counter)
let mut balanced = true
for segment in segments {
if segment.density == 0 || segment.pitch_range > 24 {
balanced = false
}
}
{ segments, cadence, total_notes: counter.length(), balanced }
}
///|
pub fn phrase_length(plan : PhrasePlan) -> Int {
let mut result = 0
for segment in plan.segments {
result += segment.notes
}
result
}
///|
pub fn cadence_message(plan : PhrasePlan) -> String {
plan.cadence.message
}
///|
pub fn phrase_role_counts(plan : PhrasePlan) -> Array[Int] {
let result = Array::make(5, 0)
for segment in plan.segments {
let index = match segment.role {
Opening => 0
Continuation => 1
Preparation => 2
Cadence => 3
Extension => 4
}
result[index] += 1
}
result
}
///|
pub fn phrase_is_cadential(plan : PhrasePlan) -> Bool {
plan.cadence.valid && plan.segments.length() > 0
}