Click here to Skip to main content
Click here to Skip to main content

Concatenating Wave Files Using C# 2005

By , 10 Mar 2007
 

Introduction

The WAVE file format is a subset of Microsoft's RIFF specification for the storage of multimedia files. A RIFF file starts out with a file header followed by a sequence of data chunks. A WAVE file is often just a RIFF file with a single "WAVE" chunk which consists of two sub-chunks -- a "fmt" chunk specifying the data format and a "data" chunk containing the actual sample data. Call this form the "Canonical form".

The main idea is to create only one header for all WAV files that you want to concatenate and then write data of each file in a single file.

Wave file headers follow the standard RIFF file format structure. The first 8 bytes in the file are the standard RIFF chunk header which have a chunk ID of "RIFF" and a chunk size equal to the file size minus the 8 bytes used by the header.

So we need to know the total length of all files to define ChunkSize and read NumChannels, SampleRate and BitsPerSample.

private void WaveHeaderIN(string spath)
        {
            FileStream fs = new FileStream(spath, FileMode.Open, FileAccess.Read);

            BinaryReader br = new BinaryReader(fs);
            length = (int)fs.Length - 8;
            fs.Position = 22;
            channels = br.ReadInt16();
            fs.Position = 24;
            samplerate = br.ReadInt32();
            fs.Position = 34;

            BitsPerSample = br.ReadInt16();
            DataLength = (int)fs.Length - 44;
            br.Close ();
            fs.Close();
        }

As we know channels are stored in the WAV header in byte number 22, we move the current position of the file to this location and the size of it is 2 bytes so we use br.ReadInt16() to read only 2 bytes and so on....

Construct the Header of Merged File

private void WaveHeaderOUT(string sPath)
        {
            FileStream fs = new FileStream(sPath, FileMode.Create, FileAccess.Write );

            BinaryWriter bw = new BinaryWriter(fs);
            bw.Write(new char[4] { 'R', 'I', 'F', 'F' });

            bw.Write(length);

            bw.Write(new char[8] {'W','A','V','E','f','m','t',' '});

            bw.Write((int)16);

            bw.Write((short)1);
            bw.Write(channels);

            bw.Write(samplerate );

            bw.Write((int)(samplerate * ((BitsPerSample * channels) / 8)));

            bw.Write((short )((BitsPerSample * channels) / 8));

            bw.Write(BitsPerSample);

            bw.Write(new char[4] {'d','a','t','a'});
            bw.Write(DataLength);
            bw.Close();
            fs.Close();
        }

We must be careful when writing the header. If there is any small mistake, the merged file doesn't work, so we write "RIFF" as an array of char, not as string and use int type for storing 4 bytes and short type for storing 2 bytes.

Write Data of all Files in the Merged File

  public void Merge(string[] files, string outfile)
        {
            WaveIO wa_IN = new WaveIO();
            WaveIO wa_out = new WaveIO();

            wa_out.DataLength = 0;
            wa_out.length = 0;


            //Gather header data
            foreach (string path in files)
            {
                wa_IN.WaveHeaderIN(@path);
                wa_out.DataLength += wa_IN.DataLength;
                wa_out.length += wa_IN.length;

            }

            //Reconstruct new header
            wa_out.BitsPerSample = wa_IN.BitsPerSample;
            wa_out.channels = wa_IN.channels;
            wa_out.samplerate = wa_IN.samplerate;
            wa_out.WaveHeaderOUT(@outfile);

            foreach (string path in files)
            {
                FileStream fs = new FileStream(@path, FileMode.Open, FileAccess.Read);
                byte[] arrfile = new byte[fs.Length - 44];
                fs.Position = 44;
                fs.Read(arrfile, 0, arrfile.Length);
                fs.Close();

                FileStream fo =
                    new FileStream(@outfile, FileMode.Append, FileAccess.Write);
                BinaryWriter bw = new BinaryWriter(fo);
                bw.Write(arrfile);
                bw.Close();
                fo.Close();
            }
          }

First we need to calculate the total length and data length of all files and then specify the channels, SampleRate and BitsPerSample of the output file.The last thing is to start reading data that is stored after byte number 44 and append it to the merged file.

All we need to do is call the Merge method and specify the input files and output file.

