nx_wav_writer.nx source
↩ module page · 154 lines · 5586 B
1// wav_writer.nx -- serialise 16-bit PCM WAV files (canonical
2// 44-byte header + raw samples). Paired with wav_header.nx for
3// the reader half of the roundtrip.
4//
5// Use cases: debug tones, sovereign test-signal generation,
6// dumping buffers from a DSP pipeline during bring-up, tiny
7// synth prototypes without external tooling.
8//
9// Produces the standard 44-byte canonical PCM header:
10// RIFF [size] WAVE
11// fmt [16] fmt_code=1 channels sample_rate byte_rate
12// block_align bits_per_sample
13// data [size] <samples>
14//
15// Samples are little-endian signed i16 in (-32768..32767).
16// Caller owns the sample buffer; this module writes header + copies.
17//
18// Invariants:
19// W1 Only 16-bit PCM produced today. 8 / 24 / 32 bit and
20// float formats pending a future commit.
21// W2 num_samples is PER-CHANNEL sample count (not total
22// sample-frames across channels). Total bytes =
23// num_samples * num_channels * 2.
24// W3 Output buffer must be at least 44 + 2*channels*num_samples
25// bytes or WAV_ERR_SHORT is returned.
26
27// nx_safety_envelope:
28// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
29// sil_target: SIL1
30// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
31// verdict: NOT_YET_EVALUATED
32
33import "nx_syscalls.nx"
34const WAV_MAGIC_32767: i64 = 32767
35const WAV_MAGIC_44100: i64 = 44100
36
37const WAV_WR_ERR_SHORT: i64 = -1
38const WAV_WR_ERR_BITS: i64 = -2
39
40// Write a little-endian u16 at out[off]. Returns off + 2.
41func wav_write_u16(out: *u8, off: i64, v: i64) -> i64 {
42 out[off] = v & 0xFF
43 out[off + 1] = (v >> 8) & 0xFF
44 return off + 2
45}
46
47// Write a little-endian u32. Returns off + 4.
48func wav_write_u32(out: *u8, off: i64, v: i64) -> i64 {
49 out[off] = v & 0xFF
50 out[off + 1] = (v >> 8) & 0xFF
51 out[off + 2] = (v >> 16) & 0xFF
52 out[off + 3] = (v >> 24) & 0xFF
53 return off + 4
54}
55
56// Write the 44-byte canonical PCM header. `data_bytes` is the
57// number of sample bytes the caller will append immediately after
58// (total wav size = 44 + data_bytes).
59func wav_write_header(out: *u8, cap: i64,
60 num_channels: i64, sample_rate: i64,
61 bits_per_sample: i64, data_bytes: i64) -> i64 {
62 if cap < 44 { return WAV_WR_ERR_SHORT }
63 if bits_per_sample != 16 { return WAV_WR_ERR_BITS }
64
65 // RIFF
66 out[0] = 0x52; out[1] = 0x49; out[2] = 0x46; out[3] = 0x46
67 // chunk size = 36 + data_bytes
68 wav_write_u32(out, 4, 36 + data_bytes)
69 // WAVE
70 out[8] = 0x57; out[9] = 0x41; out[10] = 0x56; out[11] = 0x45
71 // "fmt "
72 out[12] = 0x66; out[13] = 0x6D; out[14] = 0x74; out[15] = 0x20
73 // fmt subchunk size = 16 (PCM)
74 wav_write_u32(out, 16, 16)
75 // format code = 1 (PCM), channels
76 wav_write_u16(out, 20, 1)
77 wav_write_u16(out, 22, num_channels)
78 // sample rate
79 wav_write_u32(out, 24, sample_rate)
80 // byte_rate = sample_rate * channels * bits/8
81 let byte_rate: i64 = sample_rate * num_channels * (bits_per_sample / 8)
82 wav_write_u32(out, 28, byte_rate)
83 // block_align = channels * bits/8
84 let block_align: i64 = num_channels * (bits_per_sample / 8)
85 wav_write_u16(out, 32, block_align)
86 // bits per sample
87 wav_write_u16(out, 34, bits_per_sample)
88 // "data"
89 out[36] = 0x64; out[37] = 0x61; out[38] = 0x74; out[39] = 0x61
90 // data size
91 wav_write_u32(out, 40, data_bytes)
92 return 44
93}
94
95// Convenience: write header + one sample frame from caller's
96// i16 sample array (interleaved [ch0,ch1,...,chN, ch0,ch1,...]).
97// `samples` points to an array of length num_samples * num_channels
98// i16 values (packed little-endian into *u8 by this function).
99func wav_write_pcm16(out: *u8, cap: i64,
100 num_channels: i64, sample_rate: i64,
101 samples: *i64, num_samples: i64) -> i64 {
102 let total_samples: i64 = num_samples * num_channels
103 let data_bytes: i64 = total_samples * 2
104 if cap < 44 + data_bytes { return WAV_WR_ERR_SHORT }
105
106 let hdr_len: i64 = wav_write_header(out, cap, num_channels,
107 sample_rate, 16, data_bytes)
108 if hdr_len < 0 { return hdr_len }
109
110 var i: i64 = 0
111 var off: i64 = hdr_len
112 while i < total_samples {
113 let s: i64 = samples[i]
114 wav_write_u16(out, off, s & 0xFFFF)
115 off = off + 2
116 i = i + 1
117 }
118 return off
119}
120
121// Compile-only smoke: write 2 stereo samples @ 44100 Hz.
122func main() -> i64 {
123 let out: *u8 = sys_mmap(256)
124 let samples: *i64 = sys_mmap(64) as *i64
125 // Frame 0: L=1000, R=-1000. Frame 1: L=0, R=32767.
126 samples[0] = 1000
127 samples[1] = -1000
128 samples[2] = 0
129 samples[3] = WAV_MAGIC_32767
130
131 let n: i64 = wav_write_pcm16(out, 256, 2, WAV_MAGIC_44100, samples, 2)
132 // 44 header + 2 frames * 2 channels * 2 bytes = 52.
133 if n != 52 { return 1 }
134
135 // Magic check.
136 if out[0] != 0x52 { return 2 } // 'R'
137 if out[11] != 0x45 { return 3 } // 'E' of WAVE
138
139 // fmt subchunk says PCM (1) and 2 channels.
140 if out[20] != 1 { return 4 }
141 if out[22] != 2 { return 5 }
142
143 // data tag at offset 36.
144 if out[36] != 0x64 { return 6 } // 'd'
145
146 // First sample little-endian at offset 44: L=1000 = 0x03E8.
147 if out[44] != 0xE8 { return 7 }
148 if out[45] != 0x03 { return 8 }
149 // Second sample (right channel frame 0): -1000 = 0xFC18
150 // in two's complement 16-bit. 0xFFFFFC18 & 0xFFFF = 0xFC18.
151 if out[46] != 0x18 { return 9 }
152 if out[47] != 0xFC { return 10 }
153 return 0
154}