mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
218 lines
7.7 KiB
Python
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')
|