Clicking noise when concatenating/joining two or more WAV files

1.1k Views Asked by At

I am working on a music project where I need to join several WAV files. My Code works fine, but you hear clearly a clicking noise between two joined WAV files. That is an huge issue.

I am an audio engineer. When I work, with e.g. consecutive samples in a DAW (Digital Audio Workstation) and I want to prevent this clicking noise between two WAV samples then I have to create a crossover fade (basically this is a fadeout on the first sample and a fade in on the next sample).

Therefore my question would be if I can create such a crossover fade while concatenating two WAV files. I need to get rid of the clicking noise between concatenated wave files.

I provide my C# code below how I concatenate WAV files. This works for WAV files which are in the same "format". I found this piece of Code on (How to join 2 or more .WAV files together programatically?). Further I found this FadeIn/FadeOut possibility but I do not know how to apply this on the code. Further, I do not know if this would prevent the clicking noise.

Thank you for advice and a solution. Hopefully Mark Heath reads this :).

Best regards, Alex

Wavefile format:

AverageBytesPerSecond: 264600 | BitsPerSample: 24 | BlockAlign: 6 | Channels: 2 | Encoding: PCM | Extra Size: 0 | SampleRate: 44100 |

    public static void Concatenate(string outputFile, IEnumerable<string> sourceFiles)
{
    byte[] buffer = new byte[6]; //1024 was the original. but my wave file format has the blockAlign 6. So 1024 was not working for me. 6 does.
    WaveFileWriter waveFileWriter = null;

    try
    {
        foreach (string sourceFile in sourceFiles)
        {
            using (WaveFileReader reader = new WaveFileReader(sourceFile))
            {
                if (waveFileWriter == null)
                {
                    // first time in create new Writer
                    waveFileWriter = new WaveFileWriter(outputFile, reader.WaveFormat);
                }
                else
                {
                    if (!reader.WaveFormat.Equals(waveFileWriter.WaveFormat))
                    {
                        throw new InvalidOperationException("Can't concatenate WAV Files that don't share the same format");
                    }
                }

                int read;
                while ((read = reader.Read(buffer, 0, buffer.Length)) > 0)
                {
                    waveFileWriter.WriteData(buffer, 0, read);
                }
            }
        }
    }
    finally
    {
        if (waveFileWriter != null)
        {
            waveFileWriter.Dispose();
        }
    }
}
1

There are 1 best solutions below

25
On

This sounded like fun :)

Here's a sample I wrote to do this. It accepts a list of input filename patterns (assumes current directory) and the name of the output file. It stitches the files together, fading out ~1 second at the end of one file, then fading in ~1 second of the next file, and so on. Note: It doesn't mix that ~1 second overlap. Didn't feel like doing that :)

I used the ReadNextSampleFrame methods on the WaveFileReader to read the data as IEEE floating-point samples (one float per channel). This makes it much easier to apply volume adjustments unilaterally without having to worry about the actual input PCM representation. On the output, it uses WriteSamples on the writer to write the adjusted samples.

My first go at this used an NAudio FadeInFadeOutSampleProvider. But I found a weird bug in there when you had more than one audio channel.

So the code manually applies a volume to each sample read, ramping up the volume from 0.0 to 1.0 at the start of each file (except the first). It then copies the 'middle' of the file directly. Then at about 1 second before the end of the file (actually, (WaveFormat.SampleRate * WaveFormat.Channels) samples before the end of the file), it ramps the volume back down from 1.0f to 0.0f.

I tested it by using sox to generate a 5-second long 440Hz sine wave file, sampling rate = 96K, stereo, as follows:

sox -n -c 2 -r 96000 -b 24 sine.wav synth 5 sine 440

The test was called as follows:

FadeWeaver.FadeWeave("weaved.wav", "sine.wav", "sine.wav", "sine.wav");

And here's the code:

