nx_audio_pcm.nx source
↩ module page · 155 lines · 5713 B
1// nx_audio_pcm.nx -- 8-bit PCM audio buffer + DSP helpers.
2//
3// Substrate for sample-based audio. T1 ESP32 has PWM-based audio
4// output that's adequate for 8-bit PCM at 8-22 kHz (chimes, family
5// voice clips, footstep sound effects). This primitive holds the
6// sample buffer + provides resampling + amplitude scaling.
7//
8// Composes:
9// nx_palette -- audio is "1D voxels"; both are byte streams w/ semantics
10// nx_brane -- CAP_HARDWARE_IO required for audio output
11// nx_voicebank (queued) -- audio-reader roadmap's voice corpus
12// is a collection of these PCM buffers
13//
14// V1 ships:
15// - struct NxPcmBuffer { samples (u8), n_samples, sample_rate_hz }
16// - linear_resample to a target sample rate
17// - amplitude_scale_q10 (volume control)
18// - mix_into (additive mixing to dest buffer with clipping)
19
20import "nx_syscalls.nx"
21import "nx_tier.nx"
22const NX_MAGIC_1024: i64 = 1024
23const NX_MAGIC_1000000: i64 = 1000000
24
25const NX_PCM_OK: nx_int = 0
26const NX_PCM_ERR_BAD_RATE: nx_int = 1
27const NX_PCM_ERR_DST_TOO_SMALL: nx_int = 2
28const NX_PCM_ERR_BAD_SCALE: nx_int = 3
29
30// ===== Struct: NxPcmBuffer =========================================
31
32struct NxPcmBuffer {
33 samples: *u8,
34 n_samples: nx_size,
35 sample_rate_hz: nx_int,
36}
37
38func nx_pcm_buffer_new(samples: *u8, n_samples: nx_size, sample_rate_hz: nx_int) -> *NxPcmBuffer {
39 if sample_rate_hz <= 0 { return (0 as i64) as *NxPcmBuffer }
40 let b: *NxPcmBuffer = (sys_mmap(24)) as *NxPcmBuffer
41 b.samples = samples
42 b.n_samples = n_samples
43 b.sample_rate_hz = sample_rate_hz
44 return b
45}
46
47// ===== nx_pcm_resample_linear =====================================
48//
49// Resample src buffer to dst at dst_rate_hz. Linear interpolation
50// between adjacent samples. Returns number of samples written or
51// negative error.
52//
53// Caller-owned dst buffer; must be at least:
54// n_dst = (src.n_samples * dst_rate_hz) / src.sample_rate_hz
55
56func nx_pcm_resample_linear(src: *NxPcmBuffer,
57 dst_samples: *u8,
58 dst_cap: nx_size,
59 dst_rate_hz: nx_int) -> nx_int {
60 if dst_rate_hz <= 0 { return 0 - NX_PCM_ERR_BAD_RATE }
61 let ratio_num: nx_int = src.sample_rate_hz
62 let ratio_den: nx_int = dst_rate_hz
63 let n_dst: nx_size = (src.n_samples * (ratio_den as nx_size)) / (ratio_num as nx_size)
64 if dst_cap < n_dst { return 0 - NX_PCM_ERR_DST_TOO_SMALL }
65
66 var i: nx_size = 0
67 while i < n_dst {
68 // src position in samples = i * src_rate / dst_rate
69 let src_pos_num: nx_size = i * (ratio_num as nx_size)
70 let src_idx: nx_size = src_pos_num / (ratio_den as nx_size)
71 let frac_num: nx_size = src_pos_num - src_idx * (ratio_den as nx_size)
72 // Linear interp between src_idx and src_idx+1
73 let s0: nx_int = (src.samples[src_idx] as i64) & 255
74 var s1: nx_int = s0
75 if src_idx + 1 < src.n_samples {
76 s1 = (src.samples[src_idx + 1] as i64) & 255
77 }
78 let interpolated: nx_int = s0 + ((s1 - s0) * (frac_num as i64)) / (ratio_den as i64)
79 dst_samples[i] = (interpolated & 255) as u8
80 i = i + 1
81 }
82 return n_dst as i64
83}
84
85// ===== nx_pcm_amplitude_scale_q10 ==================================
86//
87// Scale all samples by Q10 factor. 1024 = unity (no change);
88// 512 = half volume; 2048 = 2x (clipping at 255). Operates in place.
89
90func nx_pcm_amplitude_scale_q10(b: *NxPcmBuffer, scale_q10: nx_int) -> nx_int {
91 if scale_q10 < 0 { return NX_PCM_ERR_BAD_SCALE }
92 // Center each sample around 128 (unsigned 8-bit DC), scale, re-center
93 var i: nx_size = 0
94 while i < b.n_samples {
95 let s: nx_int = (b.samples[i] as i64) & 255
96 let centered: nx_int = s - 128
97 let scaled: nx_int = (centered * scale_q10) / NX_MAGIC_1024
98 var out: nx_int = scaled + 128
99 if out < 0 { out = 0 }
100 if out > 255 { out = 255 }
101 b.samples[i] = (out & 255) as u8
102 i = i + 1
103 }
104 return NX_PCM_OK
105}
106
107// ===== nx_pcm_mix_into =============================================
108//
109// Additive mix src into dst (sample by sample, with clipping). dst
110// is modified in place; src is unchanged. Lengths needn't match —
111// the smaller of the two bounds the operation.
112
113func nx_pcm_mix_into(dst: *NxPcmBuffer, src: *NxPcmBuffer) -> nx_int {
114 var n: nx_size = dst.n_samples
115 if src.n_samples < n { n = src.n_samples }
116 var i: nx_size = 0
117 while i < n {
118 let d: nx_int = (dst.samples[i] as i64) & 255
119 let s: nx_int = (src.samples[i] as i64) & 255
120 // Center, sum, re-center, clip
121 let d_c: nx_int = d - 128
122 let s_c: nx_int = s - 128
123 var summed: nx_int = d_c + s_c + 128
124 if summed < 0 { summed = 0 }
125 if summed > 255 { summed = 255 }
126 dst.samples[i] = (summed & 255) as u8
127 i = i + 1
128 }
129 return NX_PCM_OK
130}
131
132// ===== nx_pcm_peak_amplitude ======================================
133//
134// Returns max absolute deviation from 128 (DC) in the buffer.
135// 0 = silent; 128 = full-scale.
136
137func nx_pcm_peak_amplitude(b: *NxPcmBuffer) -> nx_int {
138 var peak: nx_int = 0
139 var i: nx_size = 0
140 while i < b.n_samples {
141 let s: nx_int = (b.samples[i] as i64) & 255
142 var dev: nx_int = s - 128
143 if dev < 0 { dev = 0 - dev }
144 if dev > peak { peak = dev }
145 i = i + 1
146 }
147 return peak
148}
149
150// ===== nx_pcm_duration_us =========================================
151
152func nx_pcm_duration_us(b: *NxPcmBuffer) -> nx_size {
153 if b.sample_rate_hz <= 0 { return 0 }
154 return (b.n_samples * NX_MAGIC_1000000) / (b.sample_rate_hz as nx_size)
155}