The WAV file format (Waveform Audio File Format) is widely used on Microsoft Windows systems for storing uncompressed audio data. Its file extension .wav is commonly used to refer to this format. WAV files are ideal for applications requiring high-quality audio because they store raw sound data without any compression.

What is a WAV File?

A WAV file stores audio data in a structure that mimics the attributes of sound itself. Think of it as a musical “music box” where the sound is encoded directly into the file without compression. This results in rich, high-quality audio, but it also means larger file sizes compared to formats like MP3 or FLAC.

A WAV file consists of three main components:

  1. RIFF Header: Describes the file format and structure.
  2. ‘fmt ’ Chunk: Contains metadata about the audio data (e.g., sample rate, bit depth).
  3. ‘data’ Chunk: Stores the actual audio samples.

WAV File Structure Breakdown

RIFF Header

The RIFF header is the first part of the WAV file. It defines the file type and overall size. Key fields include:

  • Chunk ID: Always “RIFF” (4 bytes).
  • Chunk Size: File size minus the first 8 bytes (4 bytes).
  • Format: Always “WAVE” (4 bytes).

‘fmt ’ Chunk

This chunk specifies the format of the audio data. Key fields include:

  • Audio Format: PCM = 1 (Uncompressed audio).
  • Number of Channels: Mono = 1, Stereo = 2.
  • Sample Rate: Number of samples per second (e.g., 44,100 Hz).
  • Bits Per Sample: Common values are 16 or 24 bits.

‘data’ Chunk

This chunk contains the raw sample data, which is the actual sound information. Key fields include:

  • Subchunk2 Size: Size of the audio data in bytes.
  • Data: The raw audio samples.

Writing WAV Files in C++

Below is a C++ code example showing how to write a WAV file. The code focuses on creating the RIFF header, ‘fmt ’ chunk, and writing audio data. For brevity, only the core parts of the implementation are included.

Header Creation

This part writes the RIFF header and ‘fmt ’ chunk. The WAV file format uses little-endian encoding, meaning data is stored with the least significant byte first.

#include <fstream>
#include <stdexcept>
 
class WAV {
public:
  WAV(const std::string& filename, int sampleRate, int bitsPerSample, int channels)
    : file(filename, std::ios::binary), sampleRate(sampleRate), bitsPerSample(bitsPerSample), channels(channels) {
    if (!file.is_open()) throw std::runtime_error("Unable to open file.");
 
    // Write RIFF header
    file.write("RIFF", 4);
    file.write("\0\0\0\0", 4); // Placeholder for file size
    file.write("WAVE", 4);
 
    // Write 'fmt ' chunk
    file.write("fmt ", 4);
    writeLittleEndian(16, 4); // Chunk size
    writeLittleEndian(1, 2);  // Audio format (PCM)
    writeLittleEndian(channels, 2);
    writeLittleEndian(sampleRate, 4);
    writeLittleEndian(sampleRate * channels * bitsPerSample / 8, 4); // Byte rate
    writeLittleEndian(channels * bitsPerSample / 8, 2);             // Block align
    writeLittleEndian(bitsPerSample, 2);
  }
 
private:
  std::ofstream file;
  int sampleRate, bitsPerSample, channels;
 
  // Helper for writing little-endian data
  template <typename T>
  void writeLittleEndian(T value, unsigned size) {
    while (size--) {
      file.put(static_cast<char>(value & 0xFF));
      value >>= 8;
    }
  }
};

Writing Audio Data

This part stores the raw audio data in the ‘data’ chunk. The samples are normalized before being written to ensure they stay within valid amplitude bounds.

void WAV::writeData(const std::vector<std::vector<double>>& audioData) {
  // Write 'data' chunk header
  file.write("data", 4);
  file.write("\0\0\0\0", 4); // Placeholder for data size
 
  // Normalize and write audio samples
  for (const auto& frame : audioData) {
    for (double sample : frame) {
      int intSample = static_cast<int>(sample * maxAmplitude());
      writeLittleEndian(intSample, bitsPerSample / 8);
    }
  }
 
  // Update chunk sizes
  updateSizes();
}
 
double WAV::maxAmplitude() const {
  return pow(2, bitsPerSample - 1) - 1;
}
 
void WAV::updateSizes() {
  size_t fileSize = file.tellp();
 
  // Update 'data' chunk size
  file.seekp(40); // Position of 'data' chunk size
  writeLittleEndian(fileSize - 44, 4); // 44 bytes header + 'fmt ' chunk
 
  // Update RIFF header size
  file.seekp(4); // Position of RIFF chunk size
  writeLittleEndian(fileSize - 8, 4); // 8 bytes RIFF header
}

Example Usage

int main() {
  WAV wav("example.wav", 44100, 16, 2); // Stereo, 44.1kHz, 16-bit
  std::vector<std::vector<double>> audioData(44100, std::vector<double>(2, 0.0)); // 1 second of silence
  wav.writeData(audioData);
  return 0;
}

Why Use WAV?

  • Lossless Quality: WAV files preserve the original audio data without compression, making them suitable for professional audio editing and archival purposes.
  • Simple Format: The straightforward structure of WAV files makes them easy to manipulate programmatically.

Limitations

  • File Size: WAV files are significantly larger than compressed formats like MP3 or AAC.
  • Lack of Metadata: While metadata can be added, WAV does not natively support advanced tagging (e.g., album art, lyrics).

References