mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +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
|
|
@ -489,8 +489,13 @@ impl Linker {
|
|||
let has_audio = has_label(user_code, "__audio_used");
|
||||
let has_noise = has_label(user_code, "__noise_used");
|
||||
let has_triangle = has_label(user_code, "__triangle_used");
|
||||
let has_sfx_pitch = has_label(user_code, "__sfx_pitch_used");
|
||||
if has_audio {
|
||||
all_instructions.extend(runtime::gen_audio_tick(has_noise, has_triangle));
|
||||
all_instructions.extend(runtime::gen_audio_tick(
|
||||
has_noise,
|
||||
has_triangle,
|
||||
has_sfx_pitch,
|
||||
));
|
||||
all_instructions.extend(runtime::gen_period_table());
|
||||
// Emit one data block per sfx blob: a label followed by
|
||||
// the envelope bytes. `play Name` codegen emits a
|
||||
|
|
@ -500,6 +505,18 @@ impl Linker {
|
|||
&blob.label(),
|
||||
blob.envelope.clone(),
|
||||
));
|
||||
// Optional pitch envelope blob. Only emitted for
|
||||
// sfx the compiler decided actually need per-frame
|
||||
// pitch updates — the pitch_envelope is empty for
|
||||
// single-pitch sfx and the `gen_data_block` call
|
||||
// is skipped, keeping ROM bytes identical to the
|
||||
// pre-pitch-envelope behaviour.
|
||||
if blob.has_pitch_envelope() {
|
||||
all_instructions.extend(runtime::gen_data_block(
|
||||
&blob.pitch_label(),
|
||||
blob.pitch_envelope.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// Same for music: label + note stream.
|
||||
for blob in music {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue