Reading and Writing .wav files

Ivan Uemlianin ivan@REDACTED
Mon May 31 13:03:07 CEST 2010


Dear All

Please find below wave.erl, containing two functions to read and write 
.wav files.

wave:read(FileName) reads a wav file and returns a record containing the 
audio sample rate and the raw data; wave:write(WavRecord, FileName) does 
the opposite.

The script was an experiment in parsing and packing binaries and I'm 
very pleased at how straightforward it was (mostly: I did ask for help 
here for one point) and how clearly readable the code is.

It all works (tested on mono and stereo wav files).

There are a couple of ancillary functions, which perhaps re-implement 
functions already in the standard library, or are otherwise 
sub-optimal:  lp2pl converts a list of pairs into a pair of lists, and 
lt2l does (almost) the opposite, converting a list of tuples into a flat 
list. 

Comments welcome.

Best wishes

Ivan

% ----- wave.erl

-module(wave).
-export([read/1, write/2, write/3]).

%% Erlang package to read and write .wav files.

%% wave record format
%% version 1.0:
%% - does not support compression
%% - only pcm encoding (AudioFormat = 1)

% wav file data always little-endian
% data is list of lists of integers (one list for each channel)
-record(wave, {audio_format, sample_rate, data}).

read(FileName) ->
    %% returns wave record
    {ok, Binary} = file:read_file(FileName),

    <<ChunkID:4/binary,
     ChunkSize:4/little-unsigned-integer-unit:8,
     Format:4/binary,
     SubChunk1ID:4/binary,
     SubChunk1Size:4/little-unsigned-integer-unit:8,
     AudioFormat:2/little-unsigned-integer-unit:8,   % 1 = pcm
     NumChannels:2/little-unsigned-integer-unit:8,   % 1 = mono
     SampleRate:4/little-unsigned-integer-unit:8,    % 16000, etc
     ByteRate:4/little-unsigned-integer-unit:8,      % SampleRate * 
NumChannels * BitsPerSample/8
     BlockAlign:2/little-unsigned-integer-unit:8,    % NumChannels * 
BitsPerSample/8
     BitsPerSample:2/little-unsigned-integer-unit:8, % 16, etc
     SubChunk2ID:4/binary,
     SubChunk2Size:4/little-unsigned-integer-unit:8,
     Data/binary>> = Binary,

    % TODO: test file for well-formedness, e.g.:
    % ChunkID == "RIFF"
    % ChunkSize == SubChunk2Size + 36
    % Format == "WAVE"
    % AudioFormat = 1  % no other encodings supported in v1.0
    % SubChunk1ID == "fmt "
    % SubChunk1Size == 16
    % SubChunk2ID == "data"
    % SubChunk2Size == NumSamples * NumChannels * BitsPerSample div 8

    Channels = data2channels(Data, NumChannels, BitsPerSample),

    io:format("* ~p~n~n- ChunkID: ~p~n- ChunkSize: ~B~n- Format: ~p~n- 
SubChunk1ID: ~p~n- SubChunk1Size: ~B~n- AudioFormat: ~B~n- NumChannels: 
~B~n- SampleRate: ~B~n- ByteRate: ~B~n- BlockAlign: ~B~n- BitsPerSample: 
~B~n- SubChunk2ID: ~p~n- SubChunk2Size: ~B~n- Data: [...]~n~n",
    [FileName,
     binary_to_list(ChunkID),
     ChunkSize,
     binary_to_list(Format),
     binary_to_list(SubChunk1ID),
     SubChunk1Size,
     AudioFormat,
     NumChannels,
     SampleRate,
     ByteRate,
     BlockAlign,
     BitsPerSample,
     binary_to_list(SubChunk2ID),
     SubChunk2Size
    ]),

    #wave{sample_rate=SampleRate,
      audio_format=AudioFormat,
      data=Channels
     }.

mono(Data, NBytes) ->
    [[ X || <<X:NBytes/little-signed-integer-unit:8>> <= Data ]].

stereo(Data, NBytes) ->
    LP = [ [A,B] || << A:NBytes/little-signed-integer-unit:8, 
B:NBytes/little-signed-integer-unit:8 >> <= Data],
    lp2pl(LP, [], []).
   
