nx_wav.nx source
↩ module page · 160 lines · 5889 B
1// nx_wav.nx -- pure-NishiLang RIFF/WAVE (PCM 16-bit LE) writer.
2//
3// Why a sovereign WAV writer: it gives the substrate a path from i64
4// sample buffers to a file the operating system can play through any
5// sound card with no library, no codec, no userspace audio stack
6// dependency. /tmp/foo.wav -> aplay -> speakers; we hear our BFSK
7// tones in the air. That is the "real test" hook between the modem
8// primitives and human ears.
9//
10// The WAV header is the 1991 Microsoft RIFF spec; fully public.
11// We write the canonical 44-byte form (no LIST/INFO chunks).
12//
13// genealogy_id: rfc_riff_1991 + microsoft_wave_spec + nx_bfsk_q10
14// lineage_id: nishi_wav_writer_q10
15//
16// Header layout (PCM 16-bit LE, 44 bytes total):
17// [0..3] "RIFF"
18// [4..7] chunk size = 36 + data_bytes (LE u32)
19// [8..11] "WAVE"
20// [12..15] "fmt "
21// [16..19] 16 (fmt chunk size, PCM)
22// [20..21] 1 (PCM format)
23// [22..23] channels
24// [24..27] sample_rate
25// [28..31] byte_rate = sample_rate * channels * 2
26// [32..33] block_align = channels * 2
27// [34..35] bits_per_sample = 16
28// [36..39] "data"
29// [40..43] data_bytes = n_samples * channels * 2
30// [44..] sample data, int16 LE
31
32// nx_safety_envelope:
33// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
34// sil_target: SIL1
35// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
36// verdict: NOT_YET_EVALUATED
37
38import "nx_syscalls_x86_64.nx"
39
40// Sealed verdict per write call.
41const NX_WAV_VERDICT_UNKNOWN: i64 = 0
42const NX_WAV_VERDICT_OK: i64 = 1
43const NX_WAV_VERDICT_OPEN_FAIL: i64 = 2
44const NX_WAV_VERDICT_WRITE_FAIL: i64 = 3
45const NX_WAV_VERDICT_BAD_PARAMS: i64 = 4
46const NX_WAV_VERDICT_N: i64 = 5
47
48const NX_WAV_HEADER_BYTES: i64 = 44
49
50// Write little-endian u32 into buf[off..off+4]. Returns off+4.
51func _w_le32(buf: *u8, off: i64, v: i64) -> i64 {
52 buf[off] = v & 0xff
53 buf[off + 1] = (v >> 8) & 0xff
54 buf[off + 2] = (v >> 16) & 0xff
55 buf[off + 3] = (v >> 24) & 0xff
56 return off + 4
57}
58
59// Write little-endian u16 into buf[off..off+2]. Returns off+2.
60func _w_le16(buf: *u8, off: i64, v: i64) -> i64 {
61 buf[off] = v & 0xff
62 buf[off + 1] = (v >> 8) & 0xff
63 return off + 2
64}
65
66// Build the 44-byte WAV header into `header` (caller-allocated, >= 44
67// bytes). Returns NX_WAV_HEADER_BYTES.
68func nx_wav_build_header(
69 header: *u8,
70 sample_rate_hz: i64, channels: i64, n_samples: i64
71) -> i64 {
72 let data_bytes: i64 = n_samples * channels * 2
73 let chunk_size: i64 = 36 + data_bytes
74 let byte_rate: i64 = sample_rate_hz * channels * 2
75 let block_align: i64 = channels * 2
76
77 header[0]=82; header[1]=73; header[2]=70; header[3]=70 // "RIFF"
78 _w_le32(header, 4, chunk_size)
79 header[8]=87; header[9]=65; header[10]=86; header[11]=69 // "WAVE"
80 header[12]=102; header[13]=109; header[14]=116; header[15]=32 // "fmt "
81 _w_le32(header, 16, 16) // fmt chunk size
82 _w_le16(header, 20, 1) // PCM format
83 _w_le16(header, 22, channels)
84 _w_le32(header, 24, sample_rate_hz)
85 _w_le32(header, 28, byte_rate)
86 _w_le16(header, 32, block_align)
87 _w_le16(header, 34, 16) // bits per sample
88 header[36]=100; header[37]=97; header[38]=116; header[39]=97 // "data"
89 _w_le32(header, 40, data_bytes)
90 return NX_WAV_HEADER_BYTES
91}
92
93// Open `path` for writing (creates / truncates), write header, return fd
94// or -errno-as-NX-verdict (negative = open failed).
95func nx_wav_open(path: *u8, sample_rate_hz: i64, channels: i64, n_samples: i64,
96 out_fd: *i64) -> i64 {
97 if sample_rate_hz < 1 { return NX_WAV_VERDICT_BAD_PARAMS }
98 if channels < 1 { return NX_WAV_VERDICT_BAD_PARAMS }
99 if channels > 8 { return NX_WAV_VERDICT_BAD_PARAMS }
100 if n_samples < 0 { return NX_WAV_VERDICT_BAD_PARAMS }
101
102 let fd: i64 = sys_openat_wr(path, 0x1a4) // 0644
103 if fd < 0 { *out_fd = fd; return NX_WAV_VERDICT_OPEN_FAIL }
104
105 let header: *u8 = sys_mmap(64)
106 nx_wav_build_header(header, sample_rate_hz, channels, n_samples)
107 let wn: i64 = sys_write(fd, header, NX_WAV_HEADER_BYTES)
108 if wn != NX_WAV_HEADER_BYTES {
109 sys_close(fd)
110 return NX_WAV_VERDICT_WRITE_FAIL
111 }
112 *out_fd = fd
113 return NX_WAV_VERDICT_OK
114}
115
116// Write `n` samples (i64; low 16 bits used as signed int16) to fd.
117// Samples are clipped to [-32768, 32767].
118func nx_wav_write_samples(fd: i64, samples: *i64, n: i64) -> i64 {
119 if n < 0 { return NX_WAV_VERDICT_BAD_PARAMS }
120 let chunk: *u8 = sys_mmap(2048)
121 var i: i64 = 0
122 while i < n {
123 var batch: i64 = 1024
124 if i + batch > n { batch = n - i }
125 var j: i64 = 0
126 while j < batch {
127 var v: i64 = samples[i + j]
128 if v > 32767 { v = 32767 }
129 if v < -32768 { v = -32768 }
130 if v < 0 { v = v + 65536 }
131 chunk[j * 2] = v & 0xff
132 chunk[j * 2 + 1] = (v >> 8) & 0xff
133 j = j + 1
134 }
135 let wn: i64 = sys_write(fd, chunk, batch * 2)
136 if wn != batch * 2 { return NX_WAV_VERDICT_WRITE_FAIL }
137 i = i + batch
138 }
139 return NX_WAV_VERDICT_OK
140}
141
142// One-shot: open + write samples + close.
143func nx_wav_write_file(path: *u8, sample_rate_hz: i64, channels: i64,
144 samples: *i64, n_samples: i64) -> i64 {
145 let fd_slot: *i64 = sys_mmap(16) as *i64
146 let v: i64 = nx_wav_open(path, sample_rate_hz, channels, n_samples, fd_slot)
147 if v != NX_WAV_VERDICT_OK { return v }
148 let fd: i64 = *fd_slot
149 let wv: i64 = nx_wav_write_samples(fd, samples, n_samples)
150 sys_close(fd)
151 if wv != NX_WAV_VERDICT_OK { return wv }
152 return NX_WAV_VERDICT_OK
153}
154
155// Sealed-enum validity gate.
156func nx_wav_verdict_is_valid(v: i64) -> i64 {
157 if v < 0 { return 0 }
158 if v >= NX_WAV_VERDICT_N { return 0 }
159 return 1
160}