public class FadeWeaver
{
    static
    public
    void
    FadeWeave( string _outfilename,
               params string [] _inpatterns )
    {
        WaveFileWriter output = null;
        WaveFormat waveformat = null;
        float [] sample = null;

        float volume = 1.0f;
        float volumemod = 0.0f;

        // Add .wav extension to the output if not specified.
        string extension = Path.GetExtension(_outfilename);
        if( string.Compare(extension, ".wav", true) != 0 ) _outfilename += ".wav";

        // Assume we're using the current directory.  Let's get the
        // list of filenames.
        List<string> filenames = new List<string>();
        foreach( string pattern in _inpatterns )
        {
            filenames.AddRange(Directory.GetFiles(Directory.GetCurrentDirectory(), pattern));
        }

        try
        {
            // Alrighty.  Let's march over them.  We'll index them (rather than
            // foreach'ing) so that we can monitor first/last file.
            for( int index = 0; index < filenames.Count; ++index )
            {
                // Grab the file and use an 'audiofilereader' to load it.
                string filename = filenames[index];
                using( WaveFileReader reader = new WaveFileReader(filename) )
                {
                    // Get our first/last flags.
                    bool firstfile = (index == 0 );
                    bool lastfile = (index == filenames.Count - 1);

                    // If it's the first...
                    if( firstfile )
                    {
                        // Initialize the writer.
                        waveformat = reader.WaveFormat;
                        output = new WaveFileWriter(_outfilename, waveformat);
                    }
                    else
                    {
                        // All files must have a matching format.
                        if( !reader.WaveFormat.Equals(waveformat) )
                        {
                            throw new InvalidOperationException("Different formats");
                        }
                    }


                    long fadeinsamples = 0;
                    if( !firstfile )
                    {
                        // Assume 1 second of fade in, but set it to total size
                        // if the file is less than one second.
                        fadeinsamples = waveformat.SampleRate;
                        if( fadeinsamples > reader.SampleCount ) fadeinsamples = reader.SampleCount;

                    }

                    // Initialize volume and read from the start of the file to
                    // the 'fadeinsamples' count (which may be 0, if it's the first
                    // file).
                    volume = 0.0f;
                    volumemod = 1.0f / (float)fadeinsamples;
                    int sampleix = 0;
                    while( sampleix < (long)fadeinsamples )
                    {
                        sample = reader.ReadNextSampleFrame();
                        for( int floatix = 0; floatix < waveformat.Channels; ++floatix )
                        {
                            sample[floatix] = sample[floatix] * volume;
                        }

                        // Add modifier to volume.  We'll make sure it isn't over
                        // 1.0!
                        if( (volume = (volume + volumemod)) > 1.0f ) volume = 1.0f;

                        // Write them to the output and bump the index.
                        output.WriteSamples(sample, 0, sample.Length);
                        ++sampleix;
                    }

                    // Now for the time between fade-in and fade-out.
                    // Determine when to start.
                    long fadeoutstartsample = reader.SampleCount;
                    //if( !lastfile )
                    {
                        // We fade out every file except the last.  Move the
                        // sample counter back by one second.
                        fadeoutstartsample -= waveformat.SampleRate;
                        if( fadeoutstartsample < sampleix ) 
                        {
                            // We've actually crossed over into our fade-in
                            // timeframe.  We'll have to adjust the actual
                            // fade-out time accordingly.
                            fadeoutstartsample = reader.SampleCount - sampleix;
                        }
                    }

                    // Ok, now copy everything between fade-in and fade-out.
                    // We don't mess with the volume here.
                    while( sampleix < (int)fadeoutstartsample )
                    {
                        sample = reader.ReadNextSampleFrame();
                        output.WriteSamples(sample, 0, sample.Length);
                        ++sampleix;
                    }

                    // Fade out is next.  Initialize the volume.  Note that
                    // we use a bit-shorter of a time frame just to make sure
                    // we hit 0.0f as our ending volume.
                    long samplesleft = reader.SampleCount - fadeoutstartsample;
                    volume = 1.0f;
                    volumemod = 1.0f / ((float)samplesleft * 0.95f);

                    // And loop over the reamaining samples
                    while( sampleix < (int)reader.SampleCount )
                    {
                        // Grab a sample (one float per channel) and adjust by
                        // volume.
                        sample = reader.ReadNextSampleFrame();
                        for( int floatix = 0; floatix < waveformat.Channels; ++floatix )
                        {
                            sample[floatix] = sample[floatix] * volume;
                        }

                        // Subtract modifier from volume.  We'll make sure it doesn't
                        // accidentally go below 0.
                        if( (volume = (volume - volumemod)) < 0.0f ) volume = 0.0f;

                        // Write them to the output and bump the index.
                        output.WriteSamples(sample, 0, sample.Length);
                        ++sampleix;
                    }
                }
            }
        }
        catch( Exception _ex )
        {
            Console.WriteLine("Exception: {0}", _ex.Message);
        }
        finally
        {
            if( output != null ) try{ output.Dispose(); } catch(Exception){}
        }
    }
}