///|
pub(all) enum BeatStrength {
Downbeat
Strong
Weak
Offbeat
} derive(Debug, Eq, ToJson)
///|
pub(all) struct Meter {
numerator : Int
denominator : Int
subdivision : Int
} derive(Debug, Eq, ToJson)
///|
pub(all) struct BeatPoint {
index : Int
time : Int
measure : Int
beat : Int
strength : BeatStrength
} derive(Debug, Eq, ToJson)
///|
pub(all) struct RhythmSummary {
notes : Int
measures : Int
downbeats : Int
offbeats : Int
syncopations : Int
tied_notes : Int
total_duration : Int
} derive(Debug, Eq, ToJson)
///|
pub fn Meter::new(
numerator~ : Int,
denominator~ : Int,
subdivision? : Int = 1,
) -> Meter {
{ numerator, denominator, subdivision }
}
///|
pub fn Meter::is_valid(self : Meter) -> Bool {
self.numerator > 0 &&
self.denominator > 0 &&
self.subdivision > 0 &&
self.numerator <= 16 &&
self.denominator <= 32
}
///|
pub fn Meter::measure_length(self : Meter) -> Int {
if !self.is_valid() {
0
} else {
self.numerator * self.subdivision
}
}
///|
pub fn Meter::beat_length(self : Meter) -> Int {
if self.subdivision <= 0 {
1
} else {
self.subdivision
}
}
///|
pub fn Meter::beats_per_measure(self : Meter) -> Int {
if self.numerator <= 0 {
1
} else {
self.numerator
}
}
///|
pub fn beat_strength(meter : Meter, beat : Int) -> BeatStrength {
let normalized = if meter.numerator <= 0 { 0 } else { beat % meter.numerator }
if normalized == 0 {
Downbeat
} else if meter.numerator % 2 == 0 && normalized == meter.numerator / 2 {
Strong
} else if normalized % 2 == 0 {
Weak
} else {
Offbeat
}
}
///|
pub fn beat_point(meter : Meter, time : Int, index : Int) -> BeatPoint {
let length = meter.measure_length()
let safe_length = if length <= 0 { 1 } else { length }
let measure = time / safe_length
let offset = time % safe_length
let beat_length = meter.beat_length()
let beat = if beat_length <= 0 { 0 } else { offset / beat_length }
{ index, time, measure, beat, strength: beat_strength(meter, beat) }
}
///|
pub fn beat_points(meter : Meter, notes : Array[Note]) -> Array[BeatPoint] {
let result : Array[BeatPoint] = []
for i, note in notes {
result.push(beat_point(meter, note.onset, i))
}
result
}
///|
pub fn is_accented(point : BeatPoint) -> Bool {
point.strength == Downbeat || point.strength == Strong
}
///|
pub fn is_weak(point : BeatPoint) -> Bool {
point.strength == Weak || point.strength == Offbeat
}
///|
pub fn beat_label(point : BeatPoint) -> String {
match point.strength {
Downbeat => "downbeat"
Strong => "strong"
Weak => "weak"
Offbeat => "offbeat"
}
}
///|
pub fn quantize_time(time : Int, grid : Int) -> Int {
if grid <= 0 {
time
} else {
let remainder = time % grid
if remainder * 2 < grid {
time - remainder
} else {
time + grid - remainder
}
}
}
///|
pub fn quantize_note(note : Note, grid : Int) -> Note {
{ ..note, onset: quantize_time(note.onset, grid) }
}
///|
pub fn quantize_sequence(notes : Array[Note], grid : Int) -> Array[Note] {
let result : Array[Note] = []
for note in notes {
result.push(quantize_note(note, grid))
}
result
}
///|
pub fn measure_of(meter : Meter, time : Int) -> Int {
let length = meter.measure_length()
if length <= 0 {
0
} else {
time / length
}
}
///|
pub fn beat_of(meter : Meter, time : Int) -> Int {
let length = meter.measure_length()
let beat = meter.beat_length()
if length <= 0 || beat <= 0 {
0
} else {
time % length / beat
}
}
///|
pub fn is_measure_boundary(meter : Meter, time : Int) -> Bool {
let length = meter.measure_length()
length > 0 && time % length == 0
}
///|
pub fn duration_histogram(notes : Array[Note]) -> Array[Int] {
let result = Array::make(17, 0)
for note in notes {
let index = if note.duration < 0 {
0
} else if note.duration > 16 {
16
} else {
note.duration
}
result[index] += 1
}
result
}
///|
pub fn onset_histogram(notes : Array[Note], period : Int) -> Array[Int] {
let size = if period <= 0 { 1 } else { period }
let result = Array::make(size, 0)
for note in notes {
let index = note.onset % size
let safe_index = if index < 0 { index + size } else { index }
result[safe_index] += 1
}
result
}
///|
pub fn gap_count(notes : Array[Note]) -> Int {
if notes.length() < 2 {
return 0
}
let mut result = 0
for i in 1.. note_end(notes[i - 1]) {
result += 1
}
}
result
}
///|
pub fn syncopation_count(meter : Meter, notes : Array[Note]) -> Int {
let points = beat_points(meter, notes)
let mut result = 0
for i, note in notes {
if i + 1 < notes.length() &&
note_end(note) > notes[i + 1].onset &&
is_accented(points[i]) &&
is_weak(points[i + 1]) {
result += 1
}
}
result
}
///|
pub fn summarize_rhythm(meter : Meter, notes : Array[Note]) -> RhythmSummary {
let points = beat_points(meter, notes)
let mut downbeats = 0
let mut offbeats = 0
for point in points {
if point.strength == Downbeat {
downbeats += 1
}
if point.strength == Offbeat {
offbeats += 1
}
}
{
notes: notes.length(),
measures: if notes.length() == 0 {
0
} else {
measure_of(meter, sequence_end(notes)) + 1
},
downbeats,
offbeats,
syncopations: syncopation_count(meter, notes),
tied_notes: tied_count(notes),
total_duration: total_sounding_duration(notes),
}
}
///|
pub fn rhythm_density(meter : Meter, notes : Array[Note]) -> Int {
let summary = summarize_rhythm(meter, notes)
if summary.measures == 0 {
0
} else {
summary.notes / summary.measures
}
}
///|
pub fn duration_ratio(notes : Array[Note], duration : Int) -> Int {
let total = notes.length()
if total == 0 {
0
} else {
count_duration(notes, duration) * 100 / total
}
}