Skip to main content

Crate taktus

Crate taktus 

Source
Expand description

A beat, by ear: the tempo of music and where its beats fall, from the sound itself, for almost nothing.

Sound goes in sample by sample (Ear) and comes out as onsets: “how much just started”, a hundred values a second (RATE). Onsets are all the rest of the crate looks at:

  • beat_of reads the beat of a few seconds of them (8 to 16 is its range);
  • tempo_of reads a long stretch, half a minute or a whole song;
  • beats_of gives every beat of a stretch already heard;
  • Follower follows a song as it plays, and is the frugal way to use all of this live: it listens until two readings agree, then only looks again now and then, and each look says more exactly how long a beat is.

No dependency, no unsafe, no allocation while listening, nothing of the sound kept.

§A whole recording

use taktus::{beats_of, tempo_of, Ear};

// Twelve seconds of a drum twice a second: 120 beats a minute, at 44.1 kHz.
let rate = 44_100;
let sound: Vec<f32> = (0..rate * 12)
    .map(|i| {
        let since = (i % (rate / 2)) as f32 / rate as f32;
        if since < 0.05 { (since * 600.0).sin() * (1.0 - since / 0.05) } else { 0.0 }
    })
    .collect();

// Mono samples in, onsets out.
let mut ear = Ear::new(rate as u32);
let onsets: Vec<f32> = sound.iter().filter_map(|sample| ear.hear(*sample)).collect();

let beat = tempo_of(&onsets).expect("a beat");
assert!((beat.bpm() - 120.0).abs() < 1.0);

// Every beat, in seconds from the start of the sound.
let beats = beats_of(&onsets, beat.period);
assert!(beats.len() >= 20);

§A song as it plays

use taktus::{Ear, Follower};

let mut ear = Ear::new(rate as u32);
let mut follower = Follower::default();
let mut heard = Vec::new();
// Sound arrives a tenth of a second at a time; `now` is in seconds, on any clock.
for (i, chunk) in sound.chunks(rate / 10).enumerate() {
    let now = (i + 1) as f64 * 0.1;
    // While the follower rests the sound is not wanted: its source can be closed.
    if !follower.listens(now) {
        ear = Ear::new(rate as u32);
        continue;
    }
    heard.clear();
    heard.extend(chunk.iter().filter_map(|sample| ear.hear(*sample)));
    if follower.hear(&heard, now) {
        // The beat changed: seconds from one beat to the next, and the time of one.
        if let Some((period, beat)) = follower.beat() {
            println!("{:.1} beats a minute, one of them at {beat:.2} s", 60.0 / period);
        }
    }
}
let (period, _) = follower.beat().expect("a beat");
assert!((60.0 / period - 120.0).abs() < 1.0);

§What the numbers mean

A Beat says how far to trust it. sure is how well the onsets keep its period, from 0 to 1; catch is how much of what starts, starts on its beats, where 1 is no more than anywhere else and sound with no beat in it catches about 0.7. Readings with sure at 0.05 or more and catch at 1.0 or more were right 97 to 99 times in 100 on the recordings the crate was measured on, counting a tempo read at half or double as right; the repository’s README has every figure, and what they do not show.

A tempo and its half or double are the same beats counted at another level, and which level people call the tempo is partly convention. The crate looks from 60 to 200 beats a minute and leans towards 135.

Sound is mono: mix the channels of each frame into one sample before Ear::hear. Any rate from 8 kHz up will do; nothing above 6 kHz is used.

Structs§

Beat
A beat found in a stretch of onsets.
Ear
Listens: sound in, sample by sample; out, every 10 ms, how much just started — the rise of the spectrum, band by band, on a log scale.
Follower
What is made of a song as it plays. Times are seconds on the caller’s clock, whichever it is, as long as it only goes forward.

Constants§

LEAD
How long after the sound begins the ear gives its first value, seconds (for sound at 11 kHz and above): a first look of 43 ms, then three more before it has something to measure a rise against.
RATE
Onset values a second: one every 10 ms.

Functions§

beat_of
The beat in onsets (one value per 10 ms, the newest last), if they have one.
beats_at
Where beats of a known period (in seconds) fall in onsets: how much they catch, as Beat::catch, and how many seconds before the end of the stretch the last one fell.
beats_of
Every beat of a stretch heard from the start of the sound, in seconds from that start, for beats about period seconds apart: tempo_of gives the period, and half or double it counts the same music at another level. Of all the ways of stepping through the onsets about one period at a time, the one that lands on the most that starts while keeping its steps even (after Ellis, 2007). It follows a tempo that drifts, and it is for a stretch already heard: a song as it plays is the Follower’s.
tempo_of
The beat of a long stretch — half a minute, a whole song. Many readings of sixteen seconds each are pooled, since a tempo that wavers blurs one long reading but not the vote of short ones; the long reading still says which level is the beat when it is that pulse’s own.