#!/usr/bin/env python3 """La musica delle bocce al pallaio (assets/audio/bocce.mp3), sintetizzata da zero come il valzer delle carte: una polka da circolo in do maggiore, in due quarti. Basso «um-pa» sui due movimenti, fisarmonica a contrattempo e una melodia originale di clarinetto, con un filo di vibrato. Trentadue battute che chiudono sul sol settima: il loop non si sente. (Andrea, 26 ottobre: «anche le bocce avrebbero bisogno di un sound suo».) python3 tools/polka_bocce.py # scrive assets/audio/bocce.mp3 (serve ffmpeg) """ import math import random import subprocess import tempfile import wave from array import array from pathlib import Path SR = 22050 BPM = 116 BEAT = 60 / BPM OUT = Path(__file__).resolve().parent.parent / "assets" / "audio" / "bocce.mp3" HARMONY = {"C": ["E4", "G4", "C5"], "G7": ["F4", "G4", "B4"], "F": ["F4", "A4", "C5"], "A7": ["E4", "G4", "C#5"], "Dm": ["F4", "A4", "D5"]} BASS = {"C": ("C2", "G2"), "G7": ("G1", "D2"), "F": ("F1", "C2"), "A7": ("A1", "E2"), "Dm": ("D2", "A1")} CHORDS = ("C C G7 G7 G7 G7 C C C C F F C G7 C C " "F F C C G7 G7 C C F F C A7 Dm G7 C G7").split() h = 0.5 MELODY = [ [("E5", h), ("G5", h), ("C6", 1)], [("B5", h), ("A5", h), ("G5", 1)], [("F5", h), ("G5", h), ("A5", h), ("B5", h)], [("D6", 1), ("B5", 1)], [("A5", h), ("B5", h), ("A5", h), ("G5", h)], [("F5", 1), ("D5", 1)], [("E5", h), ("F5", h), ("G5", 1)], [("C5", 2)], [("E5", h), ("G5", h), ("C6", 1)], [("E6", h), ("D6", h), ("C6", 1)], [("A5", h), ("C6", h), ("A5", h), ("F5", h)], [("A5", 1.5), ("G5", h)], [("G5", h), ("E5", h), ("C5", h), ("E5", h)], [("D5", h), ("F5", h), ("B5", 1)], [("C6", 1), ("G5", 1)], [("C6", 1), (None, 1)], [("C6", h), ("A5", h), ("F5", 1)], [("A5", h), ("G5", h), ("F5", 1)], [("G5", h), ("E5", h), ("C5", 1)], [("E5", h), ("G5", h), ("C6", 1)], [("B5", h), ("A5", h), ("G5", h), ("F5", h)], [("D5", 1), ("G5", 1)], [("E5", h), ("G5", h), ("E5", h), ("C5", h)], [("G4", 2)], [("A4", h), ("C5", h), ("F5", 1)], [("A5", 1), ("F5", 1)], [("E5", h), ("G5", h), ("C6", 1)], [("C#6", 1), ("A5", 1)], [("D6", h), ("C6", h), ("A5", 1)], [("B5", h), ("A5", h), ("G5", h), ("F5", h)], [("E5", 1), ("C5", 1)], [("D5", h), ("E5", h), ("F5", h), ("G5", h)], ] assert len(MELODY) == len(CHORDS) and all(sum(d for _, d in bar) == 2 for bar in MELODY) DUR = len(CHORDS) * 2 * BEAT buf = [0.0] * int(SR * DUR) rng = random.Random(11) 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(note: str) -> float: return 440 * 2 ** ((midi(note) - 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.006, release: float = 0.06, vibrato: float = 0.0): n = int((length + release) * SR) out, phase = [], 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) # Il vibrato arriva dopo l'attacco, come quello di un fiato. phase += 2 * math.pi * freq * (1 + vibrato * min(1.0, t / 0.25) * math.sin(2 * math.pi * 5.2 * t)) / SR out.append(vol * env * sum(a * math.exp(-t / d) * math.sin(k * phase) for k, a, d in partials)) return out at = lambda beat: beat * BEAT # noqa: E731 LONG = 99 # Fisarmonica: ance ricche di armonici, che non si spengono finché il mantice spinge. REED = ((1, 1.0, LONG), (2, 0.55, LONG), (3, 0.38, LONG), (4, 0.22, LONG), (5, 0.12, LONG), (6, 0.06, LONG)) # Clarinetto: armonici dispari, il suono di legno. REEDWOOD = ((1, 1.0, LONG), (3, 0.42, LONG), (5, 0.2, LONG), (7, 0.09, LONG), (2, 0.06, LONG)) for bar, name in enumerate(CHORDS): b0 = bar * 2 # Basso «um-pa»: fondamentale sull'uno, quinta sul due. for k, note in enumerate(BASS[name]): add(at(b0 + k), tone(hz(note), BEAT * 0.55, 0.32, ((1, 1.0, LONG), (2, 0.5, 0.25), (3, 0.2, 0.12)), decay=0.35)) # Fisarmonica a contrattempo, corta e frizzante. for k in (0.5, 1.5): for note in HARMONY[name]: for detune, vol in ((1.0, 0.026), (1.004, 0.019)): add(at(b0 + k), tone(hz(note) * detune, BEAT * 0.3, vol, REED, decay=LONG, attack=0.012, release=0.04)) # Un piatto leggero sul due, quello della banda. add(at(b0 + 1), [0.03 * math.exp(-i / SR / 0.08) * rng.uniform(-1, 1) for i in range(int(0.2 * SR))]) beat = 0.0 for bar in MELODY: for note, length in bar: if note: add(at(beat), tone(hz(note), BEAT * length * 0.9, 0.09, REEDWOOD, decay=LONG, attack=0.03, release=0.06, vibrato=0.006)) beat += length peak = max(abs(x) for x in buf) or 1 with tempfile.TemporaryDirectory() as tmp: w = Path(tmp) / "bocce.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()) # All'aperto, accanto al campo: poca eco, alti morbidi. af = "aecho=0.8:0.5:70|150:0.12|0.07,lowpass=f=8500,volume=1.25" 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)")