% list of pairs to pair of lists
lp2pl([], Left, Right) ->
    [lists:reverse(Left), lists:reverse(Right)];

lp2pl([[L,R]|T], Left, Right) ->
    lp2pl(T, [L|Left], [R|Right]).

data2channels(Data, NumChannels, BitsPerSample) ->
    NBytes = BitsPerSample div 8,
    case NumChannels of
     1  -> mono(Data, NBytes);
     2  -> stereo(Data, NBytes)
    end.

lt2l([], Acc) ->
    lists:reverse(Acc);

lt2l([H|T], Acc) ->
    lt2l(T, [element(2, H), element(1, H) | Acc]).

pl2l(PL) ->
    X = lists:zip(lists:nth(1, PL), lists:nth(2, PL)),
    lt2l(X, []).

channels2dataBin(Channels, BitsPerSample) ->
    case length(Channels) of
    1 -> Data = lists:nth(1, Channels);
    2 -> Data = pl2l(Channels)
    end,
    Size = length(Data) * (BitsPerSample div 8),
    DataBin = list_to_binary([ <<X:2/little-signed-integer-unit:8>> ||  
X <- Data]),
    {Size, DataBin}.

write(WavRecord, FileName) ->
    write(WavRecord, FileName, 16).

write(WavRecord, FileName, BitsPerSample) ->
    {SubChunk2Size, Data} = channels2dataBin(WavRecord#wave.data, 
BitsPerSample),

    ChunkID = list_to_binary("RIFF"),
    Format = list_to_binary("WAVE"),
    SubChunk1ID = list_to_binary("fmt "),
    SubChunk1Size = 16,
    AudioFormat = 1, % only pcm encoding supported
    NumChannels = length(WavRecord#wave.data),
    SampleRate = WavRecord#wave.sample_rate,
    ByteRate = SampleRate * NumChannels * BitsPerSample div 8,
    BlockAlign = NumChannels * BitsPerSample div 8,
    SubChunk2ID = list_to_binary("data"),
    ChunkSize = SubChunk2Size + 36,

    io:format("* ~p~n~n- ChunkID: ~p~n- ChunkSize: ~B~n- Format: ~p~n- 
SubChunk1ID: ~p~n- SubChunk1Size: ~B~n- AudioFormat: ~B~n- NumChannels: 
~B~n- SampleRate: ~B~n- ByteRate: ~B~n- BlockAlign: ~B~n- BitsPerSample: 
~B~n- SubChunk2ID: ~p~n- SubChunk2Size: ~B~n- Data: [...]~n~n",
    [FileName,
     binary_to_list(ChunkID),
     ChunkSize,
     binary_to_list(Format),
     binary_to_list(SubChunk1ID),
     SubChunk1Size,
     AudioFormat,
     NumChannels,
     SampleRate,
     ByteRate,
     BlockAlign,
     BitsPerSample,
     binary_to_list(SubChunk2ID),
     SubChunk2Size
    ]),

    Binary = <<ChunkID:4/binary,
           ChunkSize:4/little-unsigned-integer-unit:8,  %%
           Format:4/binary,
           SubChunk1ID:4/binary,
           SubChunk1Size:4/little-unsigned-integer-unit:8,
           AudioFormat:2/little-unsigned-integer-unit:8,
           NumChannels:2/little-unsigned-integer-unit:8,
           SampleRate:4/little-unsigned-integer-unit:8,
           ByteRate:4/little-unsigned-integer-unit:8,
           BlockAlign:2/little-unsigned-integer-unit:8,
           BitsPerSample:2/little-unsigned-integer-unit:8,
           SubChunk2ID:4/binary,
           SubChunk2Size:4/little-unsigned-integer-unit:8,  %%
          Data/binary
          >>,

    file:write_file(FileName, Binary),
    {ok, FileName}.



% ----- wave.erl

-- 
============================================================
Ivan A. Uemlianin
Speech Technology Research and Development

                    ivan@REDACTED
                     www.llaisdy.com
                         llaisdy.wordpress.com
                     www.linkedin.com/in/ivanuemlianin

    "Froh, froh! Wie seine Sonnen, seine Sonnen fliegen"
                     (Schiller, Beethoven)
============================================================



More information about the erlang-questions mailing list