![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
![[community profile]](https://www.dreamwidth.org/img/silk/identity/community.png)
Breaking podcasts into chunks for burning to CD
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.