Saturday, 26 September 2009

Trimming a WAV file using NAudio

I’m hoping to write a few brief code snippets to demonstrate various uses of NAudio, to eventually form the basis of an FAQ. This example shows how you can take a WAV file and trim a section out of it. You specify the TimeSpan to remove from the beginning and end, as well as an output WAV file. Please note this will only be reliable with PCM format WAV files.

public static class WavFileUtils
{
    public static void TrimWavFile(string inPath, string outPath, TimeSpan cutFromStart, TimeSpan cutFromEnd)
    {
        using (WaveFileReader reader = new WaveFileReader(inPath))
        {
            using (WaveFileWriter writer = new WaveFileWriter(outPath, reader.WaveFormat))
            {
                int bytesPerMillisecond = reader.WaveFormat.AverageBytesPerSecond / 1000;

                int startPos = (int)cutFromStart.TotalMilliseconds * bytesPerMillisecond;
                startPos = startPos - startPos % reader.WaveFormat.BlockAlign;

                int endBytes = (int)cutFromEnd.TotalMilliseconds * bytesPerMillisecond;
                endBytes = endBytes - endBytes % reader.WaveFormat.BlockAlign;
                int endPos = (int)reader.Length - endBytes; 

                TrimWavFile(reader, writer, startPos, endPos);
            }
        }
    }

    private static void TrimWavFile(WaveFileReader reader, WaveFileWriter writer, int startPos, int endPos)
    {
        reader.Position = startPos;
        byte[] buffer = new byte[1024];
        while (reader.Position < endPos)
        {
            int bytesRequired = (int)(endPos - reader.Position);
            if (bytesRequired > 0)
            {
                int bytesToRead = Math.Min(bytesRequired, buffer.Length);
                int bytesRead = reader.Read(buffer, 0, bytesToRead);
                if (bytesRead > 0)
                {
                    writer.WriteData(buffer, 0, bytesRead);
                }
            }
        }
    }
}

9 comments:

Frederic said...

Hello,

Was pleased with this example.
Tried it out with NAudio 1.3 (which was totally new to me) and worked perfectly on regular wav disk files.

My scenario involed in-memory Waves so I was pleased to see that (apparently starting with 1.3 release) WaveFileReader/Writer did support Streams.

Problem is that in the current WaveFileWriter class design, wave header and length is only updated when the object is disposed... which is ok when writing to a file, a little bit less when the stream is from another kind, because we might want to copy the result stream somewhere before disposing it.

Correct me if I'm wrong, but I was thinking of copying the block of code that updates the header from the Dispose method into the Flush method. Do you think that could do the job without breaking anything else?

UPDATE: Actually I did try this quick fix and it dit it, at least to me. Keep on the good work on this very helpful library.

Regards
Frederic

Anonymous said...

Can we use NAudio for web application..?

Mark H said...

the web server can use NAudio to manipulate audio files, but NAudio cannot be used on the client side in the browser

StarTraX said...

I have tried to implement this code in Visual Basic, but am getting an error
"Must read complete blocks" at
Dim bytesRead As Integer = reader.Read(buffer, 0, bytesToRead)
when invoking as follows:

Sub TrimWavFile(ByVal inPath As String, ByVal outPath As String, _
ByVal cutFromStart As TimeSpan, ByVal cutFromEnd As TimeSpan)
Dim reader As WaveFileReader = New WaveFileReader(inPath)
Dim writer As WaveFileWriter = New WaveFileWriter(outPath)
Dim bytesPerMillisecond As Integer = reader.WaveFormat.AverageBytesPerSecond / 1000
Dim startPos As Integer = cutFromStart.TotalMilliseconds * bytesPerMillisecond
startPos = startPos - startPos Mod reader.WaveFormat.BlockAlign
Dim endBytes As Integer = cutFromEnd.TotalMilliseconds * bytesPerMillisecond
endBytes = endBytes - endBytes Mod reader.WaveFormat.BlockAlign
Dim endPos As Integer = reader.Length - endBytes
TrimWavFile(reader, writer, startPos, endPos)
End Sub

Sub TrimWavFile(ByVal reader As WaveFileReader, ByVal writer As WaveFileWriter, ByVal startPos As Integer, ByVal endPos As Integer)
reader.Position = startPos
Dim buffer(1024) As Byte
While reader.Position < endPos
Dim bytesRequired As Integer = endPos - reader.Position
If bytesRequired > 0 Then
Dim bytesToRead As Integer = Math.Min(bytesRequired, buffer.Length)
Dim bytesRead As Integer = reader.Read(buffer, 0, bytesToRead)
If (bytesRead > 0) Then
writer.WriteData(buffer, 0, bytesRead)
End If
End If
End While
End Sub

Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
TrimWavFile("F:\VBProjectsDevelopment\3DTrackDisplay1\Sounds\tonovario.wav", _
"F:\VBProjectsDevelopment\3DTrackDisplay1\Sounds\tonovarioShort.wav", _
New TimeSpan(10000000), New TimeSpan(10000000))

End Sub

My C# coding level is about 3 out of 10 so maybe I've missed something in the migration.

Mark H said...

what is the value of bytesToRead?

sodaDreamer said...

Thanks for the snippet. This worked a treat for me!

I did spot one issue though - there's a rounding error when calculating bytesPerMillisecond. If you change it from an int to a double that should sort it.

Also, for what it's worth the 'Must read complete blocks' error is because the data must be read in multiples of reader.WaveFormat.BlockAlign. I had this issue reading a GSM file with a block align of 65, so I set the buffer size to BlockAlign * 100.

RelevantAds said...

Getting the "Must read complete blocks" error. Is this due to selecting a bad TimeSpan; or an oddly formed WAV file. My is 13kbps bitrate. I have 63 bytes left.

Mark H said...

instead of a buffer size of 1024, make sure it is sized to a multiple of your WaveFormat.BlockAlign.

Mark H said...

instead of a buffer size of 1024, make sure it is sized to a multiple of your WaveFormat.BlockAlign.