Calculate Beats

Calculate Beats

Some high level parts of the timeline, such as unmetered Hits, Spans, or Actions use time in absolute seconds to specify when they occur. However, most other areas use beats because it allows events to be more easily coordinated in “musical time,” taking into account tempo, meter, phrasing, etc. There are often times when it is useful to to know what beat is at a given time and vice versa.

What Is Tempo?

All tempo information in the timeline is provided in beats per minute (BPM), which is the standard unit for tempo. BPM (and tempo in general) measures speed.

Although BPM is the natural unit for talking about tempo, when doing calculations, it is far easier to work with beats per second (BPS). The reason is that 60 BPM = 1 BPS and this prevents needing to divide every result by 60.

Note: It is important to keep in mind that since tempo is a rate, you are often working with the difference (delta) between two points in time, and not necessarily absolute time.

The Anatomy of Tempo

Tempo information lives in two places in the timeline.

First is the tempo key of a metered Span. This is the initial tempo and will always be present since metered Spans by definition must have a tempo. When creating a timeline, it is possible to omit the initial tempo, in which case an appropriate one will be chosen for you. However, any timeline returned from creating a Composition (see Composition vs. Render) is guaranteed to have a tempo value.

The second place where tempo information is defined is in the tempo_changes key of a metered Span. Where tempo defines the initial value, tempo_changes provides a list of times at which the tempo changes. Starting from the initial tempo, which is at the time of the Span, the tempo at any given time is linearly interpolated between two points in time: the one at or before the time in question, and the one at a time after.

Note: While these are the only places where persistent information about tempo exists, it is important to remember that some Actions can alter tempo (specifically add_region, copy_region, copy_tempo_from_time, ramp_tempo, and set_tempo). If you are using Actions that alter tempo and you are also interested in calculating beats, you will be required to create a Composition and do your calculations based on the resulting timeline, especially if you are asking for any random tempo.

Conversions

Here are the basic steps for finding the beat at the next time (in pseudocode):

Beat find_target_beat(BPM bpm, Second start_time, Second target_time, Beat start_beat) {
    // convert to BPS
    bps = bpm / 60

    // calculate the change in time between the start and target second
    delta_time = target_time - start_time

    // calculate the change in beats based on the tempo
    delta_beats = delta_time * bps

    // calculate the next beat
    target_beat = start_beat + delta_beats

    return target_beat
}

where

Note: The only piece of information for the above function that is not explicitly in a timeline is start_beat. The beat at the start of any Span is always 0.0!

By using this, you can find any beat at any time after any beat which you already know the time of.

There are also times when it is useful to convert a beat into a time. For instance, since anything inside a Span is defined in terms of beats, you may wish to display that event in relation to other things which are defined in seconds.

Here are the basic steps for finding the time at the next beat (in pseudocode):

Second find_target_time(BPM bpm, Beat start_beat, Beat target_beat, Second start_time) {
    // convert to BPS
    bps = bpm / 60

    // convert to seconds per beat (SPB)
    spb = 1 / bps

    // calculate the change in beats between the start and target
    delta_beats = target_beat - start_beat

    // calculate the change in beats based on the tempo
    delta_time = delta_beats * spb

    // calculate the next beat
    target_time = start_time + delta_time

    return target_time
}

where

By using either find_target_beat and find_target_time you can freely convert seconds to beats and beats to seconds.

Constant Tempo

The simplest, and most common, situation to calculate is a constant tempo. If you are manually setting the tempo key of your metered Span and not using any tempo_changes, then the BPM at any time will always be the same. In this case (using the variable names from the above pseudocode):

With that, every target_time can be easily converted it to a target_beat and vice versa for anywhere in the Span.

Abrupt Tempo Changes

You may want to create sudden dramatic tempo changes that make beat/time calculations more complicated. The main difficulty is that, unlike with a constant tempo, the bpm, start_time, and start_beat values are different every time the tempo changes.

When finding the beats of events in a timeline, you already know the bpm and start_time because they will either be the tempo and time of the Span or of the Tempo Change at or before the second in question. What you do not know directly is the start_beat. As with a constant tempo, the beat at the start of any Span is always 0.0, but in order to know the start_beat for any Tempo Change, you must calculate the beat at the time of it. Since you can only calculate beats at times when the BPM is constant, this means that you must always start from the beginning of the Span and calculate the beat for every Tempo Change before you can do anything else.

Tempo Ramps

The last situation that can happen is gradually changing from one tempo to an other. This is useful for creating smooth transitions between seconds, adding expressive speedups and slowdowns, etc. It does, however, add one more level of complication when calculating beats.

The tempo between any two Tempo Changes (or between the start of the Span and the first Tempo Change) is linearly interpolated between the times of the changes. This means that, unlike with abrupt changes, the BPM at any given time between Tempo Changes is changing.

Here are the basic steps for finding the BPM at any second (in pseudocode):

BPM interpolated_bpm(BPM start_bpm, BPM end_bpm, Second start_time, Second end_time, Second target_time) {
    // calculate the change in BPM
    delta_bpm = end_bpm - start_bpm

    // calculate the change in time
    delta_time = end_time - start_time

    // calculate the slope of the line
    slope = delta_bpm / delta_time

    // y = mx+b
    target_bpm = slope * target_time + start_bpm

    return target_bpm
}

where

Most of the information needed for these calculations comes from Tempo Changes. The two special cases are:

The only other question is “how often should the BPM be calculated?” This depends on how accurately you wish to represent the mapping between beats and seconds. Typically 8 times per second is a good balance between accuracy and effort.

Note: Since the constant and abrupt cases are subsets of what is required to support ramping tempo, if you ever plan on supporting all three it may make sense to treat every case as if it contains ramps.

Tempo Memoization

Since converting between beats and seconds always requires knowing the same information, and only altering Tempo Changes has any effect on that, it sometimes makes sense to calculate the arguments required for all other conversions when you receive a timeline and store it for later (at least until receiving a new timeline or altering any Tempo Changes). By storing the (second,beat,BPM) triple from every time the BPM was sampled, you can easily create a lookup for the (second,BPM) at any beat or the (bead,BPM) at any second for all events that altered the tempo, which is everything you need to convert between beats and seconds.