nx_sound.nx source
↩ module page · 155 lines · 5146 B
1// nx_sound.nx -- audio primitives (windowing + spectrogram).
2//
3// Building on V7 FFT. Ships the foundation for short-time spectral
4// analysis -- the math that powers speech, music, and any temporal
5// signal-frequency analysis.
6//
7// What this unlocks:
8// + spectrogram (time x frequency intensity map)
9// + voice activity detection (energy in voice band)
10// + pitch detection (harmonic structure)
11// + beat detection (periodic energy spikes)
12// + room-tone characterization
13// + noise vs signal discrimination
14//
15// All Q14 windows. Cosine table reused at 5-degree resolution; window
16// precision +-2% (sufficient for most analysis tasks).
17//
18// genealogy_id: harris_1978_windowing + allen_rabiner_1977_stft
19// lineage_id: windowed_short_time_fft
20
21// nx_safety_envelope:
22// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
23// sil_target: SIL1
24// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
25// verdict: NOT_YET_EVALUATED
26
27import "syscalls.nx"
28import "nx_fft.nx"
29
30const NX_SOUND_Q: i64 = 16384 // Q14
31
32// ===== Cosine helper (full 0..359 degree range) ========================
33
34func nx_sound_cos_q10(deg_x5: i64) -> i64 {
35 let cos_t: *i64 = (sys_mmap(36 * 8)) as *i64
36 cos_t[0]=1024; cos_t[1]=1020; cos_t[2]=1008; cos_t[3]=989
37 cos_t[4]=962; cos_t[5]=928; cos_t[6]=887; cos_t[7]=839
38 cos_t[8]=784; cos_t[9]=724; cos_t[10]=658; cos_t[11]=587
39 cos_t[12]=512; cos_t[13]=433; cos_t[14]=350; cos_t[15]=265
40 cos_t[16]=178; cos_t[17]=89; cos_t[18]=0; cos_t[19]=-89
41 cos_t[20]=-178;cos_t[21]=-265;cos_t[22]=-350;cos_t[23]=-433
42 cos_t[24]=-512;cos_t[25]=-587;cos_t[26]=-658;cos_t[27]=-724
43 cos_t[28]=-784;cos_t[29]=-839;cos_t[30]=-887;cos_t[31]=-928
44 cos_t[32]=-962;cos_t[33]=-989;cos_t[34]=-1008;cos_t[35]=-1020
45 var t: i64 = deg_x5
46 while t < 0 { t = t + 72 }
47 while t >= 72 { t = t - 72 }
48 if t < 36 { return cos_t[t] }
49 return -cos_t[t - 36]
50}
51
52// ===== Window functions =================================================
53//
54// Hann window: w[n] = 0.5 * (1 - cos(2*pi*n / (N-1)))
55// In Q14: w[n] = (Q14 - cos_q14) / 2
56
57func nx_sound_hann_window(N: i64, win: *i64) -> i64 {
58 if N < 2 { return -1 }
59 var n: i64 = 0
60 while n < N {
61 // angle_deg = 360 * n / (N - 1)
62 let angle_deg: i64 = (360 * n) / (N - 1)
63 // round to nearest 5 degrees
64 let deg_x5: i64 = (angle_deg + 2) / 5
65 let c_q10: i64 = nx_sound_cos_q10(deg_x5)
66 let c_q14: i64 = c_q10 * 16
67 win[n] = (NX_SOUND_Q - c_q14) / 2
68 n = n + 1
69 }
70 return 0
71}
72
73// Hamming window: w[n] = 0.54 - 0.46 * cos(2*pi*n / (N-1))
74// In Q14: 0.54 * 16384 = 8847; 0.46 * 16384 = 7537
75
76func nx_sound_hamming_window(N: i64, win: *i64) -> i64 {
77 if N < 2 { return -1 }
78 var n: i64 = 0
79 while n < N {
80 let angle_deg: i64 = (360 * n) / (N - 1)
81 let deg_x5: i64 = (angle_deg + 2) / 5
82 let c_q10: i64 = nx_sound_cos_q10(deg_x5)
83 // 0.46 * cos in Q14 = (7537 * c_q10) / 1024
84 let scaled_cos: i64 = (7537 * c_q10) / 1024
85 win[n] = 8847 - scaled_cos
86 n = n + 1
87 }
88 return 0
89}
90
91// ===== Apply window to signal ==========================================
92//
93// Multiplies each sample by window weight; divides by Q14 to keep scale.
94
95func nx_sound_apply_window(signal: *i64, win: *i64, N: i64) -> i64 {
96 var n: i64 = 0
97 while n < N {
98 signal[n] = (signal[n] * win[n]) / NX_SOUND_Q
99 n = n + 1
100 }
101 return 0
102}
103
104// ===== Signal energy (frame RMS scaled) ================================
105//
106// Sum of squares; useful for voice activity detection.
107
108func nx_sound_frame_energy(signal: *i64, N: i64) -> i64 {
109 var acc: i64 = 0
110 var n: i64 = 0
111 while n < N {
112 acc = acc + signal[n] * signal[n]
113 n = n + 1
114 }
115 return acc / N
116}
117
118// ===== Short-time FFT (spectrogram) ====================================
119//
120// Splits signal into N-sample frames hopping by `hop` samples, applies
121// Hann window, runs FFT, stores power spectrum. Output: spec_out
122// laid out as n_frames * N row-major.
123//
124// Returns number of frames produced.
125
126func nx_sound_stft(signal: *i64, sig_len: i64, N: i64, hop: i64,
127 spec_out: *i64) -> i64 {
128 let win: *i64 = (sys_mmap(N * 8)) as *i64
129 nx_sound_hann_window(N, win)
130 let frame_re: *i64 = (sys_mmap(N * 8)) as *i64
131 let frame_im: *i64 = (sys_mmap(N * 8)) as *i64
132 let power: *i64 = (sys_mmap(N * 8)) as *i64
133
134 var frame_idx: i64 = 0
135 var start: i64 = 0
136 while start + N <= sig_len {
137 var i: i64 = 0
138 while i < N {
139 frame_re[i] = signal[start + i]
140 frame_im[i] = 0
141 i = i + 1
142 }
143 nx_sound_apply_window(frame_re, win, N)
144 nx_fft_forward(frame_re, frame_im, N)
145 nx_fft_power_spectrum(frame_re, frame_im, N, power)
146 var k: i64 = 0
147 while k < N {
148 spec_out[frame_idx * N + k] = power[k]
149 k = k + 1
150 }
151 frame_idx = frame_idx + 1
152 start = start + hop
153 }
154 return frame_idx
155}