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 	private uint remainingDataLength;
223 
224 	private Range underlying;
225 
226 	private const(ubyte)[] frontBuffer;
227 
228 	static if(is(Range == CFileChunks)) {
229 		this(FILE* fp) {
230 			underlying = CFileChunks(fp);
231 			this(0);
232 		}
233 	} else {
234 		this(Range r) {
235 			this.underlying = r;
236 			this(0);
237 		}
238 	}
239 
240 	private this(int _initializationDummyVariable) {
241 		this.frontBuffer = underlying.front;
242 
243 		WavFileHeader header;
244 		ubyte[] headerBytes = (cast(ubyte*) &header)[0 .. header.sizeof - 8];
245 
246 		if(this.frontBuffer.length >= headerBytes.length) {
247 			headerBytes[] = this.frontBuffer[0 .. headerBytes.length];
248 			this.frontBuffer = this.frontBuffer[headerBytes.length .. $];
249 		} else {
250 			throw new Exception("Probably not a wav file, or else pass bigger chunks please");
251 		}
252 
253 		if(header.header != ['R', 'I', 'F', 'F'])
254 			throw new Exception("Not a wav file; no RIFF header");
255 		if(header.type != ['W', 'A', 'V', 'E'])
256 			throw new Exception("Not a wav file");
257 		// so technically the spec does NOT require fmt to be the first chunk..
258 		// but im gonna just be lazy
259 		if(header.fmtHeader != ['f', 'm', 't', ' '])
260 			throw new Exception("Malformed or unsupported wav file");
261 
262 		if(header.fmtHeaderSize < 16)
263 			throw new Exception("Unsupported wav format header");
264 
265 		auto additionalSkip = header.fmtHeaderSize - 16;
266 
267 		if(header.audioFormat != 1)
268 			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.");
269 
270 		this.numberOfChannels = header.numberOfChannels;
271 		this.sampleRate = header.sampleRate;
272 		this.bitsPerSample = header.bitsPerSample;
273 
274 		if(header.bytesPerSampleTimesChannels != header.bitsPerSample * header.numberOfChannels / 8)
275 			throw new Exception("Malformed wav file: header.bytesPerSampleTimesChannels didn't match");
276 		if(header.bytesPerSeconds != header.bytesPerSampleTimesChannels * header.sampleRate)
277 			throw new Exception("Malformed wav file: header.bytesPerSeconds didn't match");
278 
279 		this.frontBuffer = this.frontBuffer[additionalSkip .. $];
280 
281 		static struct ChunkHeader {
282 			align(1):
283 			ubyte[4] type;
284 			uint size;
285 		}
286 		static assert(ChunkHeader.sizeof == 8);
287 
288 		ChunkHeader current;
289 		ubyte[] chunkHeader = (cast(ubyte*) &current)[0 .. current.sizeof];
290 
291 		another_chunk:
292 
293 		// now we're at the next chunk. want to skip until we hit data.
294 		if(this.frontBuffer.length < chunkHeader.length)
295 			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");
296 
297 		chunkHeader[] = frontBuffer[0 .. chunkHeader.length];
298 		frontBuffer = frontBuffer[chunkHeader.length .. $];
299 
300 		if(current.type != ['d', 'a', 't', 'a']) {
301 			// skip unsupported chunk...
302 			drop_more:
303 			if(frontBuffer.length > current.size) {
304 				frontBuffer = frontBuffer[current.size .. $];
305 			} else {
306 				current.size -= frontBuffer.length;
307 				underlying.popFront();
308 				if(underlying.empty) {
309 					throw new Exception("Ran out of data while trying to read wav chunks");
310 				} else {
311 					frontBuffer = underlying.front;
312 					goto drop_more;
313 				}
314 			}
315 			goto another_chunk;
316 		} else {
317 			this.remainingDataLength = current.size;
318 		}
319 
320 		this.dataLength = this.remainingDataLength;
321 	}
322 
323 	@property const(ubyte)[] front() {
324 		return frontBuffer;
325 	}
326 
327 	version(none)
328 	void consumeBytes(size_t count) {
329 		if(this.frontBuffer.length)
330 			this.frontBuffer = this.frontBuffer[count .. $];
331 	}
332 
333 	void popFront() {
334 		remainingDataLength -= front.length;
335 
336 		underlying.popFront();
337 		if(underlying.empty)
338 			frontBuffer = null;
339 		else
340 			frontBuffer = underlying.front;
341 	}
342 
343 	@property bool empty() {
344 		return remainingDataLength == 0 || this.underlying.empty;
345 	}
346 }
347 
348 /++
349 	Convenience constructor for [WavReader]
350 
351 	To read from a file, pass a filename, a FILE*, or a range that
352 	reads chunks from a file.
353 
354 	To read from a memory block, just pass it a `ubyte[]` slice.
355 +/
356 WavReader!T wavReader(T)(T t) {
357 	return WavReader!T(t);
358 }
359 
360 /// ditto
361 WavReader!DataBlock wavReader(const(ubyte)[] data) {
362 	return WavReader!DataBlock(DataBlock(data));
363 }
364 
365 struct DataBlock {
366 	const(ubyte)[] front;
367 	bool empty() { return front.length == 0; }
368 	void popFront() { front = null; }
369 }
370 
371 /// Construct a [WavReader] from a filename.
372 WavReader!CFileChunks wavReader(string filename) {
373 	assert(filename.length < 290);
374 
375 	char[300] fn;
376 	fn[0 .. filename.length] = filename[];
377 	fn[filename.length] = 0;
378 
379 	auto fp = fopen(fn.ptr, "rb");
380 	if(fp is null)
381 		throw new Exception("wav file unopenable"); // FIXME details
382 
383 	return WavReader!CFileChunks(fp);
384 }
385 
386 struct CFileChunks {
387 	FILE* fp;
388 	this(FILE* fp) {
389 		this.fp = fp;
390 		buffer = new ubyte[](4096);
391 		refcount = new int;
392 		*refcount = 1;
393 		popFront();
394 	}
395 	this(this) {
396 		if(refcount !is null)
397 			(*refcount) += 1;
398 	}
399 	~this() {
400 		if(refcount is null) return;
401 		(*refcount) -= 1;
402 		if(*refcount == 0) {
403 			fclose(fp);
404 		}
405 	}
406 
407 	//ubyte[4096] buffer;
408 	ubyte[] buffer;
409 	int* refcount;
410 
411 	ubyte[] front;
412 
413 	void popFront() {
414 		auto got = fread(buffer.ptr, 1, buffer.length, fp);
415 		front = buffer[0 .. got];
416 	}
417 
418 	bool empty() {
419 		return front.length == 0 && (feof(fp) ? true : false);
420 	}
421 }