1 /++
2 	Basic .wav file reading and writing.
3 
4 	History:
5 		Written May 15, 2020, but loosely based on code I wrote a
6 		long time ago, at least August 2008 which is the oldest
7 		file I have generated from the original code.
8 
9 		The old code could only write files, the reading support
10 		was all added in 2020.
11 +/
12 module arsd.wav;
13 
14 import core.stdc.stdio;
15 
16 /++
17 
18 +/
19 struct WavWriter {
20 	private FILE* fp;
21 
22 	/++
23 		Opens the file with the given header params.
24 
25 		Make sure you pass the correct params to header, except,
26 		if you have a seekable stream, the data length can be zero
27 		and it will be fixed when you close. If you have a non-seekable
28 		stream though, you must give the size up front.
29 
30 		If you need to go to memory, the best way is to just
31 		append your data to your own buffer, then create a [WavFileHeader]
32 		separately and prepend it. Wav files are simple, aside from
33 		the header and maybe a terminating byte (which isn't really important
34 		anyway), there's nothing special going on.
35 
36 		Throws: Exception on error from [open].
37 
38 		---
39 		auto writer = WavWriter("myfile.wav", WavFileHeader(44100, 2, 16));
40 		writer.write(shortSamples);
41 		---
42 	+/
43 	this(string filename, WavFileHeader header) {
44 		this.header = header;
45 
46 		if(!open(filename))
47 			throw new Exception("Couldn't open file for writing"); // FIXME: errno
48 	}
49 
50 	/++
51 		`WavWriter(WavFileHeader(44100, 2, 16));`
52 	+/
53 	this(WavFileHeader header) @nogc nothrow {
54 		this.header = header;
55 	}
56 
57 	/++
58 		Calls [close]. Errors are ignored.
59 	+/
60 	~this() @nogc {
61 		close();
62 	}
63 
64 	@disable this(this);
65 
66 	private uint size;
67 	private WavFileHeader header;
68 
69 	@nogc:
70 
71 	/++
72 		Returns: true on success, false on error. Check errno for details.
73 	+/
74 	bool open(string filename) {
75 		assert(fp is null);
76 		assert(filename.length < 290);
77 
78 		char[300] fn;
79 		fn[0 .. filename.length] = filename[];
80 		fn[filename.length] = 0;
81 
82 		fp = fopen(fn.ptr, "wb");
83 		if(fp is null)
84 			return false;
85 		if(fwrite(&header, header.sizeof, 1, fp) != 1)
86 			return false;
87 
88 		return true;
89 	}
90 
91 	/++
92 		Writes 8-bit samples to the file. You must have constructed the object with an 8 bit header.
93 
94 		Returns: true on success, false on error. Check errno for details.
95 	+/
96 	bool write(ubyte[] data) {
97 		assert(header.bitsPerSample == 8);
98 		if(fp is null)
99 			return false;
100 		if(fwrite(data.ptr, 1, data.length, fp) != data.length)
101 			return false;
102 		size += data.length;
103 		return true;
104 	}
105 
106 	/++
107 		Writes 16-bit samples to the file. You must have constructed the object with 16 bit header.
108 
109 		Returns: true on success, false on error. Check errno for details.
110 	+/
111 	bool write(short[] data) {
112 		assert(header.bitsPerSample == 16);
113 		if(fp is null)
114 			return false;
115 		if(fwrite(data.ptr, 2, data.length, fp) != data.length)
116 			return false;
117 		size += data.length * 2;
118 		return true;
119 	}
120 
121 	/++
122 		Returns: true on success, false on error. Check errno for details.
123 	+/
124 	bool close() {
125 		if(fp is null)
126 			return true;
127 
128 		// pad odd sized file as required by spec...
129 		if(size & 1) {
130 			fputc(0, fp);
131 		}
132 
133 		if(!header.dataLength) {
134 			// put the length back at the beginning of the file
135 			if(fseek(fp, 0, SEEK_SET) != 0)
136 				return false;
137 			auto n = header.withDataLengthInBytes(size);
138 			if(fwrite(&n, 1, n.sizeof, fp) != 1)
139 				return false;
140 		} else {
141 			assert(header.dataLength == size);
142 		}
143 		if(fclose(fp))
144 			return false;
145 		fp = null;
146 		return true;
147 	}
148 }
149 
150 version(LittleEndian) {} else static assert(0, "just needs endian conversion coded in but i was lazy");
151 
152 align(1)
153 ///
154 struct WavFileHeader {
155 	align(1):
156 	const ubyte[4] header = ['R', 'I', 'F', 'F'];
157 	int topSize; // dataLength + 36
158 	const ubyte[4] type = ['W', 'A', 'V', 'E'];
159 	const ubyte[4] fmtHeader = ['f', 'm', 't', ' '];
160 	const int fmtHeaderSize = 16;
161 	const ushort audioFormat = 1; // PCM
162 
163 	ushort numberOfChannels;
164 	uint sampleRate;
165 
166 	uint bytesPerSeconds; // bytesPerSampleTimesChannels * sampleRate
167 	ushort bytesPerSampleTimesChannels; // bitsPerSample * channels / 8
168 
169 	ushort bitsPerSample; // 16
170 
171 	const ubyte[4] dataHeader = ['d', 'a', 't', 'a'];
172 	uint dataLength;
173 	// data follows. put a 0 at the end if dataLength is odd.
174 
175 	///
176 	this(uint sampleRate, ushort numberOfChannels, ushort bitsPerSample, uint dataLengthInBytes = 0) @nogc pure @safe nothrow {
177 		assert(bitsPerSample == 8 || bitsPerSample == 16);
178 
179 		this.numberOfChannels = numberOfChannels;
180 		this.sampleRate = sampleRate;
181 		this.bitsPerSample = bitsPerSample;
182 
183 		this.bytesPerSampleTimesChannels = cast(ushort) (numberOfChannels * bitsPerSample / 8);
184 		this.bytesPerSeconds = this.bytesPerSampleTimesChannels * sampleRate;
185 
186 		this.topSize = dataLengthInBytes + 36;
187 		this.dataLength = dataLengthInBytes;
188 	}
189 
190 	///
191 	WavFileHeader withDataLengthInBytes(int dataLengthInBytes) const @nogc pure @safe nothrow {
192 		return WavFileHeader(sampleRate, numberOfChannels, bitsPerSample, dataLengthInBytes);
193 	}
194 }
195 static assert(WavFileHeader.sizeof == 44);
196 
197 
198 /++
199 	After construction, the parameters are set and you can set them.
200 	After that, you process the samples range-style.
201 
202 	It ignores chunks in the file that aren't the basic standard.
203 	It throws exceptions if it isn't a bare-basic PCM wav file.
204 
205 	See [wavReader] for the convenience constructors.
206 
207 	Note that if you are reading a 16 bit file (`bitsPerSample == 16`),
208 	you'll actually need to `cast(short[]) front`.
209 
210 	---
211 		auto reader = wavReader(data[]);
212 		foreach(chunk; reader)
213 			play(chunk);
214 	---
215 +/
216 struct WavReader(Range) {
217 	const ushort numberOfChannels;
218 	const int sampleRate;
219 	const ushort bitsPerSample;
220 	int dataLength; // don't modify plz
221 
222 	float duration; // in seconds, added nov 26 2022
223 
224 	private uint remainingDataLength;
225 
226 	private Range underlying;
227 
228 	private const(ubyte)[] frontBuffer;
229 
230 	static if(is(Range == CFileChunks)) {
231 		this(FILE* fp) {
232 			underlying = CFileChunks(fp);
233 			this(0);
234 		}
235 	} else {
236 		this(Range r) {
237 			this.underlying = r;
238 			this(0);
239 		}
240 	}
241 
242 	private this(int _initializationDummyVariable) {
243 		this.frontBuffer = underlying.front;
244 
245 		WavFileHeader header;
246 		ubyte[] headerBytes = (cast(ubyte*) &header)[0 .. header.sizeof - 8];
247 
248 		if(this.frontBuffer.length >= headerBytes.length) {
249 			headerBytes[] = this.frontBuffer[0 .. headerBytes.length];
250 			this.frontBuffer = this.frontBuffer[headerBytes.length .. $];
251 		} else {
252 			throw new Exception("Probably not a wav file, or else pass bigger chunks please");
253 		}
254 
255 		if(header.header != ['R', 'I', 'F', 'F'])
256 			throw new Exception("Not a wav file; no RIFF header");
257 		if(header.type != ['W', 'A', 'V', 'E'])
258 			throw new Exception("Not a wav file");
259 		// so technically the spec does NOT require fmt to be the first chunk..
260 		// but im gonna just be lazy
261 		if(header.fmtHeader != ['f', 'm', 't', ' '])
262 			throw new Exception("Malformed or unsupported wav file");
263 
264 		if(header.fmtHeaderSize < 16)
265 			throw new Exception("Unsupported wav format header");
266 
267 		auto additionalSkip = header.fmtHeaderSize - 16;
268 
269 		if(header.audioFormat != 1)
270 			throw new Exception("arsd.wav only supports the most basic wav files and this one has advanced encoding. try converting to a .mp3 file and use arsd.mp3.");
271 
272 		this.numberOfChannels = header.numberOfChannels;
273 		this.sampleRate = header.sampleRate;
274 		this.bitsPerSample = header.bitsPerSample;
275 
276 		if(header.bytesPerSampleTimesChannels != header.bitsPerSample * header.numberOfChannels / 8)
277 			throw new Exception("Malformed wav file: header.bytesPerSampleTimesChannels didn't match");
278 		if(header.bytesPerSeconds != header.bytesPerSampleTimesChannels * header.sampleRate)
279 			throw new Exception("Malformed wav file: header.bytesPerSeconds didn't match");
280 
281 		this.frontBuffer = this.frontBuffer[additionalSkip .. $];
282 
283 		static struct ChunkHeader {
284 			align(1):
285 			ubyte[4] type;
286 			uint size;
287 		}
288 		static assert(ChunkHeader.sizeof == 8);
289 
290 		ChunkHeader current;
291 		ubyte[] chunkHeader = (cast(ubyte*) &current)[0 .. current.sizeof];
292 
293 		another_chunk:
294 
295 		// now we're at the next chunk. want to skip until we hit data.
296 		if(this.frontBuffer.length < chunkHeader.length)
297 			throw new Exception("bug in arsd.wav the chunk isn't big enough to handle and im lazy. if you hit this send me your file plz");
298 
299 		chunkHeader[] = frontBuffer[0 .. chunkHeader.length];
300 		frontBuffer = frontBuffer[chunkHeader.length .. $];
301 
302 		if(current.type != ['d', 'a', 't', 'a']) {
303 			// skip unsupported chunk...
304 			drop_more:
305 			if(frontBuffer.length > current.size) {
306 				frontBuffer = frontBuffer[current.size .. $];
307 			} else {
308 				current.size -= frontBuffer.length;
309 				underlying.popFront();
310 				if(underlying.empty) {
311 					throw new Exception("Ran out of data while trying to read wav chunks");
312 				} else {
313 					frontBuffer = underlying.front;
314 					goto drop_more;
315 				}
316 			}
317 			goto another_chunk;
318 		} else {
319 			this.remainingDataLength = current.size;
320 		}
321 
322 		this.dataLength = this.remainingDataLength;
323 
324 		this.duration = cast(float) this.dataLength / header.bytesPerSeconds;
325 	}
326 
327 	@property const(ubyte)[] front() {
328 		return frontBuffer;
329 	}
330 
331 	version(none)
332 	void consumeBytes(size_t count) {
333 		if(this.frontBuffer.length)
334 			this.frontBuffer = this.frontBuffer[count .. $];
335 	}
336 
337 	void popFront() {
338 		remainingDataLength -= front.length;
339 
340 		underlying.popFront();
341 		if(underlying.empty)
342 			frontBuffer = null;
343 		else
344 			frontBuffer = underlying.front;
345 	}
346 
347 	@property bool empty() {
348 		return remainingDataLength == 0 || this.underlying.empty;
349 	}
350 }
351 
352 /++
353 	Convenience constructor for [WavReader]
354 
355 	To read from a file, pass a filename, a FILE*, or a range that
356 	reads chunks from a file.
357 
358 	To read from a memory block, just pass it a `ubyte[]` slice.
359 +/
360 WavReader!T wavReader(T)(T t) {
361 	return WavReader!T(t);
362 }
363 
364 /// ditto
365 WavReader!DataBlock wavReader(const(ubyte)[] data) {
366 	return WavReader!DataBlock(DataBlock(data));
367 }
368 
369 struct DataBlock {
370 	const(ubyte)[] front;
371 	bool empty() { return front.length == 0; }
372 	void popFront() { front = null; }
373 }
374 
375 /// Construct a [WavReader] from a filename.
376 WavReader!CFileChunks wavReader(string filename) {
377 	assert(filename.length < 290);
378 
379 	char[300] fn;
380 	fn[0 .. filename.length] = filename[];
381 	fn[filename.length] = 0;
382 
383 	auto fp = fopen(fn.ptr, "rb");
384 	if(fp is null)
385 		throw new Exception("wav file unopenable"); // FIXME details
386 
387 	return WavReader!CFileChunks(fp);
388 }
389 
390 struct CFileChunks {
391 	FILE* fp;
392 	this(FILE* fp) {
393 		this.fp = fp;
394 		buffer = new ubyte[](4096);
395 		refcount = new int;
396 		*refcount = 1;
397 		popFront();
398 	}
399 	this(this) {
400 		if(refcount !is null)
401 			(*refcount) += 1;
402 	}
403 	~this() {
404 		if(refcount is null) return;
405 		(*refcount) -= 1;
406 		if(*refcount == 0) {
407 			fclose(fp);
408 		}
409 	}
410 
411 	//ubyte[4096] buffer;
412 	ubyte[] buffer;
413 	int* refcount;
414 
415 	ubyte[] front;
416 
417 	void popFront() {
418 		auto got = fread(buffer.ptr, 1, buffer.length, fp);
419 		front = buffer[0 .. got];
420 	}
421 
422 	bool empty() {
423 		return front.length == 0 && (feof(fp) ? true : false);
424 	}
425 }