Files
MobileGL/intro-video/hype/score.py
T

218 lines
7.7 KiB
Python

"""The arrangement both the music and the picture are cut to: tempo grid, sections, harmony, melodies
and every drum / fx event. The video reads the same events for its beat pulses, so they never drift."""
import math
BPM = 128.0
BEAT = 60.0 / BPM # 0.46875 s
BAR = 4 * BEAT # 1.875 s
S16 = BEAT / 4
NBARS = 112
END = NBARS * BAR + 3.5 # music + reverb tail (213.5 s)
SECTIONS = [ # name, first bar, end bar
('intro', 0, 8), ('build1', 8, 16), ('drop1', 16, 32), ('verse', 32, 48), ('build2', 48, 56),
('drop2', 56, 72), ('break', 72, 80), ('build3', 80, 88), ('drop3', 88, 104), ('outro', 104, 112),
]
def tb(bar, beat=0.0, s16=0.0):
"""Time of bar + beat + sixteenth, in seconds."""
return bar * BAR + beat * BEAT + s16 * S16
def section(bar):
for name, a, b in SECTIONS:
if a <= bar < b:
return name, a, b
return 'end', NBARS, NBARS + 4
def key_shift(bar):
return 2 if bar >= 88 else 0 # truck-driver lift into the last drop
# --- harmony ---------------------------------------------------------------------------------------
A, F, C, G, Em = 'Am', 'F', 'C', 'G', 'Em'
CHORDS = { # close voicings around middle C (midi), root for the bass
'Am': ((57, 60, 64), 45), 'F': ((57, 60, 65), 41), 'C': ((55, 60, 64), 48),
'G': ((55, 59, 62), 43), 'Em': ((55, 59, 64), 40), 'Dm': ((57, 62, 65), 38),
}
MAIN = [A, F, C, G]
VERSE = [F, G, Em, A]
def chord_name(bar):
name, a, _ = section(bar)
i = bar - a
if name == 'verse':
return VERSE[i % 4]
if name == 'break':
return [F, F, G, G, A, A, F, G][i]
if name == 'outro':
return [A, F, C, G, C, C, C, C][i]
if name == 'end':
return C
return MAIN[i % 4]
def chord(bar):
"""(voicing, bass root) for the bar, transposed by the key lift."""
v, r = CHORDS[chord_name(bar)]
k = key_shift(bar)
return tuple(n + k for n in v), r + k
# --- melodies (16th-step grid: (step, length, midi)) ------------------------------------------------
HOOK = [
[(0, 2, 76), (3, 1, 76), (4, 2, 74), (6, 2, 76), (8, 3, 81), (11, 2, 79), (14, 2, 76)], # Am
[(0, 2, 77), (3, 1, 76), (4, 2, 74), (6, 4, 72), (10, 2, 74), (12, 4, 76)], # F
[(0, 2, 79), (3, 1, 79), (4, 2, 77), (6, 2, 76), (8, 3, 76), (11, 2, 72), (14, 2, 74)], # C
[(0, 6, 74), (6, 2, 71), (8, 4, 74), (12, 2, 76), (14, 2, 79)], # G
]
HOOK_END = [(0, 3, 74), (3, 3, 71), (6, 2, 74), (8, 8, 69)] # G, resolving
def hook_bar(i):
"""Bar i (0..7) of the 8-bar lead phrase."""
return HOOK_END if i % 8 == 7 else HOOK[i % 4]
def notes_in(bar, pattern, shift=0):
k = key_shift(bar)
return [(tb(bar, 0, s), l * S16, m + k + shift) for s, l, m in pattern]
# --- events ----------------------------------------------------------------------------------------
def _events():
ev = [] # (time, kind, vel)
def add(t, kind, vel=1.0):
ev.append((t, kind, vel))
for bar in range(NBARS):
name, a, b = section(bar)
i = bar - a
last = bar == b - 1
if name == 'intro':
if i >= 4:
for s in range(2, 16, 4):
add(tb(bar, 0, s), 'hat', 0.5 + 0.1 * (i - 4))
if i == 0:
add(tb(bar), 'impact', 0.5)
elif name == 'build1':
for q in range(4):
if not (last and q == 3):
add(tb(bar, q), 'kick', 0.7 if i < 6 else 0.78)
add(tb(bar, q, 2), 'hat', 0.7)
for q in (1, 3):
if not (last and q == 3):
add(tb(bar, q), 'clap', 0.65)
elif name in ('drop1', 'drop2', 'drop3'):
hot = {'drop1': 1.0, 'drop2': 1.0, 'drop3': 1.0}[name]
fill = last or i == 7
for q in range(4):
add(tb(bar, q), 'kick', hot)
add(tb(bar, q, 2), 'ohat', 0.8)
for q in (1, 3):
add(tb(bar, q), 'clap', 1.0)
for s in range(16):
if s % 4 != 2:
add(tb(bar, 0, s), 'hat', 0.45 + 0.25 * (s % 2 == 0))
if i % 8 == 0:
add(tb(bar), 'crash', 1.0)
if i == 14: # climb: 8th-note snares, then 16ths in the last bar
for s in range(0, 16, 2):
add(tb(bar, 0, s), 'snare', 0.35 + 0.02 * s)
elif last:
for s in range(12):
add(tb(bar, 0, s), 'snare', 0.55 + 0.035 * s)
elif fill:
for s in (12, 13, 14, 15):
add(tb(bar, 0, s), 'snare', 0.6 + 0.1 * (s - 12))
if last:
add(tb(bar, 3), 'stutter', 1.0)
elif name == 'verse':
for q in range(4):
add(tb(bar, q), 'kick', 0.8)
for q in (1, 3):
add(tb(bar, q), 'clap', 0.75)
for s in range(16):
add(tb(bar, 0, s), 'shaker', 0.5 + 0.3 * (s % 4 == 2))
if i % 4 == 3:
add(tb(bar, 3, 2), 'snare', 0.5)
add(tb(bar, 3, 3), 'snare', 0.6)
if i == 8:
add(tb(bar), 'crash', 0.7)
elif name in ('build2', 'build3'):
n = 8
for q in range(4):
if i < 6:
add(tb(bar, q), 'kick', 0.7)
# snare roll: quarters -> 8ths -> 16ths -> 32nds, rising
div = 1 if i < 2 else 2 if i < 4 else 4 if i < 6 else 8
steps = 4 * div
for s in range(steps):
if last and s >= steps * 3 // 4:
break # one beat of silence before the drop
prog = (i + s / steps) / n
add(tb(bar, s * 4 / steps), 'roll', 0.35 + 0.65 * prog)
if i == 0:
add(tb(bar), 'crash', 0.8)
elif name == 'break':
if i == 0:
add(tb(bar), 'impact', 0.8)
if i >= 4:
for q in (0, 2):
add(tb(bar, q), 'heart', 0.5 + 0.1 * (i - 4))
elif name == 'outro':
if i < 4:
for q in range(4):
add(tb(bar, q), 'kick', 1.0 - 0.15 * i)
add(tb(bar, q, 2), 'ohat', 0.7 - 0.1 * i)
for q in (1, 3):
add(tb(bar, q), 'clap', 0.9 - 0.15 * i)
if i == 0:
add(tb(bar), 'crash', 1.0)
if i == 4:
add(tb(bar), 'impact', 1.0)
add(tb(bar), 'crash', 1.0)
add(tb(bar), 'kick', 1.0)
for b in (16, 56, 88): # drops: impact + reverse swell into them
add(tb(b), 'impact', 1.0)
add(tb(b), 'drop', 1.0)
for a, b in ((4, 8), (12, 16), (48, 56), (80, 88), (30, 32), (70, 72), (102, 104)):
add(tb(a), 'riser', tb(b) - tb(a))
return sorted(ev)
EVENTS = _events()
def events(kind):
return [(t, v) for t, k, v in EVENTS if k == kind]
def pulse(t, kind, decay=0.12, kinds_cache={}):
"""Exponentially decaying pulse from the most recent event of kind (0..1 * vel)."""
ts = kinds_cache.get(kind)
if ts is None:
ts = kinds_cache[kind] = events(kind)
best = 0.0
lo, hi = 0, len(ts)
while lo < hi: # last event <= t
mid = (lo + hi) // 2
if ts[mid][0] <= t:
lo = mid + 1
else:
hi = mid
if lo:
te, v = ts[lo - 1]
best = v * math.exp(-(t - te) / decay)
return best
if __name__ == '__main__':
print(f'{NBARS} bars, {NBARS * BAR:.2f}s + tail -> {END:.2f}s, {len(EVENTS)} events')
for name, a, b in SECTIONS:
print(f'{name:8s} bars {a:3d}-{b:3d} {tb(a):7.2f}s - {tb(b):7.2f}s')