isaacschemm: A cartoon of myself as a snail (snail8)
[personal profile] isaacschemm posting in [community profile] snailsharp

Sometimes I want to listen to a podcast on a TV, and sometimes the easiest way to do that is by burning it to a CD (either a CD-ROM with MP3 files, or - if it fits - a normal audio CD). Not every CD player has the greatest seeking features, though. I've got one whose fast-forward is more of a normal-speed-forward, and another that actually goes forward when rewinding an MP3 at the slowest level.

I figured one way of working around these issues - which could come into play for podcast episodes that are, like, an hour long - would be to split the podcast into segments of five minutes each. You'd need to make a normal audio CD (which means a limit of 75 or 80 minutes or so), but you could have gapless playback, while also being able to use track selection to go forward and back in chunks.

Yesterday I put together a small Windows application to help with this. It's called Cue Sheet Generator, and it takes in one or more audio files and converts them to either a set of .wav files (to burn with Windows Media Player Legacy or another app with gapless burning support) or a .wav/.cue pair (which ImgBurn and other such apps can handle).

The main program logic is small enough to fit into this post. I wrote it in VB.NET (there's nothing here C# couldn't do, I'm just tired of looking at curly brackets), and I thought it might be helpful to annotate it.

Imports System.IO
Imports NAudio.Wave

Public Class Form1
    Private ReadOnly InputFiles As New List(Of String)

I'm using the NAudio library to read the original audio files and write the resulting .wav file(s). The application reads its input files upon load:

    Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
        InputFiles.AddRange(My.Application.CommandLineArgs.Where(Function(x) File.Exists(x)))

        If InputFiles.Count = 0 Then
            Using dialog As New OpenFileDialog()
                dialog.Multiselect = True
                If dialog.ShowDialog(Me) <> DialogResult.OK Then
                    Application.Exit()
                End If

                InputFiles.AddRange(dialog.FileNames)
            End Using
        End If
    End Sub

This shows off a distinctive feature of VB.NET - the Handles keyword, used to attach a function to an event from within the function definition, rather than in a WinForms designer file (as C# would do it). In C#, if I just delete the handler function, it breaks the designer; in VB.NET, the event hookup gets deleted along with it, so nothing ends up broken.

We start by going through all command line arguments, looking for ones that are valid files that exist (this lets you drag and drop audio files onto the app). If none were found, we show an "open" dialog. The app closes if the user hits Cancel, but if they hit OK, any selected files are loaded into the app. InputFiles stays the same from here on out.

    Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Button1.Enabled = False
        Button2.Enabled = False

        Dim inputFileByteLengths As New List(Of Long)
        Dim inputFileDurations As New List(Of TimeSpan)
        For Each inputFile In InputFiles
            Using reader As New MediaFoundationReader(inputFile)
                inputFileByteLengths.Add(reader.Length)
                inputFileDurations.Add(reader.TotalTime)
            End Using
        Next

This is the beginning of the handler for the .wav/.cue button. First, we disable the buttons. (Note that this is an Async method - any time the code is stopped at an Await, the UI thread can run, letting you drag the window around and stuff like that.) Then we read each input file, using NAudio's MediaFoundationReader, and find the length (in bytes) and duration of the uncompressed audio data for each file. (MediaFoundationReader uses the Media Foundation framework built into Windows to decode the audio, so it should work with MP3s and other formats as well.)

        Dim segmentLength = TimeSpan.FromMinutes(NumericUpDown1.Value)
        Dim trackBoundaries As New List(Of TimeSpan)
        For Each inputFileDuration In inputFileDurations
            Dim ts = inputFileDuration
            While ts > segmentLength
                trackBoundaries.Add(segmentLength)
                ts -= segmentLength
            End While
            trackBoundaries.Add(ts)
        Next

Here's how we figure out where the track lengths should be. Each input file gets zero or more tracks of length segmentLength, and a final track that contains the remainder. (It's possible this could be made cleaner with integer division and modulo operators.) trackBoundaries, then, is a list of track lengths, with track 1 first.

        Using fs As New FileStream($"out.cue", FileMode.CreateNew, FileAccess.Write)
            Using sw As New StreamWriter(fs)
                Await sw.WriteLineAsync($"FILE out.wav WAVE")

We open an output stream to write the cue sheet (note FileMode.CreateNew - this is so the app will throw an exception if out.cue already exists). A reference to out.wav is written as the first line. The rest of the cue sheet will define the tracks.

                Dim track = 1
                Dim point = TimeSpan.Zero
                For Each trackBoundary In trackBoundaries
                    Dim totalSectors = CInt(point.TotalSeconds * 75)
                    Dim totalSeconds = totalSectors \ 75
                    Dim totalMinutes = totalSeconds \ 60
                    Dim seconds = totalSeconds - (totalMinutes * 60)
                    Dim sectors = totalSectors - (totalSeconds * 75)

Starting with track 1, we go through trackBoundaries, figuring out the start of each track in mm:ss:ff format, where ff is 75ths of a second. Track 1 starts at 00:00:00.

                    Await sw.WriteLineAsync($"  TRACK {track:D2} AUDIO")
                    Await sw.WriteLineAsync($"    INDEX 01 {totalMinutes:D2}:{seconds:D2}:{sectors:D2}")

We add a track to the cue sheet, with no gap / lead-in time, and the appropriate start time.

                    point += trackBoundary
                    track += 1
                Next
            End Using
        End Using

The duration of the track is added to point so the next track starts after it, and the track number is incremented. Once all tracks have been handled, the cue sheet is closed.

        ProgressBar1.Maximum = inputFileByteLengths.Sum()

        Using fs As New FileStream("out.wav", FileMode.CreateNew, FileAccess.Write)
            Dim buffer(33554431) As Byte
            Using combinedWriter As New WaveFileWriter(fs, New WaveFormat(44100, 2))

At this point, we start writing the .wav file. A temporary buffer will be used to copy from the audio source (another MediaFoundationReader) to the destination (a WaveFileWriter set to CD-standard 44100 Hz stereo) - this means we can copy in chunks, track the progress, and report it in the UI. If we didn't want a progress bar, we could just use CopyToAsync. Note that this assumes the input is stereo - future version will add more code to handle non-stereo input files.

Note that the size of the buffer is exactly 32 MiB (32 * 1024 * 1024 bytes); when initializing an array like this in VB.NET, the number you provide is the index of the last element, not the total number of elements.

                For Each inputFile In InputFiles
                    Using reader As New MediaFoundationReader(inputFile)
                        Do
                            Dim bytesRead = Await reader.ReadAsync(buffer, 0, buffer.Length)
                            If bytesRead <= 0 Then
                                Exit Do
                            End If
                            ProgressBar1.Value += bytesRead
                            Await combinedWriter.WriteAsync(buffer, 0, bytesRead)
                        Loop
                    End Using
                Next

Now, for each input file, we open it again, then start a loop (roughly equivalent to do { ... } while (true) in C#) to read the uncompressed audio data, update the progress bar, and write it out to out.wav. Eventually, when a read returns no data, we exit the loop.

            End Using
        End Using

        Application.Exit()
End Sub

After this is done, the application closes.

If you press the multiple .wav button instead, the process is a bit simpler:

    Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        Button1.Enabled = False
        Button2.Enabled = False

        Dim inputFileByteLengths As New List(Of Long)
        For Each inputFile In InputFiles
            Using reader As New MediaFoundationReader(inputFile)
                inputFileByteLengths.Add(reader.Length)
            End Using
        Next

ProgressBar1.Maximum = inputFileByteLengths.Sum()

We read the length (in bytes) of the audio data of each input file and use this to set the length of the progress bar.

        Dim i = 1

        For Each inputFile In InputFiles
            Using reader As New MediaFoundationReader(inputFile)
                Dim buffer(reader.WaveFormat.AverageBytesPerSecond * 60 * NumericUpDown1.Value - 1) As Byte

The buffer here is set to store exactly X minutes of audio data (where X is the number entered in the UI; defaults to 5). Note the offset of 1 byte to conform to VB.NET's way of declaring array sizes. This time, we open the input stream first.

                Do
                    Dim bytesRead = Await reader.ReadAsync(buffer, 0, buffer.Length)
                    If bytesRead <= 0 Then
                        Exit Do
                    End If
                    ProgressBar1.Value += bytesRead
                    Using fs As New FileStream($"out{i:D3}.wav", FileMode.CreateNew, FileAccess.Write)
                        Using writer As New WaveFileWriter(fs, New WaveFormat(44100, 2))
                            Await writer.WriteAsync(buffer, 0, bytesRead)
                        End Using
                    End Using
                    i += 1
                Loop

We start a loop again. First, we read into the buffer from the input stream. If we didn't read anything, we exit the loop; otherwise, we update the progress bar, then open a new file (starting with out001.wav and going up from there) to store the (up to X minute) output data.

            End Using
        Next

        Application.Exit()
    End Sub
End Class

Once this is done, we exit the application here too. The resulting .wav files can be burned to a CD; if written without gaps, the playback should be seamless.

This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

Snail#

A programming blog where the gimmick is that I pretend to be a snail.

Expand Cut Tags

No cut tags

Style Credit

Page generated Jun. 24th, 2025 05:33 am
Powered by Dreamwidth Studios