nodes/audio.pyΒΆ
Part of Pixel Runner.
1"""Procedural audio: built on the engine's AudioSynth API.
2
3Replaces the upstream's bundled jump.mp3 / music.wav (unverifiable
4licence) with sounds generated at load time. The jump chirp keeps a
5small amount of bespoke numpy because AudioSynth oscillators have
6constant frequency (same pattern as the other ports' swept tones).
7"""
8
9from __future__ import annotations
10
11import math
12
13import numpy as np
14
15from simvx.core import ADSR, AudioClip, AudioSynth, Oscillator
16
17SAMPLE_RATE = 44100
18NCHANNELS = 2
19
20
21def _stereo(mono: np.ndarray, name: str) -> AudioClip:
22 out = np.empty(mono.size * NCHANNELS, dtype=np.float32)
23 out[0::NCHANNELS] = mono
24 out[1::NCHANNELS] = mono
25 return AudioClip.from_pcm(out, sample_rate=SAMPLE_RATE, channels=NCHANNELS, name=name)
26
27
28def make_jump() -> AudioClip:
29 """Upward chirp, 300 -> 700 Hz over 0.15 s with a fast decay."""
30 n = int(SAMPLE_RATE * 0.15)
31 freq = np.linspace(300, 700, n)
32 sig = np.sin(2 * math.pi * np.cumsum(freq) / SAMPLE_RATE).astype(np.float32) * 0.30
33 env = np.ones(n, dtype=np.float32)
34 a, r = int(SAMPLE_RATE * 0.005), int(SAMPLE_RATE * 0.06)
35 env[:a] = np.linspace(0, 1, a, dtype=np.float32)
36 env[-r:] = np.linspace(1, 0, r, dtype=np.float32)
37 return _stereo(sig * env, "jump")
38
39
40def make_music() -> AudioClip:
41 """Cheerful 8-bar chiptune loop: square lead over a triangle bass.
42
43 Baked once at load; the player loops it. Notes are stitched with
44 ``np.concatenate`` (AudioSynth doesn't model note sequences).
45 """
46 bpm = 132
47 beat = 60.0 / bpm
48
49 def note(freq: float, dur: float, osc, gain: float) -> np.ndarray:
50 if freq <= 0: # rest
51 return np.zeros(int(SAMPLE_RATE * dur), dtype=np.float32)
52 synth = AudioSynth()
53 synth.add(
54 osc(freq),
55 envelope=ADSR(attack=0.01, decay=0.05, sustain=0.7, release=0.05),
56 gain=gain,
57 )
58 return synth.bake(duration=dur, sample_rate=SAMPLE_RATE, channels=1).backend_data
59
60 # C major pentatonic lead, two 4-bar phrases.
61 c, d, e, g, a, c2, rest = 262.0, 294.0, 330.0, 392.0, 440.0, 523.0, 0.0
62 lead_notes = [
63 (e, 1),
64 (g, 1),
65 (a, 1),
66 (g, 1),
67 (e, 1),
68 (c, 1),
69 (d, 2),
70 (e, 1),
71 (g, 1),
72 (c2, 1),
73 (a, 1),
74 (g, 1),
75 (e, 1),
76 (c, 2),
77 (d, 1),
78 (e, 1),
79 (g, 1),
80 (a, 1),
81 (g, 1),
82 (e, 1),
83 (d, 2),
84 (e, 1),
85 (d, 1),
86 (c, 1),
87 (d, 1),
88 (e, 1),
89 (rest, 1),
90 (c, 2),
91 ]
92 lead = np.concatenate([note(f, b * beat, Oscillator.square, 0.10) for f, b in lead_notes])
93
94 # Root-fifth bass, one note per bar (4 beats), same total length.
95 c_b, g_b, a_b, f_b = 131.0, 196.0, 220.0, 175.0
96 bass_notes = [c_b, g_b, a_b, f_b, c_b, g_b, f_b, c_b]
97 bass = np.concatenate([note(f, 4 * beat, Oscillator.triangle, 0.16) for f in bass_notes])
98
99 n = min(lead.size, bass.size)
100 return _stereo(lead[:n] + bass[:n], "music_loop")
101
102
103__all__ = ["make_jump", "make_music"]