code wiki / (root) / nx_audio_pcm.nx

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}