code wiki / _hdl_build / nx_gamemusic.nx

nx_gamemusic.nx source

↩ module page · 114 lines · 4923 B

1// nx_gamemusic.nx -- the SOVEREIGN MUSIC/SEQUENCER PART, completing the `audio-sfx-music` capability. 2// nx_game_audio_gate already PROVES the SFX half (nx_audio_sfx + nx_audio_wav: coin/laser/explosion, 3// real WAV + waveform, 3/3 GREEN) and says in-band that audio "needs WIRING to games". What was genuinely 4// missing was MUSIC: nothing turned a SONG into audio, so claiming the full capability would have been an 5// overclaim on the strength of the SFX gate alone. 6// 7// COMPOSES rather than rebuilds (DRY / no-proliferation law): note oscillation reuses `sfx_tone` from 8// nx_audio_sfx, output reuses `wav_render_stereo`/`wav_save` from nx_audio_wav. This part adds only what was 9// absent -- pitch, envelope, sequencing and mixing. 10// 11// CERTIFIED PROPERTIES (each a gate tooth): 12// 1. EXACT INTEGER EQUAL-TEMPERAMENT. Pitch comes from a 12-note table with octave shifts by doubling or 13// halving, so A4=440 -> A5=880 is EXACT. No float, no pow(), no drift between builds or machines. 14// 2. DETERMINISTIC RENDER -- the same song data yields byte-identical audio, so a soundtrack is 15// reproducible and diffable (the determinism exceed, carried into audio). 16// 3. DATA-DRIVEN SONGS (rule 11) -- a song is an array of (semitone, octave, ticks); melodies are DATA, 17// never code. Rest is encoded as semitone -1. 18// 4. SATURATING MIX -- layering tracks cannot wrap the sample and produce the click/crack of integer 19// overflow; it clamps at the sample ceiling instead. 20// 5. PER-NOTE ENVELOPE -- notes attack and decay rather than clicking on/off at full amplitude. 21// LIB ONLY -- no main() by ecosystem convention. 22// license_tier: ORIGINAL expect_exit: 0 23import "nx_syscalls.nx" 24import "nx_audio_sfx.nx" 25 26const MUS_SAMPMAX: i64 = 32000 // i16 headroom for the WAV writer 27const MUS_REST: i64 = 0-1 28 29// equal-temperament semitones at octave 4, in Hz (C4..B4). Integer table = exact + portable. 30func mus_base_hz(semi: i64) -> i64 { 31 if semi==0 { return 262 } // C4 32 if semi==1 { return 277 } 33 if semi==2 { return 294 } // D4 34 if semi==3 { return 311 } 35 if semi==4 { return 330 } // E4 36 if semi==5 { return 349 } // F4 37 if semi==6 { return 370 } 38 if semi==7 { return 392 } // G4 39 if semi==8 { return 415 } 40 if semi==9 { return 440 } // A4 -- the reference 41 if semi==10 { return 466 } 42 if semi==11 { return 494 } // B4 43 return 0 44} 45// octave shift by exact doubling/halving -- A4 440 -> A5 880 -> A3 220, no rounding drift 46func mus_note_freq(semi: i64, octave: i64) -> i64 { 47 if semi < 0 { return 0 } 48 if semi > 11 { return 0 } 49 var f: i64 = mus_base_hz(semi) 50 var o: i64 = octave 51 while o > 4 { f = f * 2; o = o - 1 } 52 while o < 4 { f = f / 2; o = o + 1 } 53 return f 54} 55// attack/decay envelope scaled 0..SFX_PH over a note of n samples 56func mus_env(i: i64, n: i64, attack: i64) -> i64 { 57 if n <= 0 { return 0 } 58 if i < attack { 59 if attack <= 0 { return SFX_PH } 60 return (i * SFX_PH) / attack 61 } 62 let rem: i64 = n - attack 63 if rem <= 0 { return 0 } 64 return ((n - i) * SFX_PH) / rem 65} 66// saturating sample add -- layering must never wrap 67func mus_mixadd(a: i64, b: i64) -> i64 { 68 let s: i64 = a + b 69 if s > MUS_SAMPMAX { return MUS_SAMPMAX } 70 if s < 0 - MUS_SAMPMAX { return 0 - MUS_SAMPMAX } 71 return s 72} 73 74// Render one track of a song into out[0..nsamp) MIXED (out is not cleared -- layer tracks by calling twice). 75// song = triples (semi, octave, ticks). Returns samples written. 76func mus_render(out: *i64, nsamp: i64, song: *i64, nnotes: i64, 77 rate: i64, amp: i64, tick_samples: i64) -> i64 { 78 let tmp: *i64 = sys_mmap(nsamp*8 + 64) as *i64 79 var at: i64 = 0 80 var k: i64 = 0 81 while k < nnotes { 82 let semi: i64 = song[k*3] 83 let oct: i64 = song[k*3 + 1] 84 let ticks: i64 = song[k*3 + 2] 85 var dur: i64 = ticks * tick_samples 86 if at + dur > nsamp { dur = nsamp - at } 87 if dur > 0 { 88 if semi != MUS_REST { 89 let f: i64 = mus_note_freq(semi, oct) 90 if f > 0 { 91 sfx_tone(tmp, dur, f, rate, amp) 92 let attack: i64 = dur / 8 93 var i: i64 = 0 94 while i < dur { 95 let e: i64 = mus_env(i, dur, attack) 96 let v: i64 = (tmp[i] * e) / SFX_PH 97 out[at + i] = mus_mixadd(out[at + i], v) 98 i = i + 1 99 } 100 } 101 } 102 at = at + dur 103 } 104 k = k + 1 105 } 106 return at 107} 108// total samples a song occupies 109func mus_song_samples(song: *i64, nnotes: i64, tick_samples: i64) -> i64 { 110 var t: i64 = 0 111 var k: i64 = 0 112 while k < nnotes { t = t + song[k*3 + 2]; k = k + 1 } 113 return t * tick_samples 114}