wav_writer.nx source
↩ module page · 146 lines · 5388 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
27import "syscalls.nx"
28
29const WAV_WR_ERR_SHORT: i64 = -1
30const WAV_WR_ERR_BITS: i64 = -2
31
32// Write a little-endian u16 at out[off]. Returns off + 2.
33func wav_write_u16(out: *u8, off: i64, v: i64) -> i64 {
34 out[off] = v & 0xFF
35 out[off + 1] = (v >> 8) & 0xFF
36 return off + 2
37}
38
39// Write a little-endian u32. Returns off + 4.
40func wav_write_u32(out: *u8, off: i64, v: i64) -> i64 {
41 out[off] = v & 0xFF
42 out[off + 1] = (v >> 8) & 0xFF
43 out[off + 2] = (v >> 16) & 0xFF
44 out[off + 3] = (v >> 24) & 0xFF
45 return off + 4
46}
47
48// Write the 44-byte canonical PCM header. `data_bytes` is the
49// number of sample bytes the caller will append immediately after
50// (total wav size = 44 + data_bytes).
51func wav_write_header(out: *u8, cap: i64,
52 num_channels: i64, sample_rate: i64,
53 bits_per_sample: i64, data_bytes: i64) -> i64 {
54 if cap < 44 { return WAV_WR_ERR_SHORT }
55 if bits_per_sample != 16 { return WAV_WR_ERR_BITS }
56
57 // RIFF
58 out[0] = 0x52; out[1] = 0x49; out[2] = 0x46; out[3] = 0x46
59 // chunk size = 36 + data_bytes
60 wav_write_u32(out, 4, 36 + data_bytes)
61 // WAVE
62 out[8] = 0x57; out[9] = 0x41; out[10] = 0x56; out[11] = 0x45
63 // "fmt "
64 out[12] = 0x66; out[13] = 0x6D; out[14] = 0x74; out[15] = 0x20
65 // fmt subchunk size = 16 (PCM)
66 wav_write_u32(out, 16, 16)
67 // format code = 1 (PCM), channels
68 wav_write_u16(out, 20, 1)
69 wav_write_u16(out, 22, num_channels)
70 // sample rate
71 wav_write_u32(out, 24, sample_rate)
72 // byte_rate = sample_rate * channels * bits/8
73 let byte_rate: i64 = sample_rate * num_channels * (bits_per_sample / 8)
74 wav_write_u32(out, 28, byte_rate)
75 // block_align = channels * bits/8
76 let block_align: i64 = num_channels * (bits_per_sample / 8)
77 wav_write_u16(out, 32, block_align)
78 // bits per sample
79 wav_write_u16(out, 34, bits_per_sample)
80 // "data"
81 out[36] = 0x64; out[37] = 0x61; out[38] = 0x74; out[39] = 0x61
82 // data size
83 wav_write_u32(out, 40, data_bytes)
84 return 44
85}
86
87// Convenience: write header + one sample frame from caller's
88// i16 sample array (interleaved [ch0,ch1,...,chN, ch0,ch1,...]).
89// `samples` points to an array of length num_samples * num_channels
90// i16 values (packed little-endian into *u8 by this function).
91func wav_write_pcm16(out: *u8, cap: i64,
92 num_channels: i64, sample_rate: i64,
93 samples: *i64, num_samples: i64) -> i64 {
94 let total_samples: i64 = num_samples * num_channels
95 let data_bytes: i64 = total_samples * 2
96 if cap < 44 + data_bytes { return WAV_WR_ERR_SHORT }
97
98 let hdr_len: i64 = wav_write_header(out, cap, num_channels,
99 sample_rate, 16, data_bytes)
100 if hdr_len < 0 { return hdr_len }
101
102 var i: i64 = 0
103 var off: i64 = hdr_len
104 while i < total_samples {
105 let s: i64 = samples[i]
106 wav_write_u16(out, off, s & 0xFFFF)
107 off = off + 2
108 i = i + 1
109 }
110 return off
111}
112
113// Compile-only smoke: write 2 stereo samples @ 44100 Hz.
114func main() -> i64 {
115 let out: *u8 = sys_mmap(256)
116 let samples: *i64 = sys_mmap(64) as *i64
117 // Frame 0: L=1000, R=-1000. Frame 1: L=0, R=32767.
118 samples[0] = 1000
119 samples[1] = -1000
120 samples[2] = 0
121 samples[3] = 32767
122
123 let n: i64 = wav_write_pcm16(out, 256, 2, 44100, samples, 2)
124 // 44 header + 2 frames * 2 channels * 2 bytes = 52.
125 if n != 52 { return 1 }
126
127 // Magic check.
128 if out[0] != 0x52 { return 2 } // 'R'
129 if out[11] != 0x45 { return 3 } // 'E' of WAVE
130
131 // fmt subchunk says PCM (1) and 2 channels.
132 if out[20] != 1 { return 4 }
133 if out[22] != 2 { return 5 }
134
135 // data tag at offset 36.
136 if out[36] != 0x64 { return 6 } // 'd'
137
138 // First sample little-endian at offset 44: L=1000 = 0x03E8.
139 if out[44] != 0xE8 { return 7 }
140 if out[45] != 0x03 { return 8 }
141 // Second sample (right channel frame 0): -1000 = 0xFC18
142 // in two's complement 16-bit. 0xFFFFFC18 & 0xFFFF = 0xFC18.
143 if out[46] != 0x18 { return 9 }
144 if out[47] != 0xFC { return 10 }
145 return 0
146}