nodes/audio.pyΒΆ
Part of Dodge the Creeps.
1"""Procedural audio: built on the engine's AudioSynth API.
2
3Replaces the upstream demo's bundled ``gameover.wav`` (whose origin the
4upstream README never documents) with a sound generated at load time, so
5the port bundles no third-party sound files. Same pattern as the other
6ports' synth helpers.
7"""
8
9from __future__ import annotations
10
11import numpy as np
12
13from simvx.core import ADSR, AudioClip, AudioSynth, Oscillator
14
15SAMPLE_RATE = 44100
16NCHANNELS = 2
17
18
19def _stereo(mono: np.ndarray) -> AudioClip:
20 out = np.empty(mono.size * NCHANNELS, dtype=np.float32)
21 out[0::NCHANNELS] = mono
22 out[1::NCHANNELS] = mono
23 return AudioClip.from_pcm(out, sample_rate=SAMPLE_RATE, channels=NCHANNELS, name="gameover")
24
25
26def _fade(n: int, attack: float, release: float) -> np.ndarray:
27 env = np.ones(n, dtype=np.float32)
28 a = min(max(1, int(SAMPLE_RATE * attack)), n)
29 r = min(max(1, int(SAMPLE_RATE * release)), n)
30 env[:a] = np.linspace(0, 1, a, dtype=np.float32)
31 env[-r:] = np.linspace(1, 0, r, dtype=np.float32)
32 return env
33
34
35def make_gameover() -> AudioClip:
36 """Death cue: a descending three-note motif with a soft low rumble.
37
38 Kept partly bespoke because AudioSynth oscillators are constant
39 frequency and the API does not model note-concatenation: each note is
40 baked on its own and the notes are stitched with ``np.concatenate``.
41 """
42
43 def _note(freq: float, dur: float) -> np.ndarray:
44 synth = AudioSynth()
45 synth.add(
46 Oscillator.sine(freq),
47 envelope=ADSR(attack=0.005, decay=0.0, sustain=1.0, release=0.06),
48 gain=0.32,
49 )
50 synth.add( # soft second harmonic for body
51 Oscillator.sine(freq * 2.0),
52 envelope=ADSR(attack=0.005, decay=0.0, sustain=1.0, release=0.06),
53 gain=0.08,
54 )
55 return synth.bake(duration=dur, sample_rate=SAMPLE_RATE, channels=1).backend_data
56
57 sig = np.concatenate([_note(330, 0.18), _note(262, 0.18), _note(196, 0.42)])
58
59 # Light low rumble under the motif (seeded for reproducibility).
60 rng = np.random.default_rng(7)
61 noise = rng.uniform(-1, 1, sig.size).astype(np.float32) * 0.05
62 noise *= _fade(sig.size, 0.001, 0.12)
63
64 return _stereo(sig + noise)
65
66
67__all__ = ["make_gameover"]