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){}
}
}
}