DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
WavFile.h
Go to the documentation of this file.
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
41#include "AudioFile.h"
42
43#include <algorithm>
44#include <cmath>
45#include <cstdint>
46#include <cstring>
47#include <filesystem>
48#include <fstream>
49#include <type_traits>
50#include <vector>
51
52namespace dspark {
53
66class WavFile : public AudioFile
67{
68public:
69 ~WavFile() override { close(); }
70
71 // -- AudioFile interface ---------------------------------------------------
72
78 [[nodiscard]] bool openRead(const std::filesystem::path& path) override
79 {
80 close();
81 file_.open(path, std::ios::binary | std::ios::in);
82 if (!file_.is_open()) return false;
83
84 if (!parseHeader())
85 {
86 close();
87 return false;
88 }
89
90 mode_ = Mode::Read;
91 resizeIoBuffer();
92 return true;
93 }
94
106 [[nodiscard]] bool openWrite(const std::filesystem::path& path, const AudioFileInfo& info) override
107 {
108 close();
109
110 // Validate/normalize the requested format first so we never emit a
111 // corrupt header AND never destroy an existing file on a doomed
112 // request. WAV supports PCM at 8/16/24/32-bit and IEEE float at
113 // 32/64-bit; the fmt chunk stores the rate as uint32.
114 if (info.numChannels < 1 || info.numChannels > kMaxChannels) return false;
115 if (!(info.sampleRate >= 1.0) || info.sampleRate > 4294967295.0) return false;
116 if (info.isFloatingPoint)
117 {
118 if (info.bitsPerSample != 32 && info.bitsPerSample != 64)
119 return false;
120 }
121 else if (info.bitsPerSample != 8 && info.bitsPerSample != 16 &&
122 info.bitsPerSample != 24 && info.bitsPerSample != 32)
123 {
124 return false;
125 }
126
127 file_.open(path, std::ios::binary | std::ios::out | std::ios::trunc);
128 if (!file_.is_open()) return false;
129
130 info_ = info;
131 mode_ = Mode::Write;
132 totalFramesWritten_ = 0;
133
134 // Write placeholder header (sizes will be patched on close)
135 if (!writeHeader())
136 {
137 close();
138 return false;
139 }
140
141 resizeIoBuffer();
142 return true;
143 }
144
145 [[nodiscard]] AudioFileInfo getInfo() const override { return info_; }
146
147 [[nodiscard]] bool readSamples(AudioBufferView<float> dest) override
148 {
149 // An open-but-empty file (zero-length data chunk) reads zero frames
150 // successfully; only unopened handles or I/O errors report failure.
151 if (mode_ == Mode::Read && file_.is_open() && info_.numSamples == 0)
152 return true;
153 return readSamples(dest, 0, info_.numSamples);
154 }
155
156 [[nodiscard]] bool readSamples(AudioBufferView<float> dest,
157 int64_t startFrame, int64_t numFrames) override
158 {
159 if (mode_ != Mode::Read || !file_.is_open()) return false;
160 if (startFrame < 0 || numFrames <= 0) return false;
161 if (startFrame + numFrames > info_.numSamples) return false;
162
163 const int frameSize = static_cast<int>(info_.bitsPerSample / 8 * info_.numChannels);
164
165 // Seek to the exact start frame within the 'data' chunk
166 auto seekPos = dataChunkOffset_ + static_cast<std::streamoff>(startFrame * frameSize);
167 file_.seekg(seekPos, std::ios::beg);
168 if (!file_.good()) return false;
169
170 const int nCh = std::min(dest.getNumChannels(), static_cast<int>(info_.numChannels));
171 int64_t framesRemaining = std::min(numFrames, static_cast<int64_t>(dest.getNumSamples()));
172 int64_t destOffset = 0;
173
174 while (framesRemaining > 0)
175 {
176 const auto toRead = std::min<int64_t>(framesRemaining, kChunkFrames);
177 const auto rawBytes = static_cast<std::streamsize>(toRead * frameSize);
178
179 file_.read(reinterpret_cast<char*>(ioBuffer_.data()), rawBytes);
180 if (file_.gcount() != rawBytes) return false;
181
182 deinterleave(ioBuffer_.data(), dest, nCh, static_cast<int>(toRead), static_cast<int>(destOffset));
183
184 destOffset += toRead;
185 framesRemaining -= toRead;
186 }
187
188 return true;
189 }
190
191 [[nodiscard]] bool writeSamples(AudioBufferView<const float> src) override
192 {
193 if (mode_ != Mode::Write || !file_.is_open()) return false;
194
195 const int nCh = static_cast<int>(info_.numChannels);
196 const int srcCh = std::min(src.getNumChannels(), nCh);
197 const int nS = src.getNumSamples();
198 const int frameSize = static_cast<int>(info_.bitsPerSample / 8) * nCh;
199
200 // Classic RIFF sizes are 32-bit: refuse a write that would push the
201 // data chunk past what the header can describe (header <= 128 bytes).
202 const int64_t dataAfter = (totalFramesWritten_ + nS) * static_cast<int64_t>(frameSize);
203 if (dataAfter > static_cast<int64_t>(0xFFFFFFFFu) - 128) return false;
204
205 int framesRemaining = nS;
206 int srcOffset = 0;
207
208 while (framesRemaining > 0)
209 {
210 const int toWrite = std::min(framesRemaining, kChunkFrames);
211
212 interleave(src, ioBuffer_.data(), nCh, srcCh, toWrite, srcOffset);
213
214 auto rawBytes = static_cast<std::streamsize>(static_cast<int64_t>(toWrite) * frameSize);
215 file_.write(reinterpret_cast<const char*>(ioBuffer_.data()), rawBytes);
216 if (!file_.good()) return false;
217
218 srcOffset += toWrite;
219 framesRemaining -= toWrite;
220 }
221
222 totalFramesWritten_ += nS;
223 return true;
224 }
225
226 void close() override
227 {
228 if (!file_.is_open()) { mode_ = Mode::Closed; info_ = {}; return; }
229
230 if (mode_ == Mode::Write)
231 finaliseHeader();
232
233 file_.close();
234 ioBuffer_.clear();
235 ioBuffer_.shrink_to_fit();
236 mode_ = Mode::Closed;
237 info_ = {}; // getInfo() contract: defaults once no file is open
238 }
239
240 [[nodiscard]] bool isOpen() const noexcept override
241 {
242 return file_.is_open() && mode_ != Mode::Closed;
243 }
244
245private:
246 // -- RIFF/WAV constants ----------------------------------------------------
247
248 static constexpr uint16_t kFormatPCM = 1;
249 static constexpr uint16_t kFormatIEEEFloat = 3;
250 static constexpr uint16_t kFormatExtensible = 0xFFFE;
251 static constexpr int kChunkFrames = 8192;
252 static constexpr uint32_t kMaxChannels = 64;
253
254 enum class Mode { Closed, Read, Write };
255
256 std::fstream file_;
257 Mode mode_ = Mode::Closed;
258 AudioFileInfo info_ {};
259 std::vector<uint8_t> ioBuffer_;
260
261 std::streamoff dataChunkOffset_ = 0;
262 std::streamoff dataChunkSizeOffset_ = 0;
263 uint32_t dataChunkSize_ = 0;
264 int64_t totalFramesWritten_ = 0;
265
266 void resizeIoBuffer()
267 {
268 const size_t bytesNeeded = static_cast<size_t>(kChunkFrames)
269 * info_.numChannels * (info_.bitsPerSample / 8);
270 if (ioBuffer_.size() < bytesNeeded)
271 ioBuffer_.resize(bytesNeeded);
272 }
273
274 // -- Header parsing (read) -------------------------------------------------
275
276 bool parseHeader()
277 {
278 char riffId[4];
279 if (!readBytes(riffId, 4)) return false;
280 if (std::memcmp(riffId, "RIFF", 4) != 0) return false;
281
282 uint32_t riffSize = 0;
283 if (!readLE32(riffSize)) return false;
284 (void)riffSize; // Deliberately ignored; many DAWs write invalid lengths
285
286 char waveId[4];
287 if (!readBytes(waveId, 4)) return false;
288 if (std::memcmp(waveId, "WAVE", 4) != 0) return false;
289
290 bool hasFmt = false, hasData = false;
291
292 while (file_.good() && !(hasFmt && hasData))
293 {
294 char chunkId[4];
295 uint32_t chunkSize = 0;
296 if (!readBytes(chunkId, 4)) break;
297 if (!readLE32(chunkSize)) break;
298
299 // RIFF chunks are word-aligned: an odd-sized chunk is followed by
300 // one pad byte that is not part of the declared size. All skips
301 // below must account for it (in 64-bit arithmetic, so a corrupt
302 // size near UINT32_MAX cannot wrap the seek to zero).
303 const std::streamoff pad = (chunkSize & 1);
304
305 if (std::memcmp(chunkId, "fmt ", 4) == 0)
306 {
307 if (!parseFmtChunk(chunkSize)) return false;
308 if (pad) file_.seekg(pad, std::ios::cur);
309 hasFmt = true;
310 }
311 else if (std::memcmp(chunkId, "data", 4) == 0)
312 {
313 auto currentPos = file_.tellg();
314 file_.seekg(0, std::ios::end);
315 auto fileEnd = file_.tellg();
316 file_.seekg(currentPos);
317
318 auto remaining = fileEnd - currentPos;
319 if (static_cast<std::streamoff>(chunkSize) > remaining)
320 chunkSize = static_cast<uint32_t>(remaining);
321
322 dataChunkOffset_ = file_.tellg();
323 dataChunkSize_ = chunkSize;
324 hasData = true;
325
326 if (!hasFmt)
327 {
328 file_.seekg(static_cast<std::streamoff>(chunkSize) + pad, std::ios::cur);
329 }
330 }
331 else
332 {
333 file_.seekg(static_cast<std::streamoff>(chunkSize) + pad, std::ios::cur);
334 }
335 }
336
337 // numSamples is derived here rather than inside the 'data' branch: if
338 // 'data' precedes 'fmt ' (legal but unusual chunk order), bit depth
339 // and channel count were still unknown at that point.
340 if (hasFmt && hasData)
341 {
342 const int bytesPerFrame = static_cast<int>(info_.bitsPerSample / 8 * info_.numChannels);
343 if (bytesPerFrame > 0)
344 info_.numSamples = static_cast<int64_t>(dataChunkSize_) / bytesPerFrame;
345 }
346
347 return hasFmt && hasData;
348 }
349
350 bool parseFmtChunk(uint32_t chunkSize)
351 {
352 if (chunkSize < 16) return false;
353
354 uint16_t audioFormat = 0;
355 uint16_t numChannels = 0;
356 uint32_t sampleRate = 0;
357 uint32_t byteRate = 0;
358 uint16_t blockAlign = 0;
359 uint16_t bitsPerSample = 0;
360
361 if (!readLE16(audioFormat)) return false;
362 if (!readLE16(numChannels)) return false;
363 if (!readLE32(sampleRate)) return false;
364 if (!readLE32(byteRate)) return false;
365 (void)byteRate;
366 if (!readLE16(blockAlign)) return false;
367 (void)blockAlign;
368 if (!readLE16(bitsPerSample)) return false;
369
370 if (audioFormat == kFormatExtensible && chunkSize >= 40)
371 {
372 uint16_t cbSize = 0;
373 readLE16(cbSize);
374 (void)cbSize; // Standard layout assumed; trailing bytes skipped below
375
376 // wValidBitsPerSample tells how many bits are significant, but the
377 // container width (wBitsPerSample) governs the stored layout: valid
378 // bits are left-justified, so decoding by container width yields
379 // the exact same values. Do NOT adopt validBits as the frame size
380 // (that mis-strides real 24-in-32 files). Read it only for sanity.
381 uint16_t validBitsPerSample = 0;
382 readLE16(validBitsPerSample);
383 if (validBitsPerSample > bitsPerSample) return false;
384
385 uint32_t channelMask = 0;
386 readLE32(channelMask);
387 (void)channelMask;
388
389 uint16_t subFormat = 0;
390 if (!readLE16(subFormat)) return false;
391 audioFormat = subFormat;
392
393 file_.seekg(14, std::ios::cur); // rest of the 16-byte subformat GUID
394
395 // Skip any extra bytes after the 40 standard EXTENSIBLE bytes so
396 // the chunk walker stays aligned on unusual writers.
397 if (chunkSize > 40)
398 file_.seekg(static_cast<std::streamoff>(chunkSize - 40), std::ios::cur);
399 }
400 else if (chunkSize > 16)
401 {
402 auto skip = static_cast<std::streamoff>(chunkSize - 16);
403 file_.seekg(skip, std::ios::cur);
404 }
405
406 info_.sampleRate = static_cast<double>(sampleRate);
407 info_.numChannels = numChannels;
408 info_.bitsPerSample = bitsPerSample;
409 info_.isFloatingPoint = (audioFormat == kFormatIEEEFloat);
410
411 if (audioFormat != kFormatPCM && audioFormat != kFormatIEEEFloat) return false;
412 if (sampleRate == 0) return false;
413 if (numChannels == 0 || numChannels > kMaxChannels) return false;
414 if (info_.isFloatingPoint)
415 {
416 if (bitsPerSample != 32 && bitsPerSample != 64) return false;
417 }
418 else if (bitsPerSample != 8 && bitsPerSample != 16 &&
419 bitsPerSample != 24 && bitsPerSample != 32)
420 {
421 // Integer PCM has no 64-bit variant; without this gate a PCM-64
422 // header would pass validation and then "read" silence (no
423 // decoder branch exists for it).
424 return false;
425 }
426
427 return true;
428 }
429
430 // -- Header writing --------------------------------------------------------
431
432 bool writeHeader()
433 {
434 const uint16_t formatTag = getWriteFormatTag();
435 const bool extensible = (formatTag == kFormatExtensible);
436 const uint32_t fmtChunkSize = extensible ? 40u : 16u;
437
438 const int bytesPerSample = static_cast<int>(info_.bitsPerSample / 8);
439 const auto blockAlign = static_cast<uint16_t>(info_.numChannels * bytesPerSample);
440 const auto rate = static_cast<uint32_t>(std::llround(info_.sampleRate));
441 const uint64_t byteRate64 = static_cast<uint64_t>(rate) * blockAlign;
442 const auto byteRate = static_cast<uint32_t>(
443 std::min<uint64_t>(byteRate64, 0xFFFFFFFFu));
444
445 writeBytes("RIFF", 4);
446 writeLE32(0); // Patched on close
447 writeBytes("WAVE", 4);
448
449 writeBytes("fmt ", 4);
450 writeLE32(fmtChunkSize);
451 writeLE16(formatTag);
452 writeLE16(static_cast<uint16_t>(info_.numChannels));
453 writeLE32(rate);
454 writeLE32(byteRate);
455 writeLE16(blockAlign);
456 writeLE16(static_cast<uint16_t>(info_.bitsPerSample));
457
458 if (extensible)
459 {
460 writeLE16(22);
461 writeLE16(static_cast<uint16_t>(info_.bitsPerSample));
462 writeLE32(0);
463
464 uint16_t subFormat = info_.isFloatingPoint ? kFormatIEEEFloat : kFormatPCM;
465 writeLE16(subFormat);
466 static constexpr uint8_t kGuidSuffix[14] = {
467 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00,
468 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71
469 };
470 writeBytes(reinterpret_cast<const char*>(kGuidSuffix), 14);
471 }
472
473 writeBytes("data", 4);
474 dataChunkSizeOffset_ = file_.tellp();
475 writeLE32(0); // Patched on close
476
477 dataChunkOffset_ = file_.tellp();
478 return file_.good();
479 }
480
481 void finaliseHeader()
482 {
483 if (!file_.is_open()) return;
484
485 // A transient failure (e.g. one failed writeSamples) leaves sticky
486 // failbits that would turn the header patch into a silent no-op.
487 // Clear them so whatever WAS written gets a consistent header.
488 file_.clear();
489
490 const int bytesPerSample = static_cast<int>(info_.bitsPerSample / 8);
491 const auto dataSize = static_cast<uint32_t>(
492 totalFramesWritten_ * info_.numChannels * bytesPerSample);
493
494 file_.seekp(dataChunkSizeOffset_, std::ios::beg);
495 writeLE32(dataSize);
496
497 file_.seekp(0, std::ios::end);
498 auto fileSize = static_cast<uint32_t>(file_.tellp());
499 uint32_t riffSize = fileSize - 8;
500 file_.seekp(4, std::ios::beg);
501 writeLE32(riffSize);
502
503 file_.flush();
504 }
505
506 [[nodiscard]] uint16_t getWriteFormatTag() const noexcept
507 {
508 if (info_.numChannels > 2 || info_.bitsPerSample > 16) return kFormatExtensible;
509 if (info_.isFloatingPoint) return kFormatIEEEFloat;
510 return kFormatPCM;
511 }
512
513 // -- Sample conversion (read: raw to float) --------------------------------
514
515 void deinterleave(const uint8_t* raw, AudioBufferView<float>& dest, int nCh, int numFrames, int destOffset) const
516 {
517 if (info_.isFloatingPoint) {
518 if (info_.bitsPerSample == 32) deinterleaveImpl<float, 32>(raw, dest, nCh, numFrames, destOffset);
519 else deinterleaveImpl<double, 64>(raw, dest, nCh, numFrames, destOffset);
520 } else {
521 switch (info_.bitsPerSample) {
522 case 8: deinterleaveImpl<uint8_t, 8>(raw, dest, nCh, numFrames, destOffset); break;
523 case 16: deinterleaveImpl<int16_t, 16>(raw, dest, nCh, numFrames, destOffset); break;
524 case 24: deinterleaveImpl<int32_t, 24>(raw, dest, nCh, numFrames, destOffset); break;
525 case 32: deinterleaveImpl<int32_t, 32>(raw, dest, nCh, numFrames, destOffset); break;
526 }
527 }
528 }
529
530 template <typename T, int Bits>
531 void deinterleaveImpl(const uint8_t* raw, AudioBufferView<float>& dest, int nCh, int numFrames, int destOffset) const
532 {
533 const int bytesPerSample = Bits / 8;
534 const int stride = static_cast<int>(info_.numChannels) * bytesPerSample;
535
536 // Channel-major loop: each output channel is written contiguously
537 // (cache-friendly); the byte-wise strided source reads are the cost
538 // of endian-safe decoding.
539 for (int ch = 0; ch < nCh; ++ch)
540 {
541 float* out = dest.getChannel(ch) + destOffset;
542 const uint8_t* channelRaw = raw + (ch * bytesPerSample);
543
544 for (int f = 0; f < numFrames; ++f)
545 {
546 const uint8_t* ptr = channelRaw + (f * stride);
547
548 if constexpr (Bits == 8) {
549 out[f] = (static_cast<float>(ptr[0]) - 128.0f) * (1.0f / 128.0f);
550 }
551 else if constexpr (Bits == 16) {
552 auto val = static_cast<int16_t>(ptr[0] | (ptr[1] << 8));
553 out[f] = static_cast<float>(val) * (1.0f / 32768.0f);
554 }
555 else if constexpr (Bits == 24) {
556 int32_t val = ptr[0] | (ptr[1] << 8) | (ptr[2] << 16);
557 if (val & 0x800000) val |= 0xFF000000;
558 out[f] = static_cast<float>(val) * (1.0f / 8388608.0f);
559 }
560 else if constexpr (Bits == 32 && std::is_same_v<T, int32_t>) {
561 // Assemble in unsigned to avoid signed overflow UB on the high
562 // byte (ptr[3] << 24 with the top bit set), then reinterpret.
563 int32_t val = static_cast<int32_t>(
564 static_cast<uint32_t>(ptr[0])
565 | (static_cast<uint32_t>(ptr[1]) << 8)
566 | (static_cast<uint32_t>(ptr[2]) << 16)
567 | (static_cast<uint32_t>(ptr[3]) << 24));
568 out[f] = static_cast<float>(val) * (1.0f / 2147483648.0f);
569 }
570 else if constexpr (Bits == 32 && std::is_same_v<T, float>) {
571 float v; std::memcpy(&v, ptr, 4); out[f] = v;
572 }
573 else if constexpr (Bits == 64) {
574 double v; std::memcpy(&v, ptr, 8); out[f] = static_cast<float>(v);
575 }
576 }
577 }
578 }
579
580 // -- Sample conversion (write: float to raw) --------------------------------
581
582 void interleave(AudioBufferView<const float> src, uint8_t* raw,
583 int fileCh, int srcCh, int numFrames, int srcOffset) const
584 {
585 if (info_.isFloatingPoint) {
586 if (info_.bitsPerSample == 32) interleaveImpl<float, 32>(src, raw, fileCh, srcCh, numFrames, srcOffset);
587 else interleaveImpl<double, 64>(src, raw, fileCh, srcCh, numFrames, srcOffset);
588 } else {
589 switch (info_.bitsPerSample) {
590 case 8: interleaveImpl<uint8_t, 8>(src, raw, fileCh, srcCh, numFrames, srcOffset); break;
591 case 16: interleaveImpl<int16_t, 16>(src, raw, fileCh, srcCh, numFrames, srcOffset); break;
592 case 24: interleaveImpl<int32_t, 24>(src, raw, fileCh, srcCh, numFrames, srcOffset); break;
593 case 32: interleaveImpl<int32_t, 32>(src, raw, fileCh, srcCh, numFrames, srcOffset); break;
594 }
595 }
596 }
597
598 template <typename T, int Bits>
599 void interleaveImpl(AudioBufferView<const float> src, uint8_t* raw,
600 int fileCh, int srcCh, int numFrames, int srcOffset) const
601 {
602 const int bytesPerSample = Bits / 8;
603
604 // Frame-major loop is required here to emit contiguous interleaved
605 // data. Channels the source view does not carry are written as
606 // silence (same contract as Mp3File) instead of reading out of range.
607 for (int f = 0; f < numFrames; ++f)
608 {
609 for (int ch = 0; ch < fileCh; ++ch)
610 {
611 const float value = (ch < srcCh) ? src.getChannel(ch)[srcOffset + f] : 0.0f;
612 uint8_t* ptr = raw + ((f * fileCh + ch) * bytesPerSample);
613
614 // Precise rounding & strict clipping bounds
615 if constexpr (Bits == 8) {
616 auto val = static_cast<uint8_t>(std::clamp(std::round(value * 127.0f) + 128.0f, 0.0f, 255.0f));
617 ptr[0] = val;
618 }
619 else if constexpr (Bits == 16) {
620 auto val = static_cast<int16_t>(std::clamp(std::round(value * 32768.0f), -32768.0f, 32767.0f));
621 ptr[0] = val & 0xFF; ptr[1] = (val >> 8) & 0xFF;
622 }
623 else if constexpr (Bits == 24) {
624 auto val = static_cast<int32_t>(std::clamp(std::round(value * 8388608.0f), -8388608.0f, 8388607.0f));
625 ptr[0] = val & 0xFF; ptr[1] = (val >> 8) & 0xFF; ptr[2] = (val >> 16) & 0xFF;
626 }
627 else if constexpr (Bits == 32 && std::is_same_v<T, int32_t>) {
628 auto val = static_cast<int32_t>(std::clamp(std::round(static_cast<double>(value) * 2147483648.0), -2147483648.0, 2147483647.0));
629 ptr[0] = val & 0xFF; ptr[1] = (val >> 8) & 0xFF; ptr[2] = (val >> 16) & 0xFF; ptr[3] = (val >> 24) & 0xFF;
630 }
631 else if constexpr (Bits == 32 && std::is_same_v<T, float>) {
632 std::memcpy(ptr, &value, 4);
633 }
634 else if constexpr (Bits == 64) {
635 double d = static_cast<double>(value);
636 std::memcpy(ptr, &d, 8);
637 }
638 }
639 }
640 }
641
642 // -- Low-level I/O helpers (little-endian) ---------------------------------
643
644 bool readBytes(char* buf, int n)
645 {
646 file_.read(buf, n);
647 return file_.gcount() == n;
648 }
649
650 bool readLE16(uint16_t& val)
651 {
652 uint8_t b[2];
653 file_.read(reinterpret_cast<char*>(b), 2);
654 if (file_.gcount() != 2) return false;
655 val = static_cast<uint16_t>(b[0]) | (static_cast<uint16_t>(b[1]) << 8);
656 return true;
657 }
658
659 bool readLE32(uint32_t& val)
660 {
661 uint8_t b[4];
662 file_.read(reinterpret_cast<char*>(b), 4);
663 if (file_.gcount() != 4) return false;
664 val = static_cast<uint32_t>(b[0])
665 | (static_cast<uint32_t>(b[1]) << 8)
666 | (static_cast<uint32_t>(b[2]) << 16)
667 | (static_cast<uint32_t>(b[3]) << 24);
668 return true;
669 }
670
671 void writeBytes(const char* buf, int n)
672 {
673 file_.write(buf, n);
674 }
675
676 void writeLE16(uint16_t val)
677 {
678 uint8_t b[2] = {
679 static_cast<uint8_t>(val & 0xFF),
680 static_cast<uint8_t>((val >> 8) & 0xFF)
681 };
682 file_.write(reinterpret_cast<const char*>(b), 2);
683 }
684
685 void writeLE32(uint32_t val)
686 {
687 uint8_t b[4] = {
688 static_cast<uint8_t>(val & 0xFF),
689 static_cast<uint8_t>((val >> 8) & 0xFF),
690 static_cast<uint8_t>((val >> 16) & 0xFF),
691 static_cast<uint8_t>((val >> 24) & 0xFF)
692 };
693 file_.write(reinterpret_cast<const char*>(b), 4);
694 }
695};
696
697} // namespace dspark
Abstract interface for reading and writing audio files.
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
int getNumSamples() const noexcept
Returns the number of samples per channel.
int getNumChannels() const noexcept
Returns the number of channels in this view.
Abstract base class for audio file readers and writers.
Definition AudioFile.h:107
Complete WAV file reader and writer in pure C++20.
Definition WavFile.h:67
AudioFileInfo getInfo() const override
Retrieves metadata of the currently opened file.
Definition WavFile.h:145
bool readSamples(AudioBufferView< float > dest) override
Reads samples from the start of the file into the destination view.
Definition WavFile.h:147
bool isOpen() const noexcept override
Checks if a valid file handle is currently open.
Definition WavFile.h:240
~WavFile() override
Definition WavFile.h:69
bool writeSamples(AudioBufferView< const float > src) override
Writes samples from the view to the file.
Definition WavFile.h:191
bool openRead(const std::filesystem::path &path) override
Opens a WAV file for reading.
Definition WavFile.h:78
bool openWrite(const std::filesystem::path &path, const AudioFileInfo &info) override
Opens a WAV file for writing, overwriting if it exists.
Definition WavFile.h:106
void close() override
Finalizes file headers and releases system handles.
Definition WavFile.h:226
bool readSamples(AudioBufferView< float > dest, int64_t startFrame, int64_t numFrames) override
Reads a specific range of sample frames. Useful for chunked streaming.
Definition WavFile.h:156
Main namespace for the DSPark framework.
Metadata describing an audio file's format and dimensions.
Definition AudioFile.h:39
uint32_t numChannels
Number of audio channels (1 = mono, 2 = stereo).
Definition AudioFile.h:44
bool isFloatingPoint
True if the file stores floating-point samples (IEEE 754).
Definition AudioFile.h:59
uint32_t bitsPerSample
Bits per sample in the stored format (8, 16, 24, 32, 64).
Definition AudioFile.h:56
double sampleRate
Sample rate in Hz (e.g., 44100.0, 48000.0, 96000.0).
Definition AudioFile.h:41
int64_t numSamples
Total number of sample frames in the file.
Definition AudioFile.h:47