Friday, June 26, 2020

Musical Time in After Effects (BPM, beat, phase, loop)

Musical Time in After Effects

Much of modern music is generated to a strict metronome, or on a grid in a music editor. So if we known the precise BPM and where the first beat is, we can predict the exact location of every beat in the song, and extrapolate counters for bars, loops, and cycles.

Editing keyframes to music by hand is time-consuming and error-prone. It is easy enough to create a phrase of keyframes and add a loopOut() script. However the keyframes and the looping are limited by the accuracy of a single frame, and a frame is pretty large in musical time. You might have found yourself having to choose between the frame just before or just after a sound happens, searching for the best alignment. Now imagine that amount of slack multiplied tens or hundreds as the frame rounding errors accumulate every loop. At 90 BPM and 30fps, each beat is 20 frames. Assume half a frame of rounding error for a one-beat loop (imagine something throbbing with a 4/4 kick drum) and after a few seconds it is visibly out of sync. You can move to larger 8- and 16-bar loops, but the drift will still be ruinous by the end of the song.

A solution to this is to calculate (or be told) the BPM to 4 or 5 significant digits (e.g. 98.265). In a music editor you can visually and audibly verify that every single beat of the song is accurately located by this BPM. Then use some scripting in After Effects to calculate everything in floating point from this very accurate BPM.

"Music BPM.ffx" is a preset that does some basic calculations to provide accurate musical beat and bar count and phase. You can then pick-whip these and use simple expressions to design musical movements. Because everything is calculated in floating point from your very accurate BPM, everything will stay perfectly in sync.

Using the preset

Get a very accurate BPM value, and if the beat doesn't start at 0:00 also a frame-accurate time value for where the beat drops. Put the audio file in your project at 0:00, add the "Music BPM" preset. You will get the following fields:
  • input_BPM: put your very accurate BPM here
  • input_Downbeat: put here where the beat drops, in seconds
  • input_BeatsPerBar: most music works well with 4, but 8 or 16 might be better defaults for some music, or you can put odd numbers here for odd time signatures
  • BeatCount: outputs a beat counter, which starts negative and counts "down" to 0 when the downbeat happens, then up from there. You can use this to mark different sections of the song, and to calculate different cycles and rhythms;
  • BeatPhase: ramps from 0 to 1 every beat
  • BarPhase: ramps from 0 to 1 every bar
  • BarBeats: ramps from e.g. 0 to 3 every bar. Sometimes easier to think about than pure phase.

Some recipes

Throb with the beat

Add a size expression to your target layer. Pick-whip the "BarPhase" slider, and edit the expression to the following. Use linear() to set the limits for the response, because
a) size is on a scale of 100 while phase is on scale of 1
b) it looks bad to go all the way to 0, pick tasteful endpoints
c) we want to map the beginning of the phase (0) to a larger scale.
In this case we need to convert a 1D value to 2D to control the size so we [s, s] at the end.

s = linear(thisComp.layer("song").effect("BeatPhase")("Slider"), 120, 100); [s, s]

Punchy Throb

"1 - phase" to get a ramp down from 1 to 0, then take it to the third power so that values less than 1 get pulled down quickly towards 0. Since we already have a ramp down, we don't need to swap the mix and max arguments to linear(). This gives us a curve that stays mostly near 100 with a strong punch to 120 on the beat. If instead we take pow(BeatPhase) and use linear(s, 120, 100), the curve would spend most of its time near 120, only falling off towards 100 right before the next beat hits. Two different vibes, each good in their own way.

s = Math.pow(1-thisComp.layer("song").effect("BeatPhase")("Slider"), 3);
s = linear(s, 100, 120);
[s, s]

alternate

s = Math.pow(thisComp.layer("song").effect("BeatPhase")("Slider"), 3);
s = linear(s, 120, 100);
[s, s]

Rotate once per bar

360 * thisComp.layer("song").effect("BarPhase")("Slider") 

Rock back and forth with the beat, smoothly

Use cos() so it starts at an extreme (sin will start centered) and -10 so it starts at the left.
-10 * Math.cos(2*Math.PI*thisComp.layer("song").effect("BarPhase")("Slider") )

Rock back and forth with the beat, jumping from left lean to right lean

Use the (condition ? true_value : false_value) ternary operator to pick one of two values. In this case sin() is a better fit for the timing we want.
-10 * (Math.sin(2*Math.PI*thisComp.layer("song").effect("BarPhase")("Slider") ) > 0 ? 1 : -1)

Change color with the beat

Modulate the BeatCount to within the range (0-4]. This ends up being the same as BarPhase, but for more complicated motions you will often end up working directly with BeatCount so that's what we do here. Use this number to cycle white-yellow-red-black at full opacity. I decided that I didn't want the colors to flash during the intro, so I added some logic to keep the color black until the beat dropped. For a more complicated song, I will use an expression controller named "HasBeat" and keyframe it on and off as the drums come in and out, so I can pick-whip it into expressions to disable them when there is no beat.

beatCount = thisComp.layer("song").effect("BeatCount")("Slider");
beatMod = (beatCount % 4 + 4) % 4;
c = [0, 0, 0, 1];
if (beatCount >= 0) {
  if        (beatMod < 1) { c = [1,1,1, 1]; }
  else if (beatMod < 2) { c = [1,1,0, 1]; }
  else if (beatMod < 3) { c = [1,0,0, 1]; }
}
c

Draw a custom keyframe curve, and have it repeat in perfect time

In this case the song had a 16-bar piano loop, and I wanted to respond to a few swells without the jitter of converting the audio directly to keyframes. I created an layer "Piano Curve" and made sure I had the piano loop lined up at 0:00 on the timeline. Looking at the waveform and listening to the loop, I keyframed the size to swell with the piano line. I then hid the "Piano Swell" layer, deleted the audio layer I had used as a guide, and put the following expression on my target layer.

loopBeats = 16;
loopSec = loopBeats * 60 / thisComp.layer("song").effect("input_BPM")("Slider");
downbeat = thisComp.layer("song").effect("input_Downbeat")("Slider");
loopedTime = ((time-downbeat) % loopSec + loopSec) % loopSec;
thisComp.layer("Piano Curve").transform.scale.valueAtTime(loopedTime)



Notes

% is remainder, and will go negative for negative numbers. For modulo, use (x % N + N) %N;

beatLen = 60 / bpm


No comments: