mirror of
https://github.com/imjasonh/nescript
synced 2026-07-18 06:36:57 +00:00
audio: per-frame pitch envelopes for pulse SFX
Pulse-channel sfx with a multi-byte `pitch:` array used to silently ignore everything past the first byte — the runtime audio tick latched the period at trigger time and never updated it. Programs that wanted a frequency sweep had no way to express it. The compiler now compiles a per-frame pitch envelope blob alongside the existing volume envelope when `decl.pitch` has more than one distinct value. The blob is padded (or truncated) to the volume envelope's length and ends in a zero sentinel so the runtime walker stops both pointers on the same NMI. Sfx with a single scalar pitch (or an array where every byte is the same) keep their historical "no pitch blob, latch once" path and emit byte-identical ROM bytes. The runtime gains two new pieces, both gated on a new `__sfx_pitch_used` codegen marker so programs without varying-pitch sfx pay zero bytes: 1. `gen_audio_tick` emits a per-frame pitch update block inside the SFX tick: read a byte through `(AUDIO_SFX_PITCH_PTR),Y`, write it to `$4002` (pulse-1 period low), advance the pointer. The block bails on a zero high-byte pointer so a single program can mix scalar-pitch and varying-pitch sfx without one clobbering the other. 2. `emit_play_pulse` seeds `AUDIO_SFX_PITCH_PTR_LO/HI` with the pitch-blob label for varying-pitch sfx and zeros it for scalar-pitch sfx. The per-call branch is skipped entirely when the program has no varying-pitch sfx anywhere. The new `examples/sfx_pitch_envelope.ne` exercises the path with a 16-frame siren sweep. Triangle and noise per-frame pitch are deferred — they share the same data shape but the runtime ticks for those channels still write only their volume registers, see docs/future-work.md for the gap. https://claude.ai/code/session_01KEczoNUX3WmcFLfq6iAQxB
This commit is contained in:
parent
db3a4adc57
commit
9878b7d87d
12 changed files with 449 additions and 31 deletions
42
examples/sfx_pitch_envelope.ne
Normal file
42
examples/sfx_pitch_envelope.ne
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Per-frame pitch envelope on a pulse SFX. The user authors the
|
||||
// `pitch:` array as one byte per frame (matching the per-frame
|
||||
// `volume:` array) and the runtime audio tick walks both pointers
|
||||
// in lockstep, writing pitch to `$4002` and volume to `$4000` on
|
||||
// every NMI. The result is a frequency-sweeping "siren" tone — the
|
||||
// classic latch-once pulse SFX driver couldn't model this at all.
|
||||
//
|
||||
// Per-frame pitch is opt-in: if the `pitch:` array has a single
|
||||
// value (or repeats one byte) the compiler emits the byte-identical
|
||||
// pre-pitch-envelope sequence and no extra blob, so existing
|
||||
// programs that just want a static pitch keep working unchanged.
|
||||
// See `runtime/gen_audio_tick` for the gated extension.
|
||||
//
|
||||
// Build: cargo run -- build examples/sfx_pitch_envelope.ne
|
||||
|
||||
game "SFX Pitch Envelope" {
|
||||
mapper: NROM
|
||||
}
|
||||
|
||||
// 16-frame pitch sweep from $40 down to $20 paired with a slow
|
||||
// volume ramp. Both arrays are the same length so the runtime's
|
||||
// lockstep walker handles the simplest possible case.
|
||||
sfx Siren {
|
||||
duty: 2
|
||||
pitch: [0x40, 0x3D, 0x3A, 0x37, 0x34, 0x31, 0x2E, 0x2B, 0x28, 0x26, 0x24, 0x22, 0x21, 0x20, 0x20, 0x20]
|
||||
volume: [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
|
||||
}
|
||||
|
||||
var tick: u8 = 0
|
||||
|
||||
on frame {
|
||||
// Re-trigger the sfx every 60 frames so the siren restarts
|
||||
// whenever the previous pass mutes itself, giving the
|
||||
// emulator harness a stable pattern to capture.
|
||||
tick += 1
|
||||
if tick == 60 {
|
||||
tick = 0
|
||||
play Siren
|
||||
}
|
||||
}
|
||||
|
||||
start Main
|
||||
Loading…
Add table
Add a link
Reference in a new issue