#!/usr/bin/env python3 """La musica dentro la Pieve (assets/audio/pieve.mp3), sintetizzata da zero: un organo morbido (canne di flauto, poche armoniche) con una melodia lenta originale, sul registro principale, sopra otto accordi in fa maggiore. Dura un giro intero e ricomincia sullo stesso accordo, così il loop non si sente; il riverbero della chiesa lo aggiunge ffmpeg. python3 tools/organo.py # scrive assets/audio/pieve.mp3 (serve ffmpeg) """ import math import subprocess import tempfile import wave from array import array from pathlib import Path SR = 22050 BEAT = 3.0 # secondi per nota della melodia: due note per accordo CHORD = 2 * BEAT OUT = Path(__file__).resolve().parent.parent / "assets" / "audio" / "pieve.mp3" # Accordi (basso + tre voci) e melodia, due note per accordo. Il giro chiude su fa, come comincia. CHORDS = [ ("F2", ["F3", "A3", "C4"]), ("D2", ["D3", "F3", "A3"]), ("Bb1", ["Bb2", "D3", "F3"]), ("C2", ["C3", "E3", "G3"]), ("A1", ["F3", "A3", "C4"]), ("G2", ["G3", "Bb3", "D4"]), ("C2", ["C3", "F3", "G3"]), ("F2", ["F3", "A3", "C4"]), ] MELODY = ["A4", "C5", "D5", "C5", "Bb4", "A4", "G4", "G4", "A4", "C5", "D5", "Bb4", "A4", "G4", "F4", "F4"] DUR = CHORD * len(CHORDS) buf = [0.0] * int(SR * DUR) def hz(note: str) -> float: 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 note[1] == "b" else 0) return 440 * 2 ** ((semis + 12 * (int(note[-1]) + 1) - 69) / 12) def pipe(start: float, length: float, freq: float, vol: float, attack: float, release: float, partials=((1, 1.0), (2, 0.35), (3, 0.12), (4, 0.06)), vibrato: float = 0.0, wrap: bool = False) -> None: """Una canna d'organo: poche armoniche, attacco e rilascio lenti. Con `wrap` la coda che esce dalla fine rientra all'inizio: il loop resta continuo.""" n0, n = int(start * SR), int((length + release) * SR) for i in range(n): t = i / SR env = min(1.0, t / attack) if t < length else max(0.0, 1 - (t - length) / release) if env <= 0: continue f = freq * (1 + vibrato * math.sin(2 * math.pi * 5.2 * t)) s = sum(a * math.sin(2 * math.pi * f * k * t) for k, a in partials) j = n0 + i if j >= len(buf): if not wrap: break j -= len(buf) buf[j] += vol * env * s for c, (bass, voices) in enumerate(CHORDS): at = c * CHORD pipe(at, CHORD, hz(bass), 0.10, 1.0, 1.6, partials=((1, 1.0), (2, 0.5), (3, 0.15)), wrap=True) for v in voices: pipe(at, CHORD, hz(v), 0.045, 1.2, 1.8, wrap=True) for m, note in enumerate(MELODY): # La melodia su un altro registro dello stesso organo, il principale: più armoniche, nessun vibrato, note staccate. # (Su un flauto a onda pura col vibrato sembrava un theremin: Andrea, 25 ottobre.) pipe(m * BEAT, BEAT * 0.86, hz(note), 0.04, 0.12, 0.7, partials=((1, 1.0), (2, 0.55), (3, 0.32), (4, 0.18), (5, 0.1), (6, 0.06)), wrap=True) peak = max(abs(x) for x in buf) or 1 with tempfile.TemporaryDirectory() as tmp: w = Path(tmp) / "pieve.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.8 * 32767) for x in buf)).tobytes()) # Il riverbero della navata: echi lunghi e morbidi, senza alti. af = "aecho=0.8:0.75:140|310|520|830:0.35|0.26|0.18|0.12,lowpass=f=5000,volume=1.45" 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)")