|
|
|
@@ -0,0 +1,261 @@
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""I segnaposto delle console dell'Ingorgo Sonoro, sintetizzati da zero: quattro loop da sedici battute, uno per genere.
|
|
|
|
|
Nel gioco ora suonano i brani di Andrea (Suno, 30 ottobre: assets/raw/musica/ingorgo-*-suno.mp3, portati al volume
|
|
|
|
|
del gioco in assets/audio/ingorgo-*.mp3); questi restano in assets/raw/musica/ingorgo-*-sintesi.mp3, per prova.
|
|
|
|
|
python3 tools/ingorgo_musiche.py # tutti e quattro (serve ffmpeg)
|
|
|
|
|
python3 tools/ingorgo_musiche.py house # uno solo
|
|
|
|
|
Generi: latino (dembow e piano montuno), house (cassa dritta, accordi d'organo in levare), techno (cassa dura,
|
|
|
|
|
linea acida che si apre), underground (minimale scuro, basso che rotola e colpi lontani).
|
|
|
|
|
"""
|
|
|
|
|
import math
|
|
|
|
|
import random
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
import wave
|
|
|
|
|
from array import array
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
SR = 22050
|
|
|
|
|
AUDIO = Path(__file__).resolve().parent.parent / "assets" / "raw" / "musica"
|
|
|
|
|
NAMES = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def hz(note: str) -> float:
|
|
|
|
|
semis = NAMES[note[0]] + (1 if "#" in note else -1 if len(note) > 2 and note[1] == "b" else 0)
|
|
|
|
|
return 440 * 2 ** ((semis + 12 * (int(note[-1]) + 1) - 69) / 12)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Track:
|
|
|
|
|
def __init__(self, bpm: int, bars: int = 16, seed: int = 1):
|
|
|
|
|
self.beat = 60 / bpm
|
|
|
|
|
self.buf = [0.0] * int(SR * bars * 4 * self.beat)
|
|
|
|
|
self.rng = random.Random(seed)
|
|
|
|
|
self.cache = {}
|
|
|
|
|
|
|
|
|
|
def add(self, beat: float, samples):
|
|
|
|
|
# Il loop gira: quello che sborda oltre la fine rientra dall'inizio.
|
|
|
|
|
n, i0 = len(self.buf), int(beat * self.beat * SR)
|
|
|
|
|
for i, x in enumerate(samples):
|
|
|
|
|
self.buf[(i0 + i) % n] += x
|
|
|
|
|
|
|
|
|
|
def once(self, key, make):
|
|
|
|
|
if key not in self.cache:
|
|
|
|
|
self.cache[key] = make()
|
|
|
|
|
return self.cache[key]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- I suoni: la batteria si calcola una volta e si timbra, le note una per una.
|
|
|
|
|
def kick(vol=0.9, top=150.0, bottom=45.0, length=0.32, punch=0.03):
|
|
|
|
|
out, ph = [], 0.0
|
|
|
|
|
for i in range(int(length * SR)):
|
|
|
|
|
t = i / SR
|
|
|
|
|
f = bottom + (top - bottom) * math.exp(-t / punch)
|
|
|
|
|
ph += 2 * math.pi * f / SR
|
|
|
|
|
out.append(vol * math.exp(-t / (length / 3.2)) * math.sin(ph))
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def noise(rng, vol, length, decay, bright=True):
|
|
|
|
|
out, prev = [], 0.0
|
|
|
|
|
for i in range(int(length * SR)):
|
|
|
|
|
x = rng.uniform(-1, 1)
|
|
|
|
|
y = x - prev if bright else (prev + x) * 0.5 # differenza: solo alti; media: più corpo
|
|
|
|
|
prev = x
|
|
|
|
|
out.append(vol * y * math.exp(-i / SR / decay))
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clap(rng, vol=0.35):
|
|
|
|
|
# Tre colpi ravvicinati di rumore e una coda: il battimani.
|
|
|
|
|
out = [0.0] * int(0.25 * SR)
|
|
|
|
|
for k, d in ((0, 0.006), (0.011, 0.006), (0.022, 0.07)):
|
|
|
|
|
for i, x in enumerate(noise(rng, vol, 0.2, d, bright=False)):
|
|
|
|
|
j = int(k * SR) + i
|
|
|
|
|
if j < len(out):
|
|
|
|
|
out[j] += x
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def drum(freq, vol, decay, bend=0.0):
|
|
|
|
|
# Bongo, conga, tom: un seno che cala un poco.
|
|
|
|
|
out, ph = [], 0.0
|
|
|
|
|
for i in range(int(decay * 5 * SR)):
|
|
|
|
|
t = i / SR
|
|
|
|
|
ph += 2 * math.pi * freq * (1 + bend * math.exp(-t / 0.02)) / SR
|
|
|
|
|
out.append(vol * math.exp(-t / decay) * math.sin(ph))
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def synth(freq, length, vol, wave_="saw", cutoff=1200.0, sweep=0.0, decay=9.0, attack=0.004, release=0.03, detune=0.0, res=0.0):
|
|
|
|
|
"""Un oscillatore (sega o quadra, o due appena stonati) dentro un filtro passa basso a due poli, che può aprirsi
|
|
|
|
|
e chiudersi (`sweep`, in Hz verso cui va il taglio durante la nota). `res` dà il fischio dell'acido."""
|
|
|
|
|
n = int((length + release) * SR)
|
|
|
|
|
out, p1, p2 = [], 0.0, 0.0
|
|
|
|
|
lo = bp = 0.0
|
|
|
|
|
for i in range(n):
|
|
|
|
|
t = i / SR
|
|
|
|
|
env = min(1.0, t / attack) * math.exp(-t / decay)
|
|
|
|
|
if t > length:
|
|
|
|
|
env *= max(0.0, 1 - (t - length) / release)
|
|
|
|
|
p1 = (p1 + freq / SR) % 1
|
|
|
|
|
p2 = (p2 + freq * (1 + detune) / SR) % 1
|
|
|
|
|
if wave_ == "square":
|
|
|
|
|
x = (1 if p1 < 0.5 else -1) * 0.7
|
|
|
|
|
else:
|
|
|
|
|
x = (2 * p1 - 1) + ((2 * p2 - 1) if detune else 0)
|
|
|
|
|
x *= 0.5 if detune else 1
|
|
|
|
|
cut = cutoff + sweep * min(1.0, t / max(length, 1e-3))
|
|
|
|
|
f = 2 * math.sin(math.pi * min(cut, SR / 6) / SR)
|
|
|
|
|
q = 1 - res
|
|
|
|
|
lo += f * bp
|
|
|
|
|
hi = x - lo - q * bp
|
|
|
|
|
bp += f * hi
|
|
|
|
|
out.append(vol * env * lo)
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sine(freq, length, vol, decay=9.0, attack=0.004, release=0.05, partials=((1, 1.0),)):
|
|
|
|
|
n = int((length + release) * SR)
|
|
|
|
|
out = []
|
|
|
|
|
for i in range(n):
|
|
|
|
|
t = i / SR
|
|
|
|
|
env = min(1.0, t / attack) * math.exp(-t / decay)
|
|
|
|
|
if t > length:
|
|
|
|
|
env *= max(0.0, 1 - (t - length) / release)
|
|
|
|
|
out.append(vol * env * sum(a * math.sin(2 * math.pi * freq * k * t) for k, a in partials))
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- I quattro generi.
|
|
|
|
|
def house():
|
|
|
|
|
tr = Track(124, seed=2)
|
|
|
|
|
K, H, C = kick(), tr.once("h", lambda: noise(tr.rng, 0.16, 0.08, 0.025)), clap(tr.rng, 0.32)
|
|
|
|
|
OH = tr.once("oh", lambda: noise(tr.rng, 0.12, 0.25, 0.09))
|
|
|
|
|
chords = [["A3", "C4", "E4", "G4"], ["F3", "A3", "C4", "E4"], ["D3", "F3", "A3", "C4"], ["E3", "G#3", "B3", "D4"]]
|
|
|
|
|
roots = ["A1", "F1", "D2", "E1"]
|
|
|
|
|
for bar in range(16):
|
|
|
|
|
b0, ch, root = bar * 4, chords[bar // 2 % 4], roots[bar // 2 % 4]
|
|
|
|
|
for k in range(4):
|
|
|
|
|
tr.add(b0 + k, K)
|
|
|
|
|
tr.add(b0 + k + 0.5, OH)
|
|
|
|
|
for s in (0.25, 0.75):
|
|
|
|
|
tr.add(b0 + k + s, H)
|
|
|
|
|
for k in (1, 3):
|
|
|
|
|
tr.add(b0 + k, C)
|
|
|
|
|
# Gli accordi d'organo in levare, corti, il cuore della house.
|
|
|
|
|
for s in (0.5, 1.5, 2.75, 3.5):
|
|
|
|
|
for note in ch:
|
|
|
|
|
tr.add(b0 + s, synth(hz(note), tr.beat * 0.22, 0.05, cutoff=2600, decay=0.25, detune=0.006))
|
|
|
|
|
# Il basso: in levare, che salta d'ottava.
|
|
|
|
|
for s, octv in ((0.5, 1), (1.5, 2), (2.5, 1), (3.25, 1), (3.5, 2)):
|
|
|
|
|
tr.add(b0 + s, synth(hz(root) * octv, tr.beat * 0.3, 0.32, wave_="square", cutoff=500, decay=0.3))
|
|
|
|
|
return tr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def techno():
|
|
|
|
|
tr = Track(132, seed=3)
|
|
|
|
|
K = kick(1.0, top=170, bottom=48, length=0.36, punch=0.022)
|
|
|
|
|
H = tr.once("h", lambda: noise(tr.rng, 0.13, 0.05, 0.012))
|
|
|
|
|
R = tr.once("r", lambda: noise(tr.rng, 0.1, 0.6, 0.25, bright=False)) # il ride lungo, nella seconda metà
|
|
|
|
|
line = ["A1", "A1", "A2", "A1", "C2", "A1", "G2", "A1", "A1", "E2", "A1", "A2", "A1", "C2", "D2", "A1"]
|
|
|
|
|
for bar in range(16):
|
|
|
|
|
b0 = bar * 4
|
|
|
|
|
for k in range(4):
|
|
|
|
|
tr.add(b0 + k, K)
|
|
|
|
|
tr.add(b0 + k + 0.5, H)
|
|
|
|
|
if bar >= 8:
|
|
|
|
|
tr.add(b0 + k + 0.5, R)
|
|
|
|
|
if bar % 4 == 3:
|
|
|
|
|
tr.add(b0 + 3.75, clap(tr.rng, 0.25))
|
|
|
|
|
# La linea acida: sedicesimi, il filtro che si apre battuta dopo battuta e poi richiude.
|
|
|
|
|
open_ = 300 + 1700 * (0.5 - 0.5 * math.cos(2 * math.pi * bar / 16))
|
|
|
|
|
for s, note in enumerate(line):
|
|
|
|
|
if s % 4 == 0 or tr.rng.random() < 0.8:
|
|
|
|
|
tr.add(b0 + s / 4, synth(hz(note), tr.beat * 0.2, 0.2, wave_="saw", cutoff=open_, sweep=-open_ * 0.6, decay=0.15, res=0.85))
|
|
|
|
|
return tr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def underground():
|
|
|
|
|
tr = Track(122, seed=4)
|
|
|
|
|
K = kick(0.95, top=110, bottom=42, length=0.42, punch=0.04)
|
|
|
|
|
H = tr.once("h", lambda: noise(tr.rng, 0.09, 0.04, 0.01))
|
|
|
|
|
RIM = drum(1700, 0.12, 0.012)
|
|
|
|
|
for bar in range(16):
|
|
|
|
|
b0 = bar * 4
|
|
|
|
|
for k in range(4):
|
|
|
|
|
tr.add(b0 + k, K)
|
|
|
|
|
for s in (0.5, 0.75):
|
|
|
|
|
tr.add(b0 + k + s, H)
|
|
|
|
|
for s in (1.75, 3.25) if bar % 2 else (2.75,):
|
|
|
|
|
tr.add(b0 + s, RIM)
|
|
|
|
|
# Il basso che rotola sui sedicesimi in levare, sempre la stessa nota: ipnotico.
|
|
|
|
|
for k in range(4):
|
|
|
|
|
for s in (0.25, 0.5, 0.75):
|
|
|
|
|
tr.add(b0 + k + s, synth(hz("F1"), tr.beat * 0.16, 0.24, cutoff=260, decay=0.12))
|
|
|
|
|
# Un blip lontano ogni tanto, con l'eco.
|
|
|
|
|
if bar % 2 == 1:
|
|
|
|
|
note = ["C5", "Ab4", "Eb5", "F4"][bar // 2 % 4]
|
|
|
|
|
for rep, v in ((0, 0.07), (0.75, 0.04), (1.5, 0.02)):
|
|
|
|
|
tr.add(b0 + 2.5 + rep, sine(hz(note), 0.06, v, decay=0.08, partials=((1, 1), (3, 0.2))))
|
|
|
|
|
# Il pad scuro che respira sotto, ogni quattro battute.
|
|
|
|
|
if bar % 4 == 0:
|
|
|
|
|
for note in ("F2", "Ab2", "C3"):
|
|
|
|
|
tr.add(b0, synth(hz(note), tr.beat * 15, 0.03, cutoff=420, sweep=300, decay=99, attack=1.5, release=1.0, detune=0.004))
|
|
|
|
|
return tr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def latino():
|
|
|
|
|
tr = Track(98, seed=5)
|
|
|
|
|
K = kick(0.85, top=130, bottom=50, length=0.3)
|
|
|
|
|
SN = tr.once("sn", lambda: [a + b for a, b in zip(noise(tr.rng, 0.22, 0.15, 0.05, bright=False), drum(220, 0.18, 0.05) + [0.0] * 9999)])
|
|
|
|
|
H = tr.once("h", lambda: noise(tr.rng, 0.08, 0.05, 0.015))
|
|
|
|
|
BONGO = (drum(380, 0.16, 0.06, bend=0.3), drum(520, 0.13, 0.05, bend=0.3))
|
|
|
|
|
CONGA = drum(210, 0.2, 0.12, bend=0.2)
|
|
|
|
|
chords = [["A3", "C4", "E4"], ["D4", "F4", "A4"], ["E3", "G#3", "B3"], ["A3", "C4", "E4"]]
|
|
|
|
|
roots = ["A1", "D2", "E1", "A1"]
|
|
|
|
|
for bar in range(16):
|
|
|
|
|
b0, ch, root = bar * 4, chords[bar % 4], roots[bar % 4]
|
|
|
|
|
# Il dembow: cassa sull'uno e sul tre, rullante sul «tre sedicesimi» e sul due e mezzo.
|
|
|
|
|
for k in (0, 2):
|
|
|
|
|
tr.add(b0 + k, K)
|
|
|
|
|
tr.add(b0 + k + 0.75, SN)
|
|
|
|
|
tr.add(b0 + k + 1.5, SN)
|
|
|
|
|
for k in range(8):
|
|
|
|
|
tr.add(b0 + k / 2, H)
|
|
|
|
|
for s, b in ((0.5, 0), (1.0, 1), (2.5, 0), (3.0, 1), (3.5, 1)):
|
|
|
|
|
tr.add(b0 + s, BONGO[b])
|
|
|
|
|
tr.add(b0 + 3.75, CONGA)
|
|
|
|
|
# Il piano montuno: accordi sincopati, voce alta raddoppiata all'ottava.
|
|
|
|
|
for s in (0, 0.75, 1.5, 2.5, 3.25):
|
|
|
|
|
for note in ch:
|
|
|
|
|
tr.add(b0 + s, sine(hz(note), tr.beat * 0.3, 0.05, decay=0.35, partials=((1, 1), (2, 0.4), (3, 0.15), (4, 0.08))))
|
|
|
|
|
# Il basso del tumbao: in anticipo sul due e mezzo e sul quattro.
|
|
|
|
|
for s, f in ((0, 1), (1.5, 1.5), (3, 1)):
|
|
|
|
|
tr.add(b0 + s, synth(hz(root) * f, tr.beat * 0.8, 0.3, wave_="square", cutoff=380, decay=0.6))
|
|
|
|
|
return tr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
GENRES = {"house": house, "techno": techno, "underground": underground, "latino": latino}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def write(name: str, tr: Track):
|
|
|
|
|
peak = max(abs(x) for x in tr.buf) or 1
|
|
|
|
|
AUDIO.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
out = AUDIO / f"ingorgo-{name}-sintesi.mp3"
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
w = Path(tmp) / "loop.wav"
|
|
|
|
|
with wave.open(str(w), "wb") as f:
|
|
|
|
|
f.setnchannels(1)
|
|
|
|
|
f.setsampwidth(2)
|
|
|
|
|
f.setframerate(SR)
|
|
|
|
|
f.writeframes(array("h", (int(x / peak * 0.9 * 32767) for x in tr.buf)).tobytes())
|
|
|
|
|
# Una piazza all'aperto: un filo d'eco dai muri, e il compressore delle casse.
|
|
|
|
|
af = "acompressor=threshold=0.25:ratio=3:attack=5:release=80,aecho=0.85:0.4:90:0.12,loudnorm=I=-18:TP=-2:LRA=7"
|
|
|
|
|
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-i", str(w), "-af", af, "-c:a", "libmp3lame", "-b:a", "96k", str(out)], check=True)
|
|
|
|
|
print(f"-> {out} ({len(tr.buf) / SR:.0f} s)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
for name in sys.argv[1:] or GENRES:
|
|
|
|
|
write(name, GENRES[name]())
|