string[] files = new string[2] { @"C:\WINDOWS\Media\Windows XP Startup.wav",
                @"C:\WINDOWS\Media\Windows XP Shutdown.wav" };

WaveIO wa = new WaveIO();
wa.Merge(files,@"c:\oou.wav");

Play the Merged File

Visual Studio 2005 provides a new class to play sound. Therefore, we don't need an API or anything else.

FileStream fs = new FileStream(@"c:\oou.wav", FileMode.Open,FileAccess.Read);
System.Media.SoundPlayer sp = new System.Media.SoundPlayer(fs);
sp.Play();
fs.Close();

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Ehab Mohamed Essa
Egypt Egypt
Member
Ehab M. M. Essa
Computer Science Department
Faculty of Computers and Information, Mansoura University, Egypt

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
Suggestionminor improvements neededmembermdanh20021 Oct '11 - 4:24 
The code has 2 problems
 
(1) The wav files to be merged need to have exactly the same format, e.g. number of channels (mono/stereo), bits per sample (8 or 16) and sampling rate (in kHz). If the formats are not the same, the combined wav files will have distortion as indicated by some earlier comments.
 
(2) Value for ChunkSize in the merged wave file header is incorrect. The beginning of function Merge should read
 
  public void Merge(string[] files, string outfile)
        {
            WaveIO wa_IN = new WaveIO();
            WaveIO wa_out = new WaveIO();
 
            wa_out.DataLength = 0;
            wa_out.length = 0;
 

            //Gather header data
            foreach (string path in files)
            {
                wa_IN.WaveHeaderIN(@path);
                wa_out.DataLength += wa_IN.DataLength;
            }
 
            wa_out.length = wa_out.DataLength + 36;
        ....
      } 
 
ChunkSize (wa_out.length) is always 36 bytes more than Subchunk2Size (wa_out.length) so you can't sum up all the ChunkSize values of the individual wave files to get the final value.
 
This error will be ignored and inaudible by many players (including Windows Media Player), but some advanced audio editors (GoldWave for example) may detect the error and prompt the user, or refuse to open the file.
GeneralLicensememberthumb2231 Jan '11 - 3:10 
It says this article is licensed under the CPOL, but the code says it is copyrighted by you. Which is it? Great code by the way. Thanks.
GeneralI have figured out a solution to the noise problem...memberAbhi2028 Aug '10 - 15:17 
Hi friends, I too got the problem of noise in the 2nd sound when combining two wav sounds. Based on my observation on several sounds, the problem is due to byte misalignment of the 2nd sound's data after the 1st sound in the merged file, i.e., the noise comes for 2nd sound only when its data (after its Subchunk2) starts at odd offset within the merged file.
 
I've figured out that if the second file's sound data starts at even offset within the merged file, then there will be no noise problem. So, first write newly created header to the new file. Then write the first file's data. After writing the 1st file's data to the new file, just check the position value of offset to be written. If it is odd, then increment it by 1. If it's already even, continue writing 2nd file's data in the merged file. Hope this solves your problem too...
QuestionHeavy distortion in output file [modified]memberGolph4217 Nov '09 - 8:33 
Whether I'm processing one or many, the output file contains heavy distortion and background noise. I see several others have experienced the same, but no solution is posted. I'm almost there with this solution, but for the noise issue. Can anyone lend a hand?
 
