diff --git a/assets/audio/pieve.mp3 b/assets/audio/pieve.mp3 new file mode 100644 index 0000000..6d4274a Binary files /dev/null and b/assets/audio/pieve.mp3 differ diff --git a/assets/voices/index.json b/assets/voices/index.json index 16ee72b..3dd4c92 100644 --- a/assets/voices/index.json +++ b/assets/voices/index.json @@ -1083,9 +1083,9 @@ "file": "andrea-70f150bdec.mp3", "sec": 2.67 }, - "tozzino|E chi sennò? Ha chiamato ora: è imbottigliato in Bolognese.": { - "file": "tozzino-d51159c364.mp3", - "sec": 4.08 + "tozzino|E chi sennò? Ha chiamato ora: s'è fermato a Pratolino a mettere le catene. A breve arriva.": { + "file": "tozzino-9fef6eb47d.mp3", + "sec": 6.42 }, "tozzino|Se non arriva, la vinco a tavolino.": { "file": "tozzino-c06cc01612.mp3", diff --git a/assets/voices/tozzino-9fef6eb47d.mp3 b/assets/voices/tozzino-9fef6eb47d.mp3 new file mode 100644 index 0000000..023c2a1 Binary files /dev/null and b/assets/voices/tozzino-9fef6eb47d.mp3 differ diff --git a/assets/voices/tozzino-d51159c364.mp3 b/assets/voices/tozzino-d51159c364.mp3 deleted file mode 100644 index c4b3d3b..0000000 Binary files a/assets/voices/tozzino-d51159c364.mp3 and /dev/null differ diff --git a/src/story/story.js b/src/story/story.js index 287e4a6..bea75d7 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -571,6 +571,7 @@ export const story = { name: 'Dentro la Pieve', map: [56, 39], background: 'assets/scenes/pieve-interno.png', + music: 'assets/audio/pieve.mp3', // l'organo soave della chiesa (tools/organo.py, 25 ottobre) effects: [{ type: 'beam', phase: ['pomeriggio', 'tramonto'], poly: [[22, 122], [32, 118], [250, 360], [150, 360]] }, { type: 'dust', phase: ['pomeriggio', 'tramonto'], area: [60, 200, 90, 110], count: 9 }], // Penombra, e le sei candele dell'altare accese (Andrea, 24 ottobre). indoor: true, @@ -1690,7 +1691,7 @@ export const story = { { say: 'Tozzino! Che fai lì davanti al cancello?' }, { say: 'Aspetto Federer. Oggi si fa una partita.', who: 'tozzino', pose: 'explain' }, { say: 'Federer? Roger Federer?' }, - { say: 'E chi sennò? Ha chiamato ora: è imbottigliato in Bolognese.', who: 'tozzino', pose: 'watch' }, + { say: 'E chi sennò? Ha chiamato ora: s\'è fermato a Pratolino a mettere le catene. A breve arriva.', who: 'tozzino', pose: 'watch' }, { say: 'Se non arriva, la vinco a tavolino.', who: 'tozzino', pose: 'look_far' }, ], }, diff --git a/tests/story.test.mjs b/tests/story.test.mjs index 950306a..6bc2c26 100644 --- a/tests/story.test.mjs +++ b/tests/story.test.mjs @@ -519,7 +519,7 @@ test('gli amici in giro: l\'Arcu al circolino, il Tozzino al cancello del tennis assert.deepEqual(ask('Come va alla Casa dell\'Armando?'), ['andrea: Come va alla Casa dell\'Armando?', 'arcu: Mah... un si fa una sega...']); ask('Ci si becca da i\' Trappola, ai\' Barrettino!'); assert.equal(g.dialogue, null); - assert.ok(talk('carza', 'tozzino').includes('tozzino: E chi sennò? Ha chiamato ora: è imbottigliato in Bolognese.')); + assert.ok(talk('carza', 'tozzino').includes('tozzino: E chi sennò? Ha chiamato ora: s\'è fermato a Pratolino a mettere le catene. A breve arriva.')); const baron = talk('polisportiva', 'barondeduke'); assert.ok(baron.includes('barondeduke: Avevo due donne e due jack. Entra la terza donna, e penso: «Male male si divide».')); assert.equal(baron.at(-1), 'barondeduke: L\'altro aveva colore.'); diff --git a/tools/organo.py b/tools/organo.py new file mode 100644 index 0000000..401d69f --- /dev/null +++ b/tools/organo.py @@ -0,0 +1,75 @@ +#!/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 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 flauto un po' più chiaro, con un vibrato appena accennato. + pipe(m * BEAT, BEAT * 0.92, hz(note), 0.055, 0.35, 0.9, partials=((1, 1.0), (2, 0.18), (3, 0.05)), vibrato=0.004, 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)")