Musichetta a 8 bit per il cartello «Manzolo (Poca) Art Presents»

tools/jingle.py la sintetizza da zero, a tempo con le scritte: arpeggio su MANZOLO, accordo su
Art, trombone triste sul (Poca), accordo finale su PRESENTS. Suona una volta sola; la musica del
titolo entra quando compare il titolo; saltando il cartello si salta anche lei. Senza un gesto
del giocatore il browser può tenerla muta.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UdeKd62opy9PKgpQXtVctF
This commit is contained in:
2026-10-23 15:30:42 +02:00
co-authored by Claude Opus 5.5
parent c3ce1241d2
commit 5e6d7d4cfb
7 changed files with 100 additions and 3 deletions
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""La musichetta del cartello «Manzolo (Poca) Art Presents» (assets/audio/cartello.mp3), sintetizzata da zero:
onde quadre e triangolari da 8 bit, a tempo con le scritte di style.css (#splash).
python3 tools/jingle.py # scrive assets/audio/cartello.mp3 (serve ffmpeg)
0,2 s MANZOLO arpeggio che sale
1,0 s Art accordo
1,9 s (Poca) «uah-uaah» da trombone triste: l'arte è poca
2,6 s PRESENTS accordo finale che si spegne prima della dissolvenza (4,3 s)
"""
import math
import subprocess
import tempfile
import wave
from array import array
from pathlib import Path
SR = 44100
DUR = 4.6
OUT = Path(__file__).resolve().parent.parent / "assets" / "audio" / "cartello.mp3"
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 square(phase: float, duty: float = 0.25) -> float:
return 1.0 if phase % 1 < duty else -1.0
def triangle(phase: float) -> float:
return 4 * abs(phase % 1 - 0.5) - 1
def saw(phase: float) -> float:
return 2 * (phase % 1) - 1
def tone(start, length, freq, wave_fn=square, vol=0.18, attack=0.008, release=0.08, slide_to=None, vibrato=0.0):
"""Una nota: inviluppo attacco/rilascio, glissando facoltativo verso slide_to, vibrato in semitoni."""
phase = 0.0
n0, n = int(start * SR), int(length * SR)
for i in range(n):
t = i / SR
f = freq if slide_to is None else freq * (slide_to / freq) ** (t / length)
if vibrato:
f *= 2 ** (vibrato * math.sin(2 * math.pi * 5.5 * t) / 12)
phase += f / SR
env = min(1.0, t / attack) * min(1.0, (length - t) / release)
if n0 + i < len(buf):
buf[n0 + i] += vol * env * wave_fn(phase)
# MANZOLO: arpeggio di do maggiore che sale, col basso.
for k, note in enumerate(["C4", "E4", "G4", "C5", "E5"]):
tone(0.2 + k * 0.13, 0.16, hz(note))
tone(0.2, 0.75, hz("C3"), triangle, vol=0.3)
# Art: accordo pieno.
for note in ["G4", "C5", "E5"]:
tone(1.0, 0.55, hz(note), vol=0.11, release=0.25)
tone(1.0, 0.6, hz("C3"), triangle, vol=0.3, release=0.25)
# (Poca): il trombone triste, due note che scendono e l'ultima che si lamenta.
tone(1.9, 0.26, hz("Eb4"), saw, vol=0.3, attack=0.03, slide_to=hz("D4"))
tone(2.18, 0.42, hz("D4"), saw, vol=0.3, attack=0.03, slide_to=hz("Db4"), vibrato=0.35, release=0.15)
# PRESENTS: accordo finale arpeggiato veloce, poi lungo, che si spegne.
for k, note in enumerate(["C4", "E4", "G4", "C5"]):
tone(2.65 + k * 0.05, 1.6 - k * 0.05, hz(note), vol=0.1, release=1.1)
tone(2.65, 1.7, hz("C3"), triangle, vol=0.32, release=1.2)
tone(2.65, 0.12, hz("C6"), square, vol=0.08, release=0.06)
peak = max(abs(x) for x in buf)
pcm = array("h", (int(x / peak * 0.85 * 32767) for x in buf))
with tempfile.TemporaryDirectory() as tmp:
w = Path(tmp) / "cartello.wav"
with wave.open(str(w), "wb") as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(SR)
f.writeframes(pcm.tobytes())
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-i", str(w), "-af", "lowpass=f=9000", "-c:a", "libmp3lame", "-b:a", "96k", str(OUT)], check=True)
print(f"-> {OUT.relative_to(OUT.parent.parent.parent)} ({DUR} s)")