Skip to content

Reading the header of a wav file

WAV or WAVE is one of the most common audio file formats. In this post, we’ll see how to access the information stored in the header of a wave file. We’ll start by taking a look at the header’s format, and what data it stores. Then, we’ll see how to read this information with a program written in C.

WAV header

For this post, and the program implementation we’ll see below, we’ll concentrate on the simplest possible header format for wav files. We’ll use the same names for the header fields as described in the soundfile++ library documentation.

The header information starts at the beginning of the wave file (byte offset 0). The first 4 bytes are used to encode the characters RIFF, which refers to the container format used for wave files. Following it, we’ll have fields of either 4 or 2 bytes long that represent more information. We’ll not get into the details of the header format as it has been well explained before (also here).

The most important features are that the header is 44 bytes long and that it’s divided into fields of 2 or 4 bytes long. These fields are either strings, such as RIFF, or numerical values. The numerical values are either 2 or 4 bytes long and they are encoded in little-endian ordering (more information on the ordering below).

An example of the contents of the header for a wave file can be found below.

NameoffsetSizeValue
ChunkID04“RIFF”
ChunkSize44405040
Format84“WAVE”
Subchunk1 ID124“fmt”
Subchunk1 Size16416
Audio Format2021
Num Channels2222
Sample Rate24422050
Byte Rate28488200
Block Align3224
Bits per Sample34216
Subchunk2 ID364“data”
Subchunk 2 Size404405004
Example header values

Now that we’re familiar with the header structure, we can start describing the implementation.

Header struct

The main way we’ll access the bytes of the header is by using a struct. This will instruct C to allocate a contiguous space in memory large enough to contain all of the variables that form the struct.

struct header_struct {
  char chunk_id[4];
  union int_data chunk_size;
  char format[4];
  char subchunk1_id[4];
  union int_data subchunk1_size;
  union short_data audio_format;
  union short_data num_channels;
  union int_data sample_rate;
  union int_data byte_rate;
  union short_data block_align;
  union short_data bits_per_sample;
  char subchunk2_id[4];
  union int_data subchunk2_size;
};

See that the numerical fields are implemented using theunion int_data and union short_data data types. We’ll explain them in the section about endianness below.

While in this case it’s not relevant, there are some memory-address alignment requirements that could introduce extra bytes for padding and alignment between variables in the struct. Usually, we require a data type of size t bytes to be in a memory address that’s a multiple of t. Luckily, the wave header can be adequately aligned in memory without having to add padding bytes so we don’t need to concern about this.

Allocating header bytes

The bytes stored in the wave file header will be read byte-by-byte and stored in an array. In C, the char data type is one byte long. We’ll use this data type to fetch the data byte-by-byte. We will alias the unsigned char data type byte to reflect this.

typedef unsigned char byte;

The 44 bytes of the header data will be retrieved in an array. Those same bytes represent the header information described by the struct header_struct data type. To read the data as an array of chars and then reuse that same data as the header, we’ll use a union.

union header_data {
  byte data[HEADER_SIZE];
  struct header_struct header;
};

This will permit us use the same space in memory both to retrieve the information as an array of bytes and as the struct that was defined above.

Numerical data types sizes

The size of the numerical data fields in the header is either 2 or 4 bytes long. The size of some C data types such as int depends on the implementation (int can be 2 or 4 bytes long). For our program, we’ll rely on the fact that on 64-bit x86 machines int and short are 4 and 2 bytes long respectively. We’ll verify this in the program using the sizeof function.

// Verifying int is 4 bytes long
if (sizeof(int) != 4) {
  fprintf(stderr, "Error: machine data type INT isn't 4 bytes long\nCan't process wav header ");
    exit(4);
}

An alternative for this program to work across all implementations is to use the fixed size C data types.

Big and little-endian

Besides the length in bytes of numerical data, another fact that needs to be considered is the order in which these bytes are stored. In the wave file header, all of the numerical values are stored in little-endian ordering. This means that the bytes are stored starting from the least significant bytes first. The figure below illustrates the difference in byte-ordering between big-endian and little-endian.

The numerical data in the header of a wav file is stored in little-endian ordering.
Source left right

The value considered above is 0x0A0B0C0D. The initial 0x denotes that the value is in hexadecimal base. Each byte is 2 hexadecimal digits long, so we have 4 bytes: 0A, 0B, 0C , and 0D.

The wav file stores this information in little-endian ordering, that is, starting from the least significant byte first (memory address a in the figure above). That is the order in which the bytes will be retrieved in the data array.

Each machine has its own way of ordering numerical data. No extra steps will be required for machines that store it with the little-endian ordering, as the data will be retrieved in the same ordering. For machines that use big-ending ordering, however, we’ll require to reverse the ordering of the numerical fields.

if (is_bigendian())
  reverse_numerical_bytes(file_bytes);

In order to be able to access the bytes of the numerical fields and reverse them, we’ll use union data types again:

union int_data {
  int int_value;
  byte int_bytes[4];
};

union short_data {
  short short_value;
  byte short_bytes[2];
};

This way we’ll be able to manipulate the bytes when necessary. Once that’s done, we can just retrieve that same information as either int or short data types.

Retrieving bytes from the wave file

With the data types that will be used to store the data defined, we can begin to retrieve the data. As it was expressed before, we’ll do it byte-by-byte, as it would be done for a regular text file. The only difference is that the data is stored inside a union.

fp = fopen(wav_file, "r");
union header_data *file_bytes = (union header_data *) malloc(sizeof(union header_data));

int i;
for (i = 0; i < HEADER_SIZE && (file_bytes->data[i] = getc(fp)) != EOF; i++)
  ;

Retrieving header information

Once the bytes have been read from the header, the struct type defined above will take care of organizing them as required. We can access them directly.

// union header_data *file_bytes 
printf("ChunkID: %.4s\n", file_bytes->header.chunk_id);
printf("ChunkSize: %d\n", file_bytes->header.chunk_size.int_value);
...

The result

Now that the most relevant parts of the implementation have been explained, we can present the results for an example wave file.

$ ./wave test.wav 
ChunkID: RIFF
ChunkSize: 405040
Format: WAVE
Subchunk1 ID: fmt 
Subchunk1 Size: 16
Audio Format: 1
Num Channels: 2
Sample Rate: 22050
ByteRate: 88200
Block Align: 4
Bits per sample: 16
Subchunk2 Id: data
Subchunk2 size: 405004

You can acess the complete program here.

Published inProgramming
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments