Carte che volano: dalla mano al tavolo e, finita la presa, sul mucchietto di chi ha preso (briscola, tressette, scopa; Andrea: «non si capisce quando vengono buttate le carte»). Il valzer della balera ai tavoli da carte (tools/valzer_carte.py: fisarmonica musette, basso, melodia di mandolino col tremolo), musica per primo piano; video «Come si gioca»: cache del Chromium della VM spenta, narratore e personaggi a turno, musica di sottofondo, pronunce del narratore
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""La musica dei tavoli da carte all'oratorio (assets/audio/carte.mp3), sintetizzata da zero come il trio del biliardo:
|
||||
un valzer da balera in sol maggiore. Fisarmonica a due voci un po' scordate fra loro (il «musette») sul due e sul tre,
|
||||
basso pizzicato sull'uno, e una melodia originale di mandolino, col tremolo sulle note lunghe. Trentadue battute che
|
||||
chiudono sul re settima: il loop non si sente. (Andrea, 26 ottobre: «come per il biliardo, una sua melodia».)
|
||||
python3 tools/valzer_carte.py # scrive assets/audio/carte.mp3 (serve ffmpeg)
|
||||
"""
|
||||
import math
|
||||
import random
|
||||
import subprocess
|
||||
import tempfile
|
||||
import wave
|
||||
from array import array
|
||||
from pathlib import Path
|
||||
|
||||
SR = 22050
|
||||
BPM = 138
|
||||
BEAT = 60 / BPM
|
||||
OUT = Path(__file__).resolve().parent.parent / "assets" / "audio" / "carte.mp3"
|
||||
|
||||
# Un accordo per battuta: fondamentale del basso e note della fisarmonica.
|
||||
HARMONY = {"G": ["B3", "D4", "G4"], "D7": ["C4", "D4", "F#4"], "G7": ["B3", "F4", "G4"], "C": ["C4", "E4", "G4"],
|
||||
"Em": ["B3", "E4", "G4"], "Am": ["C4", "E4", "A4"], "E7": ["B3", "D4", "G#4"]}
|
||||
BASS = {"G": ("G2", "D2"), "D7": ("D2", "A1"), "G7": ("G2", "F2"), "C": ("C2", "G1"), "Em": ("E2", "B1"), "Am": ("A1", "E2"),
|
||||
"E7": ("E2", "B1")}
|
||||
CHORDS = ("G G D7 D7 D7 D7 G G G G7 C C G D7 G G "
|
||||
"Em Em Am Am D7 D7 G G C C G E7 Am D7 G D7").split()
|
||||
# La melodia del mandolino, battuta per battuta: (nota, durata in movimenti); None è una pausa.
|
||||
MELODY = [
|
||||
[("B4", 2), ("D5", 1)], [("G5", 3)], [("F#5", 2), ("E5", 1)], [("D5", 2), ("C5", 1)],
|
||||
[("A4", 2), ("C5", 1)], [("F#5", 3)], [("E5", 2), ("D5", 1)], [("B4", 3)],
|
||||
[("B4", 1), ("D5", 1), ("G5", 1)], [("F5", 3)], [("E5", 2), ("G5", 1)], [("C6", 3)],
|
||||
[("B5", 2), ("G5", 1)], [("A5", 1), ("F#5", 1), ("D5", 1)], [("G5", 3)], [(None, 2), ("D5", 1)],
|
||||
[("G5", 2), ("F#5", 1)], [("E5", 3)], [("C5", 1), ("E5", 1), ("A5", 1)], [("A5", 2), ("G5", 1)],
|
||||
[("F#5", 2), ("E5", 1)], [("D5", 1), ("F#5", 1), ("A5", 1)], [("B5", 3)], [("G5", 2), ("B4", 1)],
|
||||
[("C5", 1), ("E5", 1), ("G5", 1)], [("E5", 3)], [("D5", 2), ("G5", 1)], [("G#5", 2), ("B5", 1)],
|
||||
[("A5", 2), ("C6", 1)], [("A5", 1), ("F#5", 1), ("C5", 1)], [("B4", 3)], [("D5", 1), ("E5", 1), ("F#5", 1)],
|
||||
]
|
||||
assert len(MELODY) == len(CHORDS) and all(sum(d for _, d in bar) == 3 for bar in MELODY)
|
||||
DUR = len(CHORDS) * 3 * BEAT
|
||||
buf = [0.0] * int(SR * DUR)
|
||||
rng = random.Random(7)
|
||||
|
||||
|
||||
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):
|
||||
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.exp(-t / d) * math.sin(2 * math.pi * freq * k * t) 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))
|
||||
# Mandolino: corda pizzicata, armonici alti che muoiono subito.
|
||||
PLUCK = ((1, 1.0, LONG), (2, 0.5, 0.25), (3, 0.3, 0.12), (4, 0.15, 0.06))
|
||||
|
||||
for bar, name in enumerate(CHORDS):
|
||||
b0 = bar * 3
|
||||
# Basso sull'uno: la fondamentale, una battuta sì e una no la quinta.
|
||||
add(at(b0), tone(hz(BASS[name][bar % 2]), BEAT * 0.9, 0.3, ((1, 1.0, LONG), (2, 0.4, 0.3), (3, 0.15, 0.15)), decay=0.5))
|
||||
# Fisarmonica sul due e sul tre: due ance, una un filo crescente, che battono fra loro.
|
||||
for beat in (1, 2):
|
||||
for note in HARMONY[name]:
|
||||
for detune, vol in ((1.0, 0.03), (1.0035, 0.022)):
|
||||
add(at(b0 + beat), tone(hz(note) * detune, BEAT * 0.62, vol, REED, decay=LONG, attack=0.02, release=0.05))
|
||||
|
||||
beat = 0.0
|
||||
for bar in MELODY:
|
||||
for note, length in bar:
|
||||
if note:
|
||||
if length >= 2:
|
||||
# Le note lunghe col tremolo del plettro: tanti colpi fitti, sempre più piano.
|
||||
picks = int(length * BEAT / 0.068)
|
||||
for k in range(picks):
|
||||
add(at(beat) + k * 0.068, tone(hz(note), 0.068, 0.07 * (1 - 0.35 * k / picks) * (1.15 if k == 0 else 1),
|
||||
PLUCK, decay=0.12, attack=0.002, release=0.02))
|
||||
else:
|
||||
add(at(beat), tone(hz(note), BEAT * length * 0.95, 0.1, PLUCK, decay=0.35, attack=0.002, release=0.05))
|
||||
beat += length
|
||||
|
||||
peak = max(abs(x) for x in buf) or 1
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
w = Path(tmp) / "carte.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())
|
||||
# Il cortile dell'oratorio: un po' d'eco dai muri, niente alti taglienti.
|
||||
af = "aecho=0.8:0.55:60|130:0.2|0.12,lowpass=f=8000,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)")
|
||||
Reference in New Issue
Block a user