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"]