Sala dei biliardi: trio jazz da circolo sintetizzato da zero (tools/jazz_biliardo.py: contrabbasso, piano elettrico, spazzole, vibrafono, swing in si bemolle) e un sottofondo con gli altri tavoli e il bar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""La musica della sala dei biliardi del Circolo 4 Maggio (assets/audio/biliardo.mp3), sintetizzata da zero: un trio
|
||||
jazz da circolo in si bemolle, con lo swing: contrabbasso che cammina, piano elettrico che accompagna, piatto e spazzole,
|
||||
e una melodia originale di vibrafono. Sedici battute che chiudono sul giro di ritorno: il loop non si sente.
|
||||
python3 tools/jazz_biliardo.py # scrive assets/audio/biliardo.mp3 (serve ffmpeg)
|
||||
"""
|
||||
import math
|
||||
import random
|
||||
import subprocess
|
||||
import tempfile
|
||||
import wave
|
||||
from array import array
|
||||
from pathlib import Path
|
||||
|
||||
SR = 22050
|
||||
BPM = 104
|
||||
BEAT = 60 / BPM
|
||||
SWING = 2 / 3 # la croma in levare cade ai due terzi del movimento
|
||||
OUT = Path(__file__).resolve().parent.parent / "assets" / "audio" / "biliardo.mp3"
|
||||
|
||||
# Il giro: un accordo per battuta. Basso (fondamentale), qualità, voci del piano (senza fondamentale).
|
||||
CHORDS = [
|
||||
("Bb1", "maj", ["D3", "F3", "A3", "C4"]), ("G1", "dom", ["F3", "B3", "D4", "E4"]),
|
||||
("C2", "min", ["Eb3", "G3", "Bb3", "D4"]), ("F1", "dom", ["Eb3", "A3", "C4", "D4"]),
|
||||
("D2", "min", ["F3", "A3", "C4", "E4"]), ("G1", "dom", ["F3", "B3", "D4", "E4"]),
|
||||
("C2", "min", ["Eb3", "G3", "Bb3", "D4"]), ("F1", "dom", ["Eb3", "A3", "C4", "D4"]),
|
||||
("Bb1", "maj", ["D3", "F3", "A3", "C4"]), ("Bb1", "dom", ["D3", "Ab3", "C4", "F4"]),
|
||||
("Eb2", "maj", ["G3", "Bb3", "D4", "F4"]), ("E2", "dim", ["E3", "G3", "Bb3", "Db4"]),
|
||||
("D2", "min", ["F3", "A3", "C4", "E4"]), ("G1", "dom", ["F3", "B3", "D4", "E4"]),
|
||||
("C2", "min", ["Eb3", "G3", "Bb3", "D4"]), ("F1", "dom", ["Eb3", "A3", "C4", "D4"]),
|
||||
]
|
||||
THIRD_FIFTH = {"maj": (4, 7), "dom": (4, 7), "min": (3, 7), "dim": (3, 6)}
|
||||
# La melodia del vibrafono: (movimento dall'inizio, nota, durata in movimenti). I levare cadono a .66.
|
||||
S = SWING
|
||||
MELODY = [
|
||||
(0, "F5", 1.5), (1 + S, "D5", 1 - S), (2, "C5", S), (2 + S, "Bb4", 1.3),
|
||||
(5, "D5", 1), (6, "F5", S), (6 + S, "E5", 1.3),
|
||||
(8, "Eb5", 1.5), (9 + S, "G5", 1 - S), (10, "Bb5", 2),
|
||||
(12 + S, "A5", 1 - S), (13, "G5", S), (13 + S, "F5", 1 - S), (14, "Eb5", 1), (15, "C5", 1),
|
||||
(16, "D5", 1), (17, "F5", S), (17 + S, "A5", 1.3), (19 + S, "G5", 1 - S),
|
||||
(20, "B4", 1), (21, "D5", 1), (22, "F5", 1.5),
|
||||
(24, "Eb5", S), (24 + S, "D5", 1 - S), (25, "C5", 1), (26, "G4", 2),
|
||||
(29, "A4", S), (29 + S, "C5", 1 - S), (30, "Eb5", 1), (31, "D5", 1),
|
||||
(33 + S, "F5", 1 - S), (34, "D5", 1.6),
|
||||
(36, "Ab5", 1.5), (37 + S, "G5", 1 - S), (38, "F5", 2),
|
||||
(40, "G5", 1), (41, "Bb5", 1), (42, "F5", 2),
|
||||
(44, "E5", 1), (45, "G5", 1), (46, "Bb5", 1.8),
|
||||
(48, "F5", 1.5), (49 + S, "E5", 1 - S), (50, "D5", 2),
|
||||
(52, "B4", 1), (53, "D5", S), (53 + S, "F5", 1.3),
|
||||
(56, "Eb5", 1.5), (57 + S, "D5", 1 - S), (58, "C5", 2),
|
||||
(60, "A4", 1), (61, "C5", 1), (62, "F5", 1.8),
|
||||
]
|
||||
DUR = len(CHORDS) * 4 * BEAT
|
||||
buf = [0.0] * int(SR * DUR)
|
||||
rng = random.Random(4)
|
||||
|
||||
|
||||
def midi(note: str) -> int:
|
||||
names = {"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11}
|
||||
semis = names[note[0]] + (1 if "#" in note else -1 if len(note) > 2 and note[1] == "b" else 0)
|
||||
return semis + 12 * (int(note[-1]) + 1)
|
||||
|
||||
|
||||
def hz(m: int) -> float:
|
||||
return 440 * 2 ** ((m - 69) / 12)
|
||||
|
||||
|
||||
def add(start: float, samples) -> None:
|
||||
"""Somma un suono alla traccia; la coda che esce dalla fine rientra all'inizio (loop continuo)."""
|
||||
n0 = int(start * SR)
|
||||
for i, v in enumerate(samples):
|
||||
buf[(n0 + i) % len(buf)] += v
|
||||
|
||||
|
||||
def tone(freq: float, length: float, vol: float, partials, decay: float, attack: float = 0.008,
|
||||
tremolo: float = 0.0, rate: float = 5.0, release: float = 0.08):
|
||||
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)
|
||||
trem = 1 - tremolo * (0.5 + 0.5 * math.sin(2 * math.pi * rate * t))
|
||||
out.append(vol * env * trem * sum(a * math.exp(-t / d) * math.sin(2 * math.pi * freq * k * t) for k, a, d in partials))
|
||||
return out
|
||||
|
||||
|
||||
def noise(length: float, vol: float, decay: float, bright: bool, attack: float = 0.002):
|
||||
"""Rumore filtrato alla buona: la differenza fra campioni vicini è il piatto, la media le spazzole."""
|
||||
n, out, prev, low = int(length * SR), [], 0.0, 0.0
|
||||
for i in range(n):
|
||||
t = i / SR
|
||||
x = rng.uniform(-1, 1)
|
||||
if bright:
|
||||
v, prev = x - prev, x
|
||||
else:
|
||||
low += 0.08 * (x - low)
|
||||
v = low * 3
|
||||
out.append(vol * min(1.0, t / attack) * math.exp(-t / decay) * v)
|
||||
return out
|
||||
|
||||
|
||||
at = lambda beat: beat * BEAT # noqa: E731
|
||||
LONG = 99
|
||||
|
||||
for bar, (root, quality, voices) in enumerate(CHORDS):
|
||||
b0 = bar * 4
|
||||
# Contrabbasso: fondamentale, terza, quinta, poi un semitono vicino alla fondamentale dopo.
|
||||
r = midi(root)
|
||||
third, fifth = THIRD_FIFTH[quality]
|
||||
nxt = midi(CHORDS[(bar + 1) % len(CHORDS)][0])
|
||||
walk = [r, r + third, r + fifth, nxt + (1 if bar % 2 else -1)]
|
||||
for k, m in enumerate(walk):
|
||||
add(at(b0 + k), tone(hz(m), BEAT * 0.92, 0.26, ((1, 1.0, LONG), (2, 0.45, 0.5), (3, 0.12, 0.25)), decay=0.7))
|
||||
# Piano elettrico: colpi corti sul levare del due e sul quattro, una battuta sì e una no un po' diverso.
|
||||
hits = [(1 + SWING, 0.5), (3, 0.9)] if bar % 2 == 0 else [(0 + SWING, 0.4), (2 + SWING, 1.2)]
|
||||
for beat, length in hits:
|
||||
for v in voices:
|
||||
add(at(b0 + beat), tone(hz(midi(v)), BEAT * length, 0.045, ((1, 1.0, LONG), (2, 0.22, 0.6), (7, 0.05, 0.05)),
|
||||
decay=1.1, tremolo=0.2, rate=4.6))
|
||||
# Piatto: ogni movimento, più il levare del due e del quattro; spazzole sul due e sul quattro; cassa leggerissima.
|
||||
for k in range(4):
|
||||
add(at(b0 + k), noise(0.35, 0.05 if k % 2 else 0.035, 0.18, bright=True))
|
||||
if k % 2:
|
||||
add(at(b0 + k + SWING), noise(0.25, 0.03, 0.12, bright=True))
|
||||
add(at(b0 + k) - 0.04, noise(0.2, 0.05, 0.09, bright=False, attack=0.05))
|
||||
else:
|
||||
add(at(b0 + k), tone(55, 0.12, 0.05, ((1, 1.0, LONG),), decay=0.08))
|
||||
|
||||
for beat, note, length in MELODY:
|
||||
# Vibrafono: la nota, un armonico alto che sparisce subito, il tremolo del motore.
|
||||
add(at(beat), tone(hz(midi(note)), BEAT * length, 0.11, ((1, 1.0, LONG), (4, 0.18, 0.12), (10, 0.05, 0.03)),
|
||||
decay=1.6, tremolo=0.25, rate=5.6, release=0.4))
|
||||
|
||||
peak = max(abs(x) for x in buf) or 1
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
w = Path(tmp) / "biliardo.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.85 * 32767) for x in buf)).tobytes())
|
||||
# Una saletta piccola: poco riverbero, niente alti taglienti.
|
||||
af = "aecho=0.8:0.6:45|95:0.18|0.1,lowpass=f=9000,volume=1.3"
|
||||
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-i", str(w), "-af", af, "-c:a", "libmp3lame", "-b:a", "96k", str(OUT)], check=True)
|
||||
print(f"-> {OUT} ({DUR:.0f} s)")
|
||||
Reference in New Issue
Block a user