code wiki / _hdl_build / nx_hyp_audio.nx

nx_hyp_audio.nx source

↩ module page · 219 lines · 9047 B

1// nx_hyp_audio.nx -- HYP arc H0: sovereign hypnotherapy session-audio bed. 2// Spec: knowledge/specs/2026-06-10-hypnosis-practice-ladder.md 3// 4// Waveform source = the team's gated max-ulp=1 f64 sincos kernel 5// (_pe_f64sincos.nx, CAPREG294): hyp_table_init generates the sine table AT 6// RUNTIME from that kernel -- oracle-from-runtime, no stored waveform data. 7// Per-sample synthesis is then INTEGER-ONLY (32-bit phase accumulator + 8// table lerp), so rendering minutes of stereo audio costs well under a 9// second -- slow is a bug. 10// 11// Frequencies cross the API as integer milli-hertz; envelope gains as permil 12// rows (dur_ms, gain_start, gain_end) -- pacing patterns (e.g. 4-7-8 13// relaxation breathing) are DATA tables the caller owns, never code here. 14// 15// DEBT (named in spec): the 44-byte RIFF/WAVE header below duplicates 16// nx_wav.nx, because nx_wav imports nx_syscalls_x86_64.nx whose sys_* 17// definitions collide with the nx_syscalls.nx lane the f64 stack lives on. 18// Retire this duplication when the syscall modules unify. 19// genealogy_id: rfc_riff_1991 + nishi_wav_writer_q10 + _pe_f64sincos 20// license_tier: ORIGINAL 21 22import "nx_syscalls.nx" 23import "nx_f64.nx" 24import "nx_f64_cvt.nx" 25import "_pe_f64sincos.nx" 26 27// Sealed verdicts for file writes (mirrors nx_wav's sealed enum). 28const HYP_WAV_OK: i64 = 1 29const HYP_WAV_OPEN_FAIL: i64 = 2 30const HYP_WAV_WRITE_FAIL: i64 = 3 31const HYP_WAV_BAD_PARAMS: i64 = 4 32 33const HYP_TAB_BITS: i64 = 12 34const HYP_TAB_N: i64 = 4096 // = 1 << HYP_TAB_BITS, full sine cycle 35const HYP_PHASE_BITS: i64 = 32 // phase accumulator width 36const HYP_PEAK: i64 = 32767 // int16 full scale 37const HYP_GAIN_DENOM: i64 = 1000 // envelope gains are permil 38const HYP_MHZ_DENOM: i64 = 1000 // frequencies are milli-hertz 39const HYP_WAV_HEADER_BYTES: i64 = 44 40// 2*pi, bit-exact IEEE binary64 (0x401921FB54442D18). Mathematical constant, 41// not a tunable; cross-checked against the bigfloat120 Machin-pi trig gate. 42const HYP_TWO_PI_F64: i64 = 0x401921FB54442D18 43 44// Fill tab[0..HYP_TAB_N-1] with trunc(sin(2*pi*i/HYP_TAB_N) * HYP_PEAK), 45// computed through the gated kernel. ldexp(i, -HYP_TAB_BITS) is the EXACT 46// i/HYP_TAB_N (power-of-two denominator), so the only rounding on the angle 47// is the one f64 multiply by 2*pi. 48func hyp_table_init(tab: *i64) -> i64 { 49 let peak_f: i64 = nx_i64_to_f64(HYP_PEAK) 50 var i: i64 = 0 51 while i < HYP_TAB_N { 52 let frac: i64 = nx_f64_ldexp(nx_i64_to_f64(i), 0 - HYP_TAB_BITS) 53 let ang: i64 = nx_f64_mul(HYP_TWO_PI_F64, frac) 54 let s: i64 = nx_f64_sin(ang) 55 tab[i] = nx_f64_to_i64(nx_f64_mul(s, peak_f)) 56 i = i + 1 57 } 58 return HYP_TAB_N 59} 60 61// Phase step per sample for freq_mhz milli-hertz at sample_rate_hz. 62func hyp_phase_step(freq_mhz: i64, sample_rate_hz: i64) -> i64 { 63 return (freq_mhz << HYP_PHASE_BITS) / (sample_rate_hz * HYP_MHZ_DENOM) 64} 65 66// Render n samples of sine into buf at [off, off+stride, ...] (stride lets 67// one call own one channel of an interleaved buffer). amp_peak in 68// [0..HYP_PEAK]. Returns the final phase so callers can chain segments 69// click-free. 70func hyp_tone_fill(buf: *i64, n: i64, stride: i64, off: i64, tab: *i64, 71 phase0: i64, step: i64, amp_peak: i64) -> i64 { 72 let mask: i64 = HYP_TAB_N - 1 73 let fracbits: i64 = HYP_PHASE_BITS - HYP_TAB_BITS 74 let lerpshift: i64 = fracbits - HYP_TAB_BITS 75 let pmask: i64 = (1 << HYP_PHASE_BITS) - 1 76 var phase: i64 = phase0 & pmask 77 var k: i64 = 0 78 while k < n { 79 let idx: i64 = (phase >> fracbits) & mask 80 let nxt: i64 = (idx + 1) & mask 81 let fr: i64 = (phase >> lerpshift) & mask 82 let a: i64 = tab[idx] 83 let s: i64 = a + (((tab[nxt] - a) * fr) >> HYP_TAB_BITS) 84 buf[off + k * stride] = (s * amp_peak) / HYP_PEAK 85 phase = (phase + step) & pmask 86 k = k + 1 87 } 88 return phase 89} 90 91// Piecewise-linear gain envelope along one stride lane. rows = nseg rows of 92// [dur_ms, gain_start_permil, gain_end_permil] (stride 3). Samples past the 93// last row are silenced (the bed ends cleanly). Returns frames touched. 94func hyp_env_apply(buf: *i64, n: i64, stride: i64, off: i64, 95 sample_rate_hz: i64, rows: *i64, nseg: i64) -> i64 { 96 var frame: i64 = 0 97 var seg: i64 = 0 98 while seg < nseg { 99 let dur_ms: i64 = rows[seg * 3] 100 let g0: i64 = rows[seg * 3 + 1] 101 let g1: i64 = rows[seg * 3 + 2] 102 var sf: i64 = (dur_ms * sample_rate_hz) / 1000 103 if frame + sf > n { sf = n - frame } 104 var k: i64 = 0 105 while k < sf { 106 let g: i64 = g0 + (((g1 - g0) * k) / sf) 107 let p: i64 = off + (frame + k) * stride 108 buf[p] = (buf[p] * g) / HYP_GAIN_DENOM 109 k = k + 1 110 } 111 frame = frame + sf 112 seg = seg + 1 113 } 114 while frame < n { 115 buf[off + frame * stride] = 0 116 frame = frame + 1 117 } 118 return frame 119} 120 121func _hw_le32(buf: *u8, off: i64, v: i64) -> i64 { 122 buf[off] = v & 0xff 123 buf[off + 1] = (v >> 8) & 0xff 124 buf[off + 2] = (v >> 16) & 0xff 125 buf[off + 3] = (v >> 24) & 0xff 126 return off + 4 127} 128 129func _hw_le16(buf: *u8, off: i64, v: i64) -> i64 { 130 buf[off] = v & 0xff 131 buf[off + 1] = (v >> 8) & 0xff 132 return off + 2 133} 134 135// Canonical 44-byte PCM-16 header (1991 Microsoft RIFF spec; layout 136// byte-identical to nx_wav.nx -- see DEBT note at top). 137func hyp_wav_build_header(header: *u8, sample_rate_hz: i64, channels: i64, 138 n_samples: i64) -> i64 { 139 let data_bytes: i64 = n_samples * 2 140 let chunk_size: i64 = 36 + data_bytes 141 let byte_rate: i64 = sample_rate_hz * channels * 2 142 let block_align: i64 = channels * 2 143 144 header[0]=82; header[1]=73; header[2]=70; header[3]=70 // "RIFF" 145 _hw_le32(header, 4, chunk_size) 146 header[8]=87; header[9]=65; header[10]=86; header[11]=69 // "WAVE" 147 header[12]=102; header[13]=109; header[14]=116; header[15]=32 // "fmt " 148 _hw_le32(header, 16, 16) 149 _hw_le16(header, 20, 1) 150 _hw_le16(header, 22, channels) 151 _hw_le32(header, 24, sample_rate_hz) 152 _hw_le32(header, 28, byte_rate) 153 _hw_le16(header, 32, block_align) 154 _hw_le16(header, 34, 16) 155 header[36]=100; header[37]=97; header[38]=116; header[39]=97 // "data" 156 _hw_le32(header, 40, data_bytes) 157 return HYP_WAV_HEADER_BYTES 158} 159 160// Write interleaved samples (i64 each, clipped to int16) as a PCM-16 WAV. 161// n_samples = frames * channels. 162func hyp_wav_write(path: *u8, sample_rate_hz: i64, channels: i64, 163 samples: *i64, n_samples: i64) -> i64 { 164 if sample_rate_hz < 1 { return HYP_WAV_BAD_PARAMS } 165 if channels < 1 { return HYP_WAV_BAD_PARAMS } 166 if channels > 8 { return HYP_WAV_BAD_PARAMS } 167 if n_samples < 0 { return HYP_WAV_BAD_PARAMS } 168 169 let fd: i64 = sys_openat_wr(path, 0x1a4) 170 if fd < 0 { return HYP_WAV_OPEN_FAIL } 171 let header: *u8 = sys_mmap(64) 172 hyp_wav_build_header(header, sample_rate_hz, channels, n_samples) 173 let wn: i64 = sys_write(fd, header, HYP_WAV_HEADER_BYTES) 174 if wn != HYP_WAV_HEADER_BYTES { sys_close(fd); return HYP_WAV_WRITE_FAIL } 175 176 let chunk: *u8 = sys_mmap(2048) 177 var i: i64 = 0 178 while i < n_samples { 179 var batch: i64 = 1024 180 if i + batch > n_samples { batch = n_samples - i } 181 var j: i64 = 0 182 while j < batch { 183 var v: i64 = samples[i + j] 184 if v > 32767 { v = 32767 } 185 if v < (0 - 32768) { v = 0 - 32768 } 186 if v < 0 { v = v + 65536 } 187 chunk[j * 2] = v & 0xff 188 chunk[j * 2 + 1] = (v >> 8) & 0xff 189 j = j + 1 190 } 191 let wb: i64 = sys_write(fd, chunk, batch * 2) 192 if wb != batch * 2 { sys_close(fd); return HYP_WAV_WRITE_FAIL } 193 i = i + batch 194 } 195 sys_close(fd) 196 return HYP_WAV_OK 197} 198 199// One-call session bed: stereo binaural carrier pair (left/right frequencies 200// in milli-hertz; their difference is the beat rate) under a shared 201// paced-breathing envelope -> PCM-16 WAV at `path`. All parameters are 202// caller-owned data; nothing tunable lives in here. 203func hyp_session_bed(path: *u8, sample_rate_hz: i64, dur_ms: i64, 204 freq_l_mhz: i64, freq_r_mhz: i64, amp_peak: i64, 205 env_rows: *i64, nseg: i64) -> i64 { 206 let n_frames: i64 = (dur_ms * sample_rate_hz) / 1000 207 if n_frames < 1 { return HYP_WAV_BAD_PARAMS } 208 let total: i64 = n_frames * 2 209 let buf: *i64 = sys_mmap(total * 8 + 64) as *i64 210 let tab: *i64 = sys_mmap(HYP_TAB_N * 8 + 64) as *i64 211 hyp_table_init(tab) 212 let step_l: i64 = hyp_phase_step(freq_l_mhz, sample_rate_hz) 213 let step_r: i64 = hyp_phase_step(freq_r_mhz, sample_rate_hz) 214 hyp_tone_fill(buf, n_frames, 2, 0, tab, 0, step_l, amp_peak) 215 hyp_tone_fill(buf, n_frames, 2, 1, tab, 0, step_r, amp_peak) 216 hyp_env_apply(buf, n_frames, 2, 0, sample_rate_hz, env_rows, nseg) 217 hyp_env_apply(buf, n_frames, 2, 1, sample_rate_hz, env_rows, nseg) 218 return hyp_wav_write(path, sample_rate_hz, 2, buf, total) 219}