nx_wav_header.nx source
↩ module page · 183 lines · 6106 B
1// wav_header.nx -- parse RIFF WAVE (WAV) audio file headers.
2//
3// RIFF container with one "fmt " subchunk (mandatory, describes
4// audio format) and one "data" subchunk (the PCM samples). The
5// canonical 44-byte PCM header is:
6//
7// off size field
8// 0 4 "RIFF"
9// 4 4 chunk size (file size - 8)
10// 8 4 "WAVE"
11// 12 4 "fmt "
12// 16 4 fmt subchunk size (16 for PCM)
13// 20 2 audio format code (1 = PCM, 3 = IEEE float, ...)
14// 22 2 num_channels
15// 24 4 sample_rate
16// 28 4 byte_rate = sample_rate * num_channels * bits/8
17// 32 2 block_align = num_channels * bits/8
18// 34 2 bits_per_sample
19// 36 4 "data"
20// 40 4 data_size (bytes of PCM samples)
21// 44 N PCM samples
22//
23// Non-PCM formats may have a larger "fmt " chunk (extensions
24// following the canonical 16 bytes) and other subchunks ("LIST",
25// "fact", "INFO") interleaved before "data". We walk the chunk
26// stream to find "fmt " and "data" by tag, so extra chunks are
27// tolerated.
28//
29// Invariants:
30// W1 "RIFF" + "WAVE" magic required.
31// W2 Chunk walker returns both "fmt " and "data" offsets or
32// WAV_ERR_FORMAT if either is missing.
33// W3 Multi-byte integers are LITTLE-endian (RIFX variant is
34// big-endian but we don't accept it here).
35
36// nx_safety_envelope:
37// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
38// sil_target: SIL1
39// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
40// verdict: NOT_YET_EVALUATED
41
42import "nx_syscalls.nx"
43const WAV_MAGIC_44100: i64 = 44100
44
45const WAV_ERR_FORMAT: i64 = -1
46const WAV_ERR_SHORT: i64 = -2
47
48const WAV_FMT_PCM: i64 = 1
49const WAV_FMT_IEEE_FLOAT: i64 = 3
50const WAV_FMT_ALAW: i64 = 6
51const WAV_FMT_MULAW: i64 = 7
52const WAV_FMT_EXTENSIBLE: i64 = 65534
53
54struct WavHeader {
55 format_code: i64, // 1 = PCM, 3 = IEEE float, etc.
56 num_channels: i64, // 1 = mono, 2 = stereo
57 sample_rate: i64, // Hz (e.g. WAV_MAGIC_44100)
58 byte_rate: i64,
59 block_align: i64,
60 bits_per_sample: i64, // 8, 16, 24, 32
61 // Where the PCM samples start and how many bytes.
62 data_off: i64,
63 data_len: i64,
64}
65
66func wav_read_u16(buf: *u8, off: i64) -> i64 {
67 return buf[off] | (buf[off + 1] << 8)
68}
69
70func wav_read_u32(buf: *u8, off: i64) -> i64 {
71 let b0: i64 = buf[off]
72 let b1: i64 = buf[off + 1]
73 let b2: i64 = buf[off + 2]
74 let b3: i64 = buf[off + 3]
75 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
76}
77
78// Check a 4-byte tag at offset matches (c0,c1,c2,c3).
79func wav_tag_is(buf: *u8, off: i64,
80 c0: i64, c1: i64, c2: i64, c3: i64) -> i64 {
81 if buf[off] != c0 { return 0 }
82 if buf[off + 1] != c1 { return 0 }
83 if buf[off + 2] != c2 { return 0 }
84 if buf[off + 3] != c3 { return 0 }
85 return 1
86}
87
88// Parse a WAV file. Walks the RIFF chunks looking for "fmt " and
89// "data". Returns 0 on success or a negative WAV_ERR_*.
90func wav_parse(buf: *u8, n: i64, h: *WavHeader) -> i64 {
91 if n < 12 { return WAV_ERR_SHORT }
92 // "RIFF" magic.
93 if wav_tag_is(buf, 0, 0x52, 0x49, 0x46, 0x46) != 1 { return WAV_ERR_FORMAT }
94 // "WAVE" form.
95 if wav_tag_is(buf, 8, 0x57, 0x41, 0x56, 0x45) != 1 { return WAV_ERR_FORMAT }
96
97 h.data_off = 0
98 h.data_len = 0
99 h.format_code = 0
100
101 var off: i64 = 12
102 var have_fmt: i64 = 0
103 var have_data: i64 = 0
104
105 while off + 8 <= n {
106 let size: i64 = wav_read_u32(buf, off + 4)
107 if off + 8 + size > n { return WAV_ERR_SHORT }
108
109 if wav_tag_is(buf, off, 0x66, 0x6D, 0x74, 0x20) == 1 {
110 // "fmt "
111 if size < 16 { return WAV_ERR_FORMAT }
112 h.format_code = wav_read_u16(buf, off + 8)
113 h.num_channels = wav_read_u16(buf, off + 10)
114 h.sample_rate = wav_read_u32(buf, off + 12)
115 h.byte_rate = wav_read_u32(buf, off + 16)
116 h.block_align = wav_read_u16(buf, off + 20)
117 h.bits_per_sample = wav_read_u16(buf, off + 22)
118 have_fmt = 1
119 }
120 if wav_tag_is(buf, off, 0x64, 0x61, 0x74, 0x61) == 1 {
121 // "data"
122 h.data_off = off + 8
123 h.data_len = size
124 have_data = 1
125 }
126
127 // RIFF chunks pad to even boundaries.
128 var advance: i64 = 8 + size
129 if advance % 2 == 1 { advance = advance + 1 }
130 off = off + advance
131 }
132 if have_fmt == 0 { return WAV_ERR_FORMAT }
133 if have_data == 0 { return WAV_ERR_FORMAT }
134 return 0
135}
136
137// Compile-only smoke -- 44-byte canonical PCM header, 1-sample
138// stereo 16-bit 44.1 kHz.
139func main() -> i64 {
140 let raw: *u8 = sys_mmap(64)
141 var i: i64 = 0
142 while i < 64 { raw[i] = 0; i = i + 1 }
143
144 // "RIFF" + chunk size + "WAVE"
145 raw[0] = 0x52; raw[1] = 0x49; raw[2] = 0x46; raw[3] = 0x46
146 raw[4] = 36; raw[5] = 0; raw[6] = 0; raw[7] = 0
147 raw[8] = 0x57; raw[9] = 0x41; raw[10] = 0x56; raw[11] = 0x45
148
149 // "fmt " subchunk, size 16
150 raw[12] = 0x66; raw[13] = 0x6D; raw[14] = 0x74; raw[15] = 0x20
151 raw[16] = 16
152
153 // format_code = 1 (PCM), channels = 2
154 raw[20] = 1; raw[22] = 2
155
156 // sample_rate = 44100 = 0x0000AC44
157 raw[24] = 0x44; raw[25] = 0xAC
158
159 // byte_rate = 176400 = 0x0002B110
160 raw[28] = 0x10; raw[29] = 0xB1; raw[30] = 0x02
161
162 // block_align = 4, bits_per_sample = 16
163 raw[32] = 4; raw[34] = 16
164
165 // "data" subchunk, size = 4 (one stereo sample = 4 bytes)
166 raw[36] = 0x64; raw[37] = 0x61; raw[38] = 0x74; raw[39] = 0x61
167 raw[40] = 4
168
169 let h_raw: *u8 = sys_mmap(128)
170 let h: *WavHeader = h_raw as *WavHeader
171 if wav_parse(raw, 64, h) != 0 { return 1 }
172 if h.format_code != 1 { return 2 }
173 if h.num_channels != 2 { return 3 }
174 if h.sample_rate != WAV_MAGIC_44100 { return 4 }
175 if h.bits_per_sample != 16 { return 5 }
176 if h.data_off != 44 { return 6 }
177 if h.data_len != 4 { return 7 }
178
179 // Corrupt magic.
180 raw[0] = 0
181 if wav_parse(raw, 64, h) != WAV_ERR_FORMAT { return 8 }
182 return 0
183}