sfx.pyΒΆ

Part of PirateMaker.

 1"""Synthesized sound effects for the PirateMaker port.
 2
 3The coin, hit and jump cues are generated with ``simvx.core.AudioSynth``
 4rather than loaded from sample files. The upstream project ships those three
 5effects without a stated licence, so reusing the files would put bytes of
 6unknown provenance into a redistributable tree; a few oscillators and
 7envelopes give the same arcade read with terms this port can actually state.
 8
 9The music beds are unaffected: both ``.ogg`` files carry explicit licences
10(see ``ATTRIBUTION.md``) and are still loaded from ``assets/audio/``.
11
12Each builder returns a baked :class:`~simvx.core.AudioClip`, so the caller
13holds plain sample data and can hand it to as many ``AudioPlayer`` nodes as it
14likes. Results are cached: the level rebuilds on every play/edit switch and
15re-synthesizing per switch would be wasted work.
16"""
17
18from __future__ import annotations
19
20from functools import cache
21
22from simvx.core import ADSR, AudioClip, AudioSynth, Exponential, LowPass, Oscillator
23
24# Equal-tempered reference, matching the convention in examples/demos/afterglow.
25_A4 = 440.0
26_SEMI = 2.0 ** (1.0 / 12.0)
27
28
29def _note(semitones_from_a4: float) -> float:
30    """Frequency of a note `semitones_from_a4` semitones above (or below) A4."""
31    return _A4 * (_SEMI**semitones_from_a4)
32
33
34@cache
35def coin() -> AudioClip:
36    """Bright two-note pickup chime, the classic ascending coin blip."""
37    s = AudioSynth()
38    s.add(Oscillator.square(_note(16)), gain=0.18, envelope=ADSR(decay=0.06, sustain=0.0, release=0.03))
39    s.add(
40        Oscillator.square(_note(23)),
41        gain=0.16,
42        envelope=ADSR(attack=0.05, decay=0.12, sustain=0.0, release=0.05),
43    )
44    return s.bake(duration=0.24)
45
46
47@cache
48def hit() -> AudioClip:
49    """Damage cue: a low filtered noise burst under a falling saw."""
50    s = AudioSynth()
51    s.add(
52        Oscillator.noise.white(seed=17),
53        gain=0.24,
54        envelope=ADSR(attack=0.001, decay=0.09, sustain=0.0, release=0.05),
55        filter=LowPass(900.0),
56    )
57    s.add(Oscillator.saw(_note(-9)), gain=0.20, envelope=Exponential(start=1.0, end=0.03))
58    return s.bake(duration=0.28)
59
60
61@cache
62def jump() -> AudioClip:
63    """Short upward blip. Triangle rather than square so a held run of jumps
64    does not turn buzzy."""
65    s = AudioSynth()
66    s.add(Oscillator.triangle(_note(4)), gain=0.26, envelope=ADSR(decay=0.08, sustain=0.0, release=0.04))
67    s.add(Oscillator.sine(_note(16)), gain=0.10, envelope=ADSR(decay=0.05, sustain=0.0, release=0.03))
68    return s.bake(duration=0.15)