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}