///|
/// A tempo change in an editorial timeline. Beats are represented as a
/// continuous Double so a video frame can land between integer beat markers.
pub struct TempoPoint2 {
beat : Double
bpm : Double
} derive(Debug)
///|
pub fn tempo_point2(beat : Double, bpm : Double) -> TempoPoint2 {
{ beat, bpm: bpm.max(0.0001) }
}
///|
pub fn TempoPoint2::beat(self : TempoPoint2) -> Double {
self.beat
}
///|
pub fn TempoPoint2::bpm(self : TempoPoint2) -> Double {
self.bpm
}
///|
pub struct TempoTimeline {
points : Array[TempoPoint2]
origin_seconds : Double
} derive(Debug)
///|
pub fn TempoTimeline::new(
points : Array[TempoPoint2],
origin_seconds? : Double = 0.0,
) -> TempoTimeline {
let sorted = points.copy()
sorted.sort_by(fn(left, right) { left.beat.compare(right.beat) })
{ points: sorted, origin_seconds }
}
///|
pub fn TempoTimeline::points(self : TempoTimeline) -> Array[TempoPoint2] {
self.points.copy()
}
///|
pub fn TempoTimeline::length(self : TempoTimeline) -> Int {
self.points.length()
}
///|
fn TempoTimeline::at_beat(self : TempoTimeline, beat : Double) -> TempoPoint2 {
if self.points.length() == 0 {
return tempo_point2(0.0, 120.0)
}
let mut current = self.points[0]
for point in self.points {
if point.beat > beat {
return current
}
current = point
}
current
}
///|
fn TempoTimeline::seconds_before(self : TempoTimeline, beat : Double) -> Double {
if self.points.length() == 0 {
return beat * 0.5
}
let mut seconds = 0.0
let mut previous_beat = self.points[0].beat
let mut previous_bpm = self.points[0].bpm
if beat <= previous_beat {
return (beat - previous_beat) * 60.0 / previous_bpm
}
for index in 1.. Double {
self.origin_seconds + self.seconds_before(beat)
}
///|
pub fn TempoTimeline::seconds_to_beat(
self : TempoTimeline,
seconds : Double,
) -> Double {
if self.points.length() == 0 {
return seconds * 2.0
}
let target = seconds - self.origin_seconds
let mut accumulated = 0.0
let mut previous_beat = self.points[0].beat
let mut previous_bpm = self.points[0].bpm
if target <= 0.0 {
return previous_beat + target * previous_bpm / 60.0
}
for index in 1.. Double {
self.at_beat(self.seconds_to_beat(seconds)).bpm
}
///|
pub fn TempoTimeline::quantize_beat(
_self : TempoTimeline,
beat : Double,
subdivision : Int,
) -> Double {
let grid = subdivision.max(1).to_double()
let scaled = beat * grid
let floor_value = @math.floor(scaled)
let ceil_value = floor_value + 1.0
let chosen = if scaled - floor_value < ceil_value - scaled {
floor_value
} else {
ceil_value
}
chosen / grid
}
///|
pub enum GroovePattern {
StraightGroove
SwingGroove(Double)
TripletGroove
CustomGroove(Array[Double])
} derive(Debug)
///|
pub fn groove_patterns() -> Array[GroovePattern] {
[
StraightGroove,
SwingGroove(0.5),
TripletGroove,
CustomGroove([0.0, 0.33, 0.66]),
]
}
///|
pub fn apply_groove(beat : Double, pattern : GroovePattern) -> Double {
let cell = @math.floor(beat)
let fraction = beat - cell
match pattern {
StraightGroove => beat
SwingGroove(amount) => {
let swing = clamp(amount, 0.5, 0.75)
if fraction < 0.5 {
cell + fraction * 2.0 * swing
} else {
cell + swing + (fraction - 0.5) * 2.0 * (1.0 - swing)
}
}
TripletGroove => cell + fraction * 1.5
CustomGroove(offsets) =>
if offsets.length() == 0 {
beat
} else {
let position = fraction * offsets.length().to_double()
let index = @math.floor(position).to_int().min(offsets.length() - 1)
cell +
offsets[index] +
(position - index.to_double()) / offsets.length().to_double()
}
}
}
///|
pub struct TimeSignature2 {
numerator : Int
denominator : Int
} derive(Debug)
///|
pub fn time_signature2(numerator : Int, denominator : Int) -> TimeSignature2 {
{ numerator: numerator.max(1), denominator: denominator.max(1) }
}
///|
pub fn TimeSignature2::numerator(self : TimeSignature2) -> Int {
self.numerator
}
///|
pub fn TimeSignature2::denominator(self : TimeSignature2) -> Int {
self.denominator
}
///|
pub fn TimeSignature2::bar_beats(self : TimeSignature2) -> Double {
self.numerator.to_double() * 4.0 / self.denominator.to_double()
}
///|
pub fn TimeSignature2::beat_in_bar(
self : TimeSignature2,
beat : Double,
) -> Double {
let bar = @math.floor(beat / self.bar_beats())
beat - bar * self.bar_beats()
}
///|
pub struct BeatCue {
beat : Double
label : String
duration : Double
strength : Double
} derive(Debug)
///|
pub fn beat_cue(
beat : Double,
label : String,
duration? : Double = 0.0,
strength? : Double = 1.0,
) -> BeatCue {
{ beat, label, duration: duration.max(0.0), strength: clamp01(strength) }
}
///|
pub fn BeatCue::beat(self : BeatCue) -> Double {
self.beat
}
///|
pub fn BeatCue::label(self : BeatCue) -> String {
self.label
}
///|
pub fn BeatCue::duration(self : BeatCue) -> Double {
self.duration
}
///|
pub fn BeatCue::strength(self : BeatCue) -> Double {
self.strength
}
///|
pub fn BeatCue::seconds(self : BeatCue, tempo : TempoTimeline) -> Double {
tempo.beat_to_seconds(self.beat)
}
///|
pub struct BeatGrid {
tempo : TempoTimeline
signature : TimeSignature2
groove : GroovePattern
} derive(Debug)
///|
pub fn beat_grid(tempo : TempoTimeline, signature : TimeSignature2) -> BeatGrid {
{ tempo, signature, groove: StraightGroove }
}
///|
pub fn BeatGrid::tempo(self : BeatGrid) -> TempoTimeline {
self.tempo
}
///|
pub fn BeatGrid::signature(self : BeatGrid) -> TimeSignature2 {
self.signature
}
///|
pub fn BeatGrid::groove(self : BeatGrid) -> GroovePattern {
self.groove
}
///|
pub fn BeatGrid::with_groove(
self : BeatGrid,
groove : GroovePattern,
) -> BeatGrid {
{ tempo: self.tempo, signature: self.signature, groove }
}
///|
pub fn BeatGrid::beat_to_seconds(self : BeatGrid, beat : Double) -> Double {
self.tempo.beat_to_seconds(apply_groove(beat, self.groove))
}
///|
pub fn BeatGrid::seconds_to_beat(self : BeatGrid, seconds : Double) -> Double {
self.tempo.seconds_to_beat(seconds)
}
///|
pub fn BeatGrid::bar_start(self : BeatGrid, bar : Int) -> Double {
bar.to_double() * self.signature.bar_beats()
}
///|
pub fn BeatGrid::bar_index(self : BeatGrid, beat : Double) -> Int {
@math.floor(beat / self.signature.bar_beats()).to_int()
}
///|
pub fn BeatGrid::quantize(
self : BeatGrid,
beat : Double,
subdivision : Int,
) -> Double {
self.tempo.quantize_beat(beat, subdivision)
}
///|
pub struct CueTimeline {
cues : Array[BeatCue]
grid : BeatGrid
} derive(Debug)
///|
pub fn CueTimeline::new(grid : BeatGrid) -> CueTimeline {
{ cues: [], grid }
}
///|
pub fn CueTimeline::add(self : CueTimeline, cue : BeatCue) -> Unit {
self.cues.push(cue)
self.cues.sort_by(fn(left, right) { left.beat.compare(right.beat) })
}
///|
pub fn CueTimeline::length(self : CueTimeline) -> Int {
self.cues.length()
}
///|
pub fn CueTimeline::cues(self : CueTimeline) -> Array[BeatCue] {
self.cues.copy()
}
///|
pub fn CueTimeline::grid(self : CueTimeline) -> BeatGrid {
self.grid
}
///|
pub fn CueTimeline::at_seconds(
self : CueTimeline,
seconds : Double,
) -> Array[BeatCue] {
let result : Array[BeatCue] = []
let beat = self.grid.seconds_to_beat(seconds)
for cue in self.cues {
if cue.beat <= beat && beat <= cue.beat + cue.duration {
result.push(cue)
}
}
result
}
///|
pub fn CueTimeline::next_after(
self : CueTimeline,
seconds : Double,
) -> BeatCue? {
for cue in self.cues {
if cue.seconds(self.grid.tempo) > seconds {
return Some(cue)
}
}
None
}
///|
pub fn CueTimeline::window(
self : CueTimeline,
start : Double,
end : Double,
) -> Array[BeatCue] {
let result : Array[BeatCue] = []
let first = self.grid.seconds_to_beat(start)
let last = self.grid.seconds_to_beat(end)
for cue in self.cues {
if cue.beat + cue.duration >= first && cue.beat <= last {
result.push(cue)
}
}
result
}