code wiki / (root) / nx_wav_header.nx

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}