wav_header.nx source
↩ module page · 176 lines · 5973 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
36import "syscalls.nx"
37
38const WAV_ERR_FORMAT: i64 = -1
39const WAV_ERR_SHORT: i64 = -2
40
41const WAV_FMT_PCM: i64 = 1
42const WAV_FMT_IEEE_FLOAT: i64 = 3
43const WAV_FMT_ALAW: i64 = 6
44const WAV_FMT_MULAW: i64 = 7
45const WAV_FMT_EXTENSIBLE: i64 = 65534
46
47struct WavHeader {
48 format_code: i64, // 1 = PCM, 3 = IEEE float, etc.
49 num_channels: i64, // 1 = mono, 2 = stereo
50 sample_rate: i64, // Hz (e.g. 44100)
51 byte_rate: i64,
52 block_align: i64,
53 bits_per_sample: i64, // 8, 16, 24, 32
54 // Where the PCM samples start and how many bytes.
55 data_off: i64,
56 data_len: i64,
57}
58
59func wav_read_u16(buf: *u8, off: i64) -> i64 {
60 return buf[off] | (buf[off + 1] << 8)
61}
62
63func wav_read_u32(buf: *u8, off: i64) -> i64 {
64 let b0: i64 = buf[off]
65 let b1: i64 = buf[off + 1]
66 let b2: i64 = buf[off + 2]
67 let b3: i64 = buf[off + 3]
68 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
69}
70
71// Check a 4-byte tag at offset matches (c0,c1,c2,c3).
72func wav_tag_is(buf: *u8, off: i64,
73 c0: i64, c1: i64, c2: i64, c3: i64) -> i64 {
74 if buf[off] != c0 { return 0 }
75 if buf[off + 1] != c1 { return 0 }
76 if buf[off + 2] != c2 { return 0 }
77 if buf[off + 3] != c3 { return 0 }
78 return 1
79}
80
81// Parse a WAV file. Walks the RIFF chunks looking for "fmt " and
82// "data". Returns 0 on success or a negative WAV_ERR_*.
83func wav_parse(buf: *u8, n: i64, h: *WavHeader) -> i64 {
84 if n < 12 { return WAV_ERR_SHORT }
85 // "RIFF" magic.
86 if wav_tag_is(buf, 0, 0x52, 0x49, 0x46, 0x46) != 1 { return WAV_ERR_FORMAT }
87 // "WAVE" form.
88 if wav_tag_is(buf, 8, 0x57, 0x41, 0x56, 0x45) != 1 { return WAV_ERR_FORMAT }
89
90 h.data_off = 0
91 h.data_len = 0
92 h.format_code = 0
93
94 var off: i64 = 12
95 var have_fmt: i64 = 0
96 var have_data: i64 = 0
97
98 while off + 8 <= n {
99 let size: i64 = wav_read_u32(buf, off + 4)
100 if off + 8 + size > n { return WAV_ERR_SHORT }
101
102 if wav_tag_is(buf, off, 0x66, 0x6D, 0x74, 0x20) == 1 {
103 // "fmt "
104 if size < 16 { return WAV_ERR_FORMAT }
105 h.format_code = wav_read_u16(buf, off + 8)
106 h.num_channels = wav_read_u16(buf, off + 10)
107 h.sample_rate = wav_read_u32(buf, off + 12)
108 h.byte_rate = wav_read_u32(buf, off + 16)
109 h.block_align = wav_read_u16(buf, off + 20)
110 h.bits_per_sample = wav_read_u16(buf, off + 22)
111 have_fmt = 1
112 }
113 if wav_tag_is(buf, off, 0x64, 0x61, 0x74, 0x61) == 1 {
114 // "data"
115 h.data_off = off + 8
116 h.data_len = size
117 have_data = 1
118 }
119
120 // RIFF chunks pad to even boundaries.
121 var advance: i64 = 8 + size
122 if advance % 2 == 1 { advance = advance + 1 }
123 off = off + advance
124 }
125 if have_fmt == 0 { return WAV_ERR_FORMAT }
126 if have_data == 0 { return WAV_ERR_FORMAT }
127 return 0
128}
129
130// Compile-only smoke -- 44-byte canonical PCM header, 1-sample
131// stereo 16-bit 44.1 kHz.
132func main() -> i64 {
133 let raw: *u8 = sys_mmap(64)
134 var i: i64 = 0
135 while i < 64 { raw[i] = 0; i = i + 1 }
136
137 // "RIFF" + chunk size + "WAVE"
138 raw[0] = 0x52; raw[1] = 0x49; raw[2] = 0x46; raw[3] = 0x46
139 raw[4] = 36; raw[5] = 0; raw[6] = 0; raw[7] = 0
140 raw[8] = 0x57; raw[9] = 0x41; raw[10] = 0x56; raw[11] = 0x45
141
142 // "fmt " subchunk, size 16
143 raw[12] = 0x66; raw[13] = 0x6D; raw[14] = 0x74; raw[15] = 0x20
144 raw[16] = 16
145
146 // format_code = 1 (PCM), channels = 2
147 raw[20] = 1; raw[22] = 2
148
149 // sample_rate = 44100 = 0x0000AC44
150 raw[24] = 0x44; raw[25] = 0xAC
151
152 // byte_rate = 176400 = 0x0002B110
153 raw[28] = 0x10; raw[29] = 0xB1; raw[30] = 0x02
154
155 // block_align = 4, bits_per_sample = 16
156 raw[32] = 4; raw[34] = 16
157
158 // "data" subchunk, size = 4 (one stereo sample = 4 bytes)
159 raw[36] = 0x64; raw[37] = 0x61; raw[38] = 0x74; raw[39] = 0x61
160 raw[40] = 4
161
162 let h_raw: *u8 = sys_mmap(128)
163 let h: *WavHeader = h_raw as *WavHeader
164 if wav_parse(raw, 64, h) != 0 { return 1 }
165 if h.format_code != 1 { return 2 }
166 if h.num_channels != 2 { return 3 }
167 if h.sample_rate != 44100 { return 4 }
168 if h.bits_per_sample != 16 { return 5 }
169 if h.data_off != 44 { return 6 }
170 if h.data_len != 4 { return 7 }
171
172 // Corrupt magic.
173 raw[0] = 0
174 if wav_parse(raw, 64, h) != WAV_ERR_FORMAT { return 8 }
175 return 0
176}