NOTE: that what I use the normal windows files originally referenced, the output is clear, not distortion. When I use two .wav files generated by our corporate voice mail system (the real target files), I get the distorion. This is obvoiusly the characteristics of the files I'M using, but I don't know how to adust for their differences. (They sound fine individually, are distored when concat'd)
 
Thanks in advance
 
modified on Tuesday, November 17, 2009 2:46 PM

Generalvb.net: read, concat in mem, output to file or streammemberMember 425506423 Sep '09 - 0:37 
I reworked this example into a class which allows appending wav files and outputting it to a file or to a memory stream which can then be played like this:
 
Dim objPlayer As New Media.SoundPlayer
Dim wavFile As New wavFile
 
With wavFile
.loadFile("C:\wav\file1.wav")
.append("C:\wav\file2.wav")
.append("C:\wav\file3.wav")
objPlayer.Stream = .toStream
objPlayer.Play()
End With
 
Please note you can only append/concat files of plain PCM format without any additional chucks like 'fact'. This is working for me. Have fun.
 
This is the class:
 
Imports System.IO
 
Public Class wavFile
 
#Region "Properties"
    Dim _length As Integer = 0
    Dim _channels As Int16 = 0
    Dim _sampleRate As Int32 = 0
    Dim _dataLength As Int32 = 0
    Dim _bitsPerSample As Int16 = 0
    Dim _data(32768) As Byte
    Dim _wavFile As wavFile = Nothing
 
    Public Property length() As Integer
        Get
            Return _length
        End Get
        Set(ByVal value As Integer)
            _length = value
        End Set
    End Property
 

    Public Property channels() As Int16
        Get
            Return _channels
        End Get
        Set(ByVal value As Int16)
            _channels = value
        End Set
    End Property
 
    Public Property sampleRate() As Int32
        Get
            Return _sampleRate
        End Get
        Set(ByVal value As Int32)
            _sampleRate = value
        End Set
    End Property
 
    Public Property dataLength() As Int32
        Get
            Return _dataLength
        End Get
        Set(ByVal value As Int32)
            _dataLength = value
        End Set
    End Property
 
    Public Property bitsPerSample() As Int16
        Get
            Return _bitsPerSample
        End Get
        Set(ByVal value As Int16)
            _bitsPerSample = value
        End Set
    End Property
 
    Public Property data() As Byte()
        Get
            Return _data
        End Get
        Set(ByVal value As Byte())
            _data = value
        End Set
    End Property
 
#End Region
 
    Public Sub loadFile(ByVal path As String)
 
            Dim FS As FileStream = New FileStream(path, FileMode.Open, FileAccess.Read)
            Dim BR As BinaryReader = New BinaryReader(FS)
 
            _length = CInt(FS.Length - 8)
            FS.Position = 22
            _channels = BR.ReadInt16()
            FS.Position = 24
            _sampleRate = BR.ReadInt32()
            FS.Position = 34
            _bitsPerSample = BR.ReadInt16()
            _dataLength = CInt(FS.Length - 44)
            FS.Position = 44
 
             ReDim _data(_dataLength - 1)
            FS.Read(_data, 0, _dataLength)
            BR.Close()
            FS.Close()
    End Sub
 
    Private Function createHeader() As MemoryStream
        Dim MS As New MemoryStream
        Dim BW As New BinaryWriter(MS)
 
            BW.Write(Convert.ToChar("R"))
            BW.Write(Convert.ToChar("I"))
            BW.Write(Convert.ToChar("F"))
            BW.Write(Convert.ToChar("F"))
            BW.Write(_length)
 
            BW.Write(Convert.ToChar("W"))
            BW.Write(Convert.ToChar("A"))
            BW.Write(Convert.ToChar("V"))
            BW.Write(Convert.ToChar("E"))
            BW.Write(Convert.ToChar("f"))
            BW.Write(Convert.ToChar("m"))
            BW.Write(Convert.ToChar("t"))
            BW.Write(Convert.ToChar(" "))
 
            BW.Write(CInt(16))
            BW.Write(CShort(1))
            BW.Write(_channels)
            BW.Write(_sampleRate)
            BW.Write(CInt(_sampleRate * ((_bitsPerSample * _channels) / 8)))
            BW.Write(CShort((_bitsPerSample * _channels) / 8))
            BW.Write(_bitsPerSample)
 
            BW.Write(Convert.ToChar("d"))
            BW.Write(Convert.ToChar("a"))
            BW.Write(Convert.ToChar("t"))
            BW.Write(Convert.ToChar("a"))
 
            BW.Write(_dataLength)
 
            MS.Position = 0
        Return MS
    End Function
 
    Private Sub appendToBase(ByVal path As String)
        Dim newFile As New wavFile
        Dim baseFile As New wavFile
        Dim MSMergedFiles As New MemoryStream
        Dim BW As New BinaryWriter(MSMergedFiles)
 
            With newFile
                .loadFile(path)
                _bitsPerSample = .bitsPerSample
                _channels = .channels
                _sampleRate = .sampleRate
            End With
 
            With baseFile
                .dataLength = _dataLength
                .length = _length
                .data = _data
            End With
 
            _dataLength = newFile.dataLength + baseFile.dataLength
            _length = newFile.length + baseFile.length
            BW.Write(baseFile.data)
            BW.Write(newFile.data)
 
            MSMergedFiles.Position = 0
            _data = ToByte(MSMergedFiles)
            BW.Close()
 
    End Sub
 
#Region "Members"
 
    Public Sub append(ByVal path As String)
        If (String.IsNullOrEmpty(path)) Then
            Throw New ArgumentNullException("path")
        End If
        If File.Exists(path) = False Then
            path = "C:\voice\ding.wav"
        End If
 
        If _dataLength = 0 Then
            loadFile(path)
        Else
            appendToBase(path)
        End If
 
    End Sub
 
    Private Function ToByte(ByVal MS As Stream) As Byte()
            Dim streamLength As Integer = Convert.ToInt32(MS.Length)
            Dim buffer As Byte() = New Byte(streamLength - 1) {}
            MS.Read(buffer, 0, streamLength)
            MS.Close()
            Return buffer
    End Function
 
    Public Sub ToFile(ByVal path As String)
            Dim FS As FileStream = New FileStream(path, FileMode.Create, FileAccess.Write)
            Dim BW As BinaryWriter = New BinaryWriter(FS)
 
            BW.Write(ToByte(createHeader()))
            BW.Write(_data)
 
            BW.Close()
            FS.Close()
    End Sub
 
    Public Function toStream() As Stream
        Dim MS As New MemoryStream
        Dim BW As New BinaryWriter(MS)
            BW.Write(ToByte(createHeader()))
            BW.Write(_data)
            MS.Position = 0
        Return MS
    End Function
 
#End Region
 

End Class

GeneralAll in memorymembershem79 Mar '09 - 6:17 
Hi Seen the sample and it works great but was wondering if it is possible to open 2 files and combine the results in memory rather than writing to a new file so that they can be pushed out as a byte array, i.e.
 
FileStream fs1 = new FileStream(file1, FileMode.Open, FileAccess.Read);
FileStream fs2 = new FileStream(file2, FileMode.Open, FileAccess.Read);
 
// Create a byte array of file stream length
byte[] data1 = new byte[fs1.Length];
byte[] data2 = new byte[fs2.Length];
 

//Read block of bytes from stream into the byte array
fs1.Read(AudioData1, 0, System.Convert.ToInt32(fs1.Length));
fs2.Read(AudioData2, 0, System.Convert.ToInt32(fs2.Length));
 
//do something here to extract and combine two audio paths to a single byte array
QuestionThank & need haelpmemberMember 286830623 Jan '09 - 6:56 
Daer Ehab
 
السلام عليكم ورحمة الله
thans for your bost it help me for my project.
but stil i have problem in the output file some noice so can you tell me how i can clear it.
 
Thanks & Regards
QuestionStrange "clicks" ?memberjaph13 May '08 - 10:12 
Hello. I'm fairly new to c#, so excuse any dumbness from me Smile | :)
 
I'm trying to use this code to concatenate 2 (or more) .wav files, but it seems to be adding a "click" sound at the end of each of the wav files I join.
 
Anyway, if I use this project without any kind of modification, it will add the "clicks" too, so it isn't my code Roll eyes | :rolleyes:
 
P.S. - I'm using mono, 44,100Hz, 16bit wav files.
 
Regards
AnswerRe: Strange "clicks" ?memberthebeekeeper13 May '08 - 12:25 
Do you have any code that makes sure you're not introducing a large change in sample value at the border between two files? That's a pretty common oversight for people starting audio signal processing.
 
To understand what's happening, think about what it would sound like if you concatenated two sine waves, and at the border between them, the first one was at its peak, and the second one was at its valley. You get a click or a pop noise because your speakers can't play a sound with such a high frequency.
AnswerRe: Strange "clicks" ?memberwee_z_lee2 Oct '08 - 6:17 
You can probably avoid that problem by reading the length of the wav-file directly from the header. I had one file which had a copyright notice at the end - when I tried to set the data length by looking at the size of the FileStream (as in the code above), it gave me those strange clicks in my new files, because the audio player would try to play that copyright text, thinking it was part of the audio data.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 10 Mar 2007
Article Copyright 2006 by Ehab Mohamed Essa
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid