Task 804: .WAV File Format

Task 804: .WAV File Format

WAV File Format Specifications

The .WAV (Waveform Audio File Format) is a subset of Microsoft's RIFF (Resource Interchange File Format) specification. It is primarily used for storing uncompressed audio data, though it supports compressed formats as well. The file structure consists of a RIFF header followed by chunks, with the mandatory "fmt " (format) and "data" chunks for basic PCM audio. Optional chunks like "fact" may appear for non-PCM formats or additional metadata. All multi-byte integers are stored in little-endian order unless specified otherwise (e.g., in rare big-endian "RIFX" variants). The format supports various audio encodings, but PCM (Pulse-Code Modulation) is the most common.

1. List of Properties Intrinsic to the File Format

Based on the standard specifications, the following properties represent the core fields in a .WAV file's structure. These are derived from the RIFF container and common chunks ("fmt ", "fact", and "data"). Offsets are provided for a minimal PCM file; actual offsets may vary if additional chunks are present. Properties are listed with their byte sizes, typical offsets (for reference in a basic file), and descriptions. Only header and metadata properties are included, excluding the raw audio data itself.

  • ChunkID: 4 bytes, offset 0, description: Identifier for the RIFF container, must be "RIFF" (ASCII: 0x52494646) for little-endian files.
  • ChunkSize: 4 bytes, offset 4, description: Size of the entire file minus 8 bytes (little-endian uint32_t), calculated as 4 + (8 + fmt chunk size) + (8 + data chunk size) + sizes of any other chunks.
  • Format: 4 bytes, offset 8, description: WAVE identifier, must be "WAVE" (ASCII: 0x57415645).
  • Subchunk1ID (fmt chunk ID): 4 bytes, offset 12, description: Identifier for the format chunk, must be "fmt " (ASCII: 0x666d7420).
  • Subchunk1Size (fmt chunk size): 4 bytes, offset 16, description: Size of the fmt chunk data (little-endian uint32_t); typically 16 for PCM, 18 for non-PCM without extension, or 40 for extensible formats.
  • AudioFormat: 2 bytes, offset 20, description: Audio format code (little-endian uint16_t); 1 for PCM, 3 for IEEE float, 0xFFFE for extensible, etc.
  • NumChannels: 2 bytes, offset 22, description: Number of audio channels (little-endian uint16_t); e.g., 1 for mono, 2 for stereo.
  • SampleRate: 4 bytes, offset 24, description: Sampling rate in Hz (little-endian uint32_t); e.g., 44100 for CD quality.
  • ByteRate: 4 bytes, offset 28, description: Average bytes per second (little-endian uint32_t); calculated as SampleRate × NumChannels × (BitsPerSample / 8).
  • BlockAlign: 2 bytes, offset 32, description: Bytes per sample block across all channels (little-endian uint16_t); calculated as NumChannels × (BitsPerSample / 8).
  • BitsPerSample: 2 bytes, offset 34, description: Bits per sample (little-endian uint16_t); e.g., 8, 16, 24, or 32; defines the container size.
  • cbSize (extension size, optional): 2 bytes, offset 36 (if Subchunk1Size > 16), description: Size of the format extension (little-endian uint16_t); 0 or absent for basic PCM, 22 for extensible formats.
  • wValidBitsPerSample (optional): 2 bytes, offset 38 (if cbSize > 0), description: Number of valid bits per sample (little-endian uint16_t); <= BitsPerSample, used in extensible formats.
  • dwChannelMask (optional): 4 bytes, offset 40 (if cbSize > 0), description: Bitmask for channel-to-speaker mapping (little-endian uint32_t); e.g., 0x03 for front left/right.
  • SubFormat (optional): 16 bytes, offset 44 (if cbSize > 0), description: GUID specifying the subformat (e.g., for PCM: first 2 bytes = 0x0001, followed by fixed bytes).
  • factChunkID (optional): 4 bytes, variable offset (after fmt if present), description: Identifier for the fact chunk, "fact" (ASCII: 0x66616374); required for non-PCM.
  • factChunkSize (optional): 4 bytes, following factChunkID, description: Size of fact chunk data (little-endian uint32_t); minimum 4.
  • dwSampleLength (optional): 4 bytes, following factChunkSize, description: Number of samples per channel (little-endian uint32_t).
  • Subchunk2ID (data chunk ID): 4 bytes, variable offset (typically 36 or later), description: Identifier for the data chunk, "data" (ASCII: 0x64617461).
  • Subchunk2Size (data chunk size): 4 bytes, following Subchunk2ID, description: Size of the audio data in bytes (little-endian uint32_t).

Note: Additional chunks (e.g., "LIST", "INFO") may exist but are not intrinsic to the core audio format. The above focuses on essential properties for audio playback and storage.

3. HTML/JavaScript for Drag-and-Drop .WAV File Property Dump

The following is a self-contained HTML snippet with embedded JavaScript that can be embedded in a blog post (e.g., on Ghost or similar platforms). It creates a drop zone where users can drag and drop a .WAV file. The script reads the file as an ArrayBuffer, parses the properties listed above (assuming a basic PCM structure; handles optional fields if present), and displays them on the screen. It skips the raw audio data.

WAV Property Dumper
Drag and drop a .WAV file here

4. Python Class for .WAV File Handling

The following Python class uses the struct module to read and parse a .WAV file, decode the properties, print them to the console, and write a new .WAV file (copying the original data for simplicity). It handles basic PCM and optional fields.

import struct
import os

class WavHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {}
        self.audio_data = b''
        self.read_and_decode()

    def read_and_decode(self):
        with open(self.filepath, 'rb') as f:
            data = f.read()
        offset = 0

        def unpack(fmt, size):
            nonlocal offset
            val = struct.unpack_from(fmt, data, offset)[0]
            offset += size
            return val

        def unpack_str(size):
            return unpack('<%ds' % size, size).decode('ascii').rstrip('\x00')

        self.properties['ChunkID'] = unpack_str(4)
        self.properties['ChunkSize'] = unpack('<I', 4)
        self.properties['Format'] = unpack_str(4)
        self.properties['Subchunk1ID'] = unpack_str(4)
        fmt_size = unpack('<I', 4)
        self.properties['Subchunk1Size'] = fmt_size
        self.properties['AudioFormat'] = unpack('<H', 2)
        self.properties['NumChannels'] = unpack('<H', 2)
        self.properties['SampleRate'] = unpack('<I', 4)
        self.properties['ByteRate'] = unpack('<I', 4)
        self.properties['BlockAlign'] = unpack('<H', 2)
        self.properties['BitsPerSample'] = unpack('<H', 2)

        if fmt_size > 16:
            cb_size = unpack('<H', 2)
            self.properties['cbSize'] = cb_size
            if fmt_size >= 40:
                self.properties['wValidBitsPerSample'] = unpack('<H', 2)
                self.properties['dwChannelMask'] = unpack('<I', 4)
                sub_format = ' '.join(f'{b:02x}' for b in data[offset:offset+16])
                self.properties['SubFormat'] = sub_format
                offset += 16

        while offset < len(data):
            chunk_id = unpack_str(4)
            chunk_size = unpack('<I', 4)
            if chunk_id == 'fact':
                self.properties['factChunkID'] = chunk_id
                self.properties['factChunkSize'] = chunk_size
                self.properties['dwSampleLength'] = unpack('<I', 4)
            elif chunk_id == 'data':
                self.properties['Subchunk2ID'] = chunk_id
                self.properties['Subchunk2Size'] = chunk_size
                self.audio_data = data[offset:offset + chunk_size]
                offset += chunk_size
                if chunk_size % 2 != 0:
                    offset += 1  # Pad byte
                break
            else:
                offset += chunk_size  # Skip unknown

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key}: {value}")

    def write(self, output_path):
        with open(output_path, 'wb') as f:
            def pack(fmt, val):
                return struct.pack(fmt, val)

            f.write(pack('<4s', self.properties['ChunkID'].encode('ascii')))
            f.write(pack('<I', self.properties['ChunkSize']))
            f.write(pack('<4s', self.properties['Format'].encode('ascii')))
            f.write(pack('<4s', self.properties['Subchunk1ID'].encode('ascii')))
            f.write(pack('<I', self.properties['Subchunk1Size']))
            f.write(pack('<H', self.properties['AudioFormat']))
            f.write(pack('<H', self.properties['NumChannels']))
            f.write(pack('<I', self.properties['SampleRate']))
            f.write(pack('<I', self.properties['ByteRate']))
            f.write(pack('<H', self.properties['BlockAlign']))
            f.write(pack('<H', self.properties['BitsPerSample']))

            if 'cbSize' in self.properties:
                f.write(pack('<H', self.properties['cbSize']))
                if 'wValidBitsPerSample' in self.properties:
                    f.write(pack('<H', self.properties['wValidBitsPerSample']))
                    f.write(pack('<I', self.properties['dwChannelMask']))
                    sub_format_bytes = bytes(int(b, 16) for b in self.properties['SubFormat'].split())
                    f.write(sub_format_bytes)

            if 'factChunkID' in self.properties:
                f.write(pack('<4s', self.properties['factChunkID'].encode('ascii')))
                f.write(pack('<I', self.properties['factChunkSize']))
                f.write(pack('<I', self.properties['dwSampleLength']))

            f.write(pack('<4s', self.properties['Subchunk2ID'].encode('ascii')))
            f.write(pack('<I', self.properties['Subchunk2Size']))
            f.write(self.audio_data)
            if self.properties['Subchunk2Size'] % 2 != 0:
                f.write(b'\x00')  # Pad byte

