1

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();
        }
    }
}
Community
  • 1
  • 1
Alex
  • 11
  • 2
  • Assuming that the code takes care of the headers (skipping the 2nd one and changing the 1st one to set the number of samples to samples1 + samples2 - crossfadeLenght) to create a cross-fade you need to : decide on the number of samples to use and then, well fade from 100% + 0% to 0% + 100% for each.. – TaW Apr 26 '16 at 09:46
  • Thank you for your help. I'll figure it out with your little concept. Besides that I found this: http://www.codeproject.com/Articles/35725/C-WAV-file-class-audio-mixing-and-some-light-audio Maybe this helps me out too. – Alex Apr 26 '16 at 17:18
  • Yes, this sounds helpful and probably well worth to study. The last time I dabbled with wave files I found that it is really easy to introduce stupid artifacts, e.g. by failing to dither rounding errors.. Good luck! – TaW Apr 26 '16 at 17:25

1 Answers1

0

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){}
        }
    }
}
Bob C
  • 391
  • 2
  • 10
  • Can you give some more details about the bug you found in `FadeInFadeOutSampleProvider`? – Corey Apr 28 '16 at 22:46
  • First of all thank you very much for your code. At first glance this looks like a genius work ;). I will apply this to my algorhythm and try it out. Further if you could provide some information about this bug you found in FadeInFadeOutSampleProvider like Bob C requested, this would be very kind of you. – Alex Apr 29 '16 at 06:59
  • Ok Bob C, I applied your code, modified it a little bit to match my needs and it works perfectly. The only thing I now need to change is the duration of the fadeIn's and -out's. I basically need a crossfade that hears can not hear. A very short amount of "samples". – Alex Apr 29 '16 at 09:38
  • I tried to minimize the fading times but I do not clearly understand how this time is calculated. can you tell me what I need to change when I want a crossfade that "one can not hear"? like 1 ms or so... thank you so much – Alex Apr 29 '16 at 10:47
  • @Corey - The "Read" path reads a number of floats equal to the `count` parameter from the underlying `ISampleProvider`, but then considers channel count when writing the output to the buffer that you passed. So if you have two or more channels, it actually writes more floats than you requested. I *think* I tried (e.g.) doubling the `count` parameter to account for two channels, but that didn't work either. Maybe I didn't mess with it enough, but it kept throwing wobblies going off the end of my output buffer. – Bob C Apr 29 '16 at 10:59
  • @Alex - The fades aren't done by "time" so much as "samples". But I just set the "fade samples" counter to the "SampleRate" from the WaveFormat, which gives me how many samples per second. So we fade across a number of samples that happens to equal one second. Any of those lines that determine the sample count, you can divide by an appropriate factor. For example, divide by 2 and you get 500ms worth of samples to fade across. – Bob C Apr 29 '16 at 11:01
  • @Alex - Adding to last comment: So if you just want a 1ms fade, divide the WaveFormat's SampleRate by 1000: `fadeinsamples = WaveFormat.SampleRate / 1000;` – Bob C Apr 29 '16 at 11:08
  • @Alex I ran out of time editing my last comment :) If you don't want to bother with converting to float and back, just do `samples = waveformat.SampleRate * (ms) / 1000;` where "ms" is how many milliseconds you want the fade to last. – Bob C Apr 29 '16 at 11:15
  • @Corey - If you have the source for `FadeInFadeOutSampleProvider`, check the `Read` method starting on line 82. It calls the `Read` method on an `ISampleProvider` that you defined in the constructor. Depending on the provider, it will return `count` floating-point values, and *not* `count` samples, writing them to the `buffer` you passed. But the `FadeIn` and `FadeOut` methods assume that their `count` parameter is "samples" (e.g. one float per channel), not just raw float count. So the end up overrunning the end of the `buffer` when you have more than one channel. – Bob C Apr 29 '16 at 12:21
  • @Bob C: wow, thank you for your time and effort you took to answer my questions. I googled and tried to implement this crossfade by myself for days/weeks. Thank you so much. If I can do anything for you just let me know. I'll have a great weekend now, hope you too :)! – Alex Apr 29 '16 at 12:31
  • @Alex Hey, I was having fun :) The more you dig into NAudio, the more you learn about how it works and about audio encoding in general. If Mark Heath were a Brit, I would recommend him for knighthood ;) – Bob C Apr 29 '16 at 15:00
  • @BobC Hmm... are you sure you're not using out of date source? I can't find a way to replicate that issue in the current source: https://github.com/naudio/NAudio/blob/master/NAudio/Wave/SampleProviders/FadeInOutSampleProvider.cs#L72 – Corey Apr 30 '16 at 07:57
  • @Corey - I may not have the latest source, but that still looks like it could still have a problem. I don't remember which ISourceProvider I was using (I'm at home...I leave code at work!), but looking at line 74: There is at least one provider that only returns `count` floats, not `count` samples (i.e. one float per channel). The loop from 107-110 loops over channels, which overruns the buffer for more than one channel. I'll have to check again on Monday to see if I can remember which Provider I was using. – Bob C Apr 30 '16 at 13:21
  • @Corey - The input audio is 24-bit PCM. The eventual ISampleProvider is a `Pcm24BitToSampleProvider`. The Read path for samples in my original attempt was AudioFileReader -> SampleChannel -> FadeInFadeOutSampleProvider -> VolumeSampleProvider -> Pcm24BitToSampleProvider. There doesn't seem to be anything along that path that adjusts the 'sample count' based on the number of channels. The `FadeInFadeOutSampleProvider` seems to assume that this has been done, but that's not the case. This is all based on examining the github source at the link you provided... – Bob C Apr 30 '16 at 14:00
  • @Corey - It looks like it's really the `FadeInFadeOutSampleProvider` that's causing the problem. Everything else uses `sampleCount` or `count` as a raw count of floats (not count * channels). The `FadeInFadeOutSampleProvider` is the only one that seems to take the channel count into consideration. It could be that this particular sampler is rarely used. Or maybe I just did something really really wrong :) – Bob C Apr 30 '16 at 14:04
  • @BobC It has to deal with channels to get the scaling right. It processes `Channels` samples at a time, but the loop is bound by the total count. Unless the source is allowing you to read non-integer number of channel blocks (ie an odd count in a stereo stream) there is no problem. – Corey May 01 '16 at 00:16
  • @BobC Refer to the `FadeOut` or `FadeIn` source. They both loop on the `sourceSamplesRead` (`count`), processing each channel in the inner loop. Refer: https://github.com/naudio/NAudio/blob/master/NAudio/Wave/SampleProviders/FadeInOutSampleProvider.cs#L106 – Corey May 01 '16 at 00:18
  • @Corey Exactly. The Fader works on channels, but that's not what the underlying ISampleProvider does. The Provider gives it `sampleCount` floats (not multi-channel samples), but the Fader (starting at line 107) loops over channels. Looking more closely, the outer `while` loop is being controlled by `samples`, which is also used in the channel loop. However, if `sampleCount` is a value that is not an even multiple of the channel count, and `buffer` is sized to that same amount, then an exception will be thrown when that inner loop goes off the end of the buffer. – Bob C May 01 '16 at 22:16
  • @BobC `ISampleProvider` should never return a count that is not a multiple of the number of channels. If you ask for an odd number of samples from a stereo provider it will round down and give you the lower even number - ask for 1001, get 1000. – Corey May 02 '16 at 01:48
  • @Corey - None of them do that, though :( Check out the path I laid out above: AudioFileReader -> SampleChannel -> VolumeSampleProvider -> Pcm24BitToSampleProvider. That's the input to the FadeInFadeOutSampleProvider. None of them check the channel count. I guess the easy way to find out is to try it yourself ;) Try to read one stereo sample, for example, into a float[1] buffer. – Bob C May 02 '16 at 10:36
  • @BobC You don't understand the underlying mechanisms. `WaveFileReader` throws an exception if you try to read a number of samples that doesn't get all channels. `MediaFoundationReader` will only return full sets of channel data. `AudioFileReader` uses these, so it **guarantees** that you can only have full-channel samples groups. Which is why no other part of NAudio checks for these, because there's no need. – Corey May 02 '16 at 23:09
  • @BobC Tell you what, post a question on the NAudio tag about it and show some code that allows you to read partial channel groups from any of the provided readers. I can't go into all the necessary details in the comments here, and it's off topic for this question. Put up a question and I'll respond in full. – Corey May 02 '16 at 23:28
  • @both: I got it now. Thank you. If you ask another question with Naudio tag pls post the link in here. – Alex May 03 '16 at 08:44
  • @Alex - Will do :) Also, tag my answer as "the" answer, if you would. Here's another answer - someone converting a wav file to a-law and saving that: http://stackoverflow.com/questions/36934672/must-be-already-floating-point-when-converting-an-audio-file-into-wav-file/36941538#36941538 – Bob C May 03 '16 at 10:38
  • @Corey - Ok, you win :) I didn't want to waste a new question. I tried it with the 24-bit stereo sine wave file created for this question, with a WaveFileReader and a Pcm24BitToSampleProvider (AudioFileReader doesn't like my sine wave!), and I see the exception being thrown inside WaveFileReader whenever the requested byte count doesn't match the BlockAlign value. Alex, sorry for hijacking your question! :) – Bob C May 03 '16 at 13:20