# Example usage:
# handler = WavHandler('input.wav')
# handler.print_properties()
# handler.write('output.wav')

5. Java Class for .WAV File Handling

The following Java class uses DataInputStream to read and parse a .WAV file, decode the properties, print them to the console, and write a new .WAV file using DataOutputStream. It assumes a basic structure and handles optional fields.

import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class WavHandler {
    private String filepath;
    private Map<String, Object> properties = new HashMap<>();
    private byte[] audioData;

    public WavHandler(String filepath) {
        this.filepath = filepath;
        readAndDecode();
    }

    private void readAndDecode() {
        try (FileInputStream fis = new FileInputStream(filepath);
             DataInputStream dis = new DataInputStream(fis)) {
            properties.put("ChunkID", readString(dis, 4));
            properties.put("ChunkSize", dis.readInt());
            properties.put("Format", readString(dis, 4));
            properties.put("Subchunk1ID", readString(dis, 4));
            int fmtSize = dis.readInt();
            properties.put("Subchunk1Size", fmtSize);
            properties.put("AudioFormat", dis.readShort());
            properties.put("NumChannels", dis.readShort());
            properties.put("SampleRate", dis.readInt());
            properties.put("ByteRate", dis.readInt());
            properties.put("BlockAlign", dis.readShort());
            properties.put("BitsPerSample", dis.readShort());

            if (fmtSize > 16) {
                short cbSize = dis.readShort();
                properties.put("cbSize", cbSize);
                if (fmtSize >= 40) {
                    properties.put("wValidBitsPerSample", dis.readShort());
                    properties.put("dwChannelMask", dis.readInt());
                    byte[] subFormat = new byte[16];
                    dis.readFully(subFormat);
                    StringBuilder sb = new StringBuilder();
                    for (byte b : subFormat) sb.append(String.format("%02x ", b));
                    properties.put("SubFormat", sb.toString().trim());
                }
            }

            // Read remaining chunks
            while (dis.available() > 0) {
                String chunkId = readString(dis, 4);
                int chunkSize = dis.readInt();
                if (chunkId.equals("fact")) {
                    properties.put("factChunkID", chunkId);
                    properties.put("factChunkSize", chunkSize);
                    properties.put("dwSampleLength", dis.readInt());
                } else if (chunkId.equals("data")) {
                    properties.put("Subchunk2ID", chunkId);
                    properties.put("Subchunk2Size", chunkSize);
                    audioData = new byte[chunkSize];
                    dis.readFully(audioData);
                    if (chunkSize % 2 != 0) dis.skip(1); // Pad byte
                    break;
                } else {
                    dis.skip(chunkSize); // Skip unknown
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String readString(DataInputStream dis, int length) throws IOException {
        byte[] bytes = new byte[length];
        dis.readFully(bytes);
        return new String(bytes, "ASCII").trim();
    }

    public void printProperties() {
        for (Map.Entry<String, Object> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

    public void write(String outputPath) {
        try (FileOutputStream fos = new FileOutputStream(outputPath);
             DataOutputStream dos = new DataOutputStream(fos)) {
            dos.writeBytes((String) properties.get("ChunkID"));
            dos.writeInt((Integer) properties.get("ChunkSize"));
            dos.writeBytes((String) properties.get("Format"));
            dos.writeBytes((String) properties.get("Subchunk1ID"));
            dos.writeInt((Integer) properties.get("Subchunk1Size"));
            dos.writeShort((Short) properties.get("AudioFormat"));
            dos.writeShort((Short) properties.get("NumChannels"));
            dos.writeInt((Integer) properties.get("SampleRate"));
            dos.writeInt((Integer) properties.get("ByteRate"));
            dos.writeShort((Short) properties.get("BlockAlign"));
            dos.writeShort((Short) properties.get("BitsPerSample"));

            if (properties.containsKey("cbSize")) {
                dos.writeShort((Short) properties.get("cbSize"));
                if (properties.containsKey("wValidBitsPerSample")) {
                    dos.writeShort((Short) properties.get("wValidBitsPerSample"));
                    dos.writeInt((Integer) properties.get("dwChannelMask"));
                    String[] hex = ((String) properties.get("SubFormat")).split(" ");
                    for (String h : hex) dos.writeByte((byte) Integer.parseInt(h, 16));
                }
            }

            if (properties.containsKey("factChunkID")) {
                dos.writeBytes((String) properties.get("factChunkID"));
                dos.writeInt((Integer) properties.get("factChunkSize"));
                dos.writeInt((Integer) properties.get("dwSampleLength"));
            }

            dos.writeBytes((String) properties.get("Subchunk2ID"));
            dos.writeInt((Integer) properties.get("Subchunk2Size"));
            dos.write(audioData);
            if ((Integer) properties.get("Subchunk2Size") % 2 != 0) {
                dos.writeByte(0); // Pad byte
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Example usage:
    // public static void main(String[] args) {
    //     WavHandler handler = new WavHandler("input.wav");
    //     handler.printProperties();
    //     handler.write("output.wav");
    // }
}

6. JavaScript Class for .WAV File Handling

The following JavaScript class (for Node.js) reads and parses a .WAV file using fs, decodes the properties, prints them to the console, and writes a new .WAV file. It handles basic structures.

const fs = require('fs');

class WavHandler {
    constructor(filepath) {
        this.filepath = filepath;
        this.properties = {};
        this.audioData = Buffer.alloc(0);
        this.readAndDecode();
    }

    readAndDecode() {
        const data = fs.readFileSync(this.filepath);
        let offset = 0;

        const readString = (len) => {
            const str = data.toString('ascii', offset, offset + len).trim();
            offset += len;
            return str;
        };
        const readUint16 = () => {
            const val = data.readUInt16LE(offset);
            offset += 2;
            return val;
        };
        const readUint32 = () => {
            const val = data.readUInt32LE(offset);
            offset += 4;
            return val;
        };

        this.properties.ChunkID = readString(4);
        this.properties.ChunkSize = readUint32();
        this.properties.Format = readString(4);
        this.properties.Subchunk1ID = readString(4);
        const fmtSize = readUint32();
        this.properties.Subchunk1Size = fmtSize;
        this.properties.AudioFormat = readUint16();
        this.properties.NumChannels = readUint16();
        this.properties.SampleRate = readUint32();
        this.properties.ByteRate = readUint32();
        this.properties.BlockAlign = readUint16();
        this.properties.BitsPerSample = readUint16();

        if (fmtSize > 16) {
            this.properties.cbSize = readUint16();
            if (fmtSize >= 40) {
                this.properties.wValidBitsPerSample = readUint16();
                this.properties.dwChannelMask = readUint32();
                let subFormat = '';
                for (let i = 0; i < 16; i++) {
                    subFormat += data.readUInt8(offset + i).toString(16).padStart(2, '0') + ' ';
                }
                this.properties.SubFormat = subFormat.trim();
                offset += 16;
            }
        }

        while (offset < data.length) {
            const chunkId = readString(4);
            const chunkSize = readUint32();
            if (chunkId === 'fact') {
                this.properties.factChunkID = chunkId;
                this.properties.factChunkSize = chunkSize;
                this.properties.dwSampleLength = readUint32();
            } else if (chunkId === 'data') {
                this.properties.Subchunk2ID = chunkId;
                this.properties.Subchunk2Size = chunkSize;
                this.audioData = data.slice(offset, offset + chunkSize);
                offset += chunkSize;
                if (chunkSize % 2 !== 0) offset += 1;
                break;
            } else {
                offset += chunkSize;
            }
        }
    }

    printProperties() {
        for (const [key, value] of Object.entries(this.properties)) {
            console.log(`${key}: ${value}`);
        }
    }

    write(outputPath) {
        const buffer = Buffer.alloc(this.properties.ChunkSize + 8);
        let offset = 0;

        const writeString = (str, len) => {
            buffer.write(str, offset, len, 'ascii');
            offset += len;
        };
        const writeUint16 = (val) => {
            buffer.writeUInt16LE(val, offset);
            offset += 2;
        };
        const writeUint32 = (val) => {
            buffer.writeUInt32LE(val, offset);
            offset += 4;
        };

        writeString(this.properties.ChunkID, 4);
        writeUint32(this.properties.ChunkSize);
        writeString(this.properties.Format, 4);
        writeString(this.properties.Subchunk1ID, 4);
        writeUint32(this.properties.Subchunk1Size);
        writeUint16(this.properties.AudioFormat);
        writeUint16(this.properties.NumChannels);
        writeUint32(this.properties.SampleRate);
        writeUint32(this.properties.ByteRate);
        writeUint16(this.properties.BlockAlign);
        writeUint16(this.properties.BitsPerSample);

        if (this.properties.cbSize !== undefined) {
            writeUint16(this.properties.cbSize);
            if (this.properties.wValidBitsPerSample !== undefined) {
                writeUint16(this.properties.wValidBitsPerSample);
                writeUint32(this.properties.dwChannelMask);
                const hex = this.properties.SubFormat.split(' ');
                for (const h of hex) buffer.writeUInt8(parseInt(h, 16), offset++);
            }
        }

        if (this.properties.factChunkID !== undefined) {
            writeString(this.properties.factChunkID, 4);
            writeUint32(this.properties.factChunkSize);
            writeUint32(this.properties.dwSampleLength);
        }

        writeString(this.properties.Subchunk2ID, 4);
        writeUint32(this.properties.Subchunk2Size);
        this.audioData.copy(buffer, offset);
        offset += this.audioData.length;
        if (this.properties.Subchunk2Size % 2 !== 0) {
            buffer.writeUInt8(0, offset);
        }

        fs.writeFileSync(outputPath, buffer);
    }
}

// Example usage:
// const handler = new WavHandler('input.wav');
// handler.printProperties();
// handler.write('output.wav');

7. C++ Class for .WAV File Handling

The following C++ class uses std::ifstream and std::ofstream to read and parse a .WAV file, decode the properties, print them to the console, and write a new .WAV file. It handles basic structures using a std::map for properties.

#include <iostream>
#include <fstream>
#include <map>
#include <vector>
#include <iomanip>
#include <string>

class WavHandler {
private:
    std::string filepath;
    std::map<std::string, std::string> properties;
    std::vector<char> audioData;

public:
    WavHandler(const std::string& filepath) : filepath(filepath) {
        readAndDecode();
    }

    void readAndDecode() {
        std::ifstream file(filepath, std::ios::binary | std::ios::ate);
        if (!file) return;
        size_t size = file.tellg();
        file.seekg(0);
        std::vector<char> data(size);
        file.read(data.data(), size);
        size_t offset = 0;

        auto readString = [&](size_t len) -> std::string {
            std::string str(data.begin() + offset, data.begin() + offset + len);
            offset += len;
            // Trim nulls
            str.erase(std::find(str.begin(), str.end(), '\0'), str.end());
            return str;
        };
        auto readUint16 = [&]() -> uint16_t {
            uint16_t val = *reinterpret_cast<uint16_t*>(data.data() + offset);
            offset += 2;
            return val;
        };
        auto readUint32 = [&]() -> uint32_t {
            uint32_t val = *reinterpret_cast<uint32_t*>(data.data() + offset);
            offset += 4;
            return val;
        };

        properties["ChunkID"] = readString(4);
        properties["ChunkSize"] = std::to_string(readUint32());
        properties["Format"] = readString(4);
        properties["Subchunk1ID"] = readString(4);
        uint32_t fmtSize = readUint32();
        properties["Subchunk1Size"] = std::to_string(fmtSize);
        properties["AudioFormat"] = std::to_string(readUint16());
        properties["NumChannels"] = std::to_string(readUint16());
        properties["SampleRate"] = std::to_string(readUint32());
        properties["ByteRate"] = std::to_string(readUint32());
        properties["BlockAlign"] = std::to_string(readUint16());
        properties["BitsPerSample"] = std::to_string(readUint16());

        if (fmtSize > 16) {
            uint16_t cbSize = readUint16();
            properties["cbSize"] = std::to_string(cbSize);
            if (fmtSize >= 40) {
                properties["wValidBitsPerSample"] = std::to_string(readUint16());
                properties["dwChannelMask"] = std::to_string(readUint32());
                std::stringstream ss;
                for (size_t i = 0; i < 16; ++i) {
                    ss << std::hex << std::setw(2) << std::setfill('0') << (static_cast<unsigned char>(data[offset + i]) & 0xff) << " ";
                }
                properties["SubFormat"] = ss.str();
                offset += 16;
            }
        }

        while (offset < size) {
            std::string chunkId = readString(4);
            uint32_t chunkSize = readUint32();
            if (chunkId == "fact") {
                properties["factChunkID"] = chunkId;
                properties["factChunkSize"] = std::to_string(chunkSize);
                properties["dwSampleLength"] = std::to_string(readUint32());
            } else if (chunkId == "data") {
                properties["Subchunk2ID"] = chunkId;
                properties["Subchunk2Size"] = std::to_string(chunkSize);
                audioData.assign(data.begin() + offset, data.begin() + offset + chunkSize);
                offset += chunkSize;
                if (chunkSize % 2 != 0) ++offset;
                break;
            } else {
                offset += chunkSize;
            }
        }
    }

    void printProperties() const {
        for (const auto& pair : properties) {
            std::cout << pair.first << ": " << pair.second << std::endl;
        }
    }

    void write(const std::string& outputPath) const {
        std::ofstream file(outputPath, std::ios::binary);
        if (!file) return;

        auto writeString = [&](const std::string& str, size_t len) {
            file.write(str.c_str(), len);
        };
        auto writeUint16 = [&](uint16_t val) {
            file.write(reinterpret_cast<const char*>(&val), 2);
        };
        auto writeUint32 = [&](uint32_t val) {
            file.write(reinterpret_cast<const char*>(&val), 4);
        };

        writeString(properties.at("ChunkID"), 4);
        writeUint32(std::stoul(properties.at("ChunkSize")));
        writeString(properties.at("Format"), 4);
        writeString(properties.at("Subchunk1ID"), 4);
        writeUint32(std::stoul(properties.at("Subchunk1Size")));
        writeUint16(std::stoi(properties.at("AudioFormat")));
        writeUint16(std::stoi(properties.at("NumChannels")));
        writeUint32(std::stoul(properties.at("SampleRate")));
        writeUint32(std::stoul(properties.at("ByteRate")));
        writeUint16(std::stoi(properties.at("BlockAlign")));
        writeUint16(std::stoi(properties.at("BitsPerSample")));

        if (properties.count("cbSize")) {
            writeUint16(std::stoi(properties.at("cbSize")));
            if (properties.count("wValidBitsPerSample")) {
                writeUint16(std::stoi(properties.at("wValidBitsPerSample")));
                writeUint32(std::stoul(properties.at("dwChannelMask")));
                std::istringstream iss(properties.at("SubFormat"));
                std::string hex;
                while (iss >> hex) {
                    unsigned char byte = static_cast<unsigned char>(std::stoi(hex, nullptr, 16));
                    file.write(reinterpret_cast<const char*>(&byte), 1);
                }
            }
        }

        if (properties.count("factChunkID")) {
            writeString(properties.at("factChunkID"), 4);
            writeUint32(std::stoul(properties.at("factChunkSize")));
            writeUint32(std::stoul(properties.at("dwSampleLength")));
        }

        writeString(properties.at("Subchunk2ID"), 4);
        uint32_t dataSize = std::stoul(properties.at("Subchunk2Size"));
        writeUint32(dataSize);
        file.write(audioData.data(), dataSize);
        if (dataSize % 2 != 0) {
            char pad = 0;
            file.write(&pad, 1);
        }
    }
};

// Example usage:
// int main() {
//     WavHandler handler("input.wav");
//     handler.printProperties();
//     handler.write("output.wav");
//     return 0;
// }