Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Creating Audio CDs using IMAPI2

4.86/5 (18 votes)
14 Apr 2008CPOL3 min read 7   3.2K  
Using the Image Mastering API to create Red Book Audio CDs

Introduction

This is my third article now on burning media using IMAPI2, version 2 of Microsoft's Image Mastering API. I received a few inquiries on creating Audio CDs after I wrote my last article, Burning and Erasing CD/DVD/Blu-ray Media with C# and IMAPI2, so I decided to write this article next. This article goes into more detail about the IDiscFormat2TrackAtOnce interface, which is the IMAPI2 Interface for creating audio CDs.

Background

It would help to read the last article, but if you don't, there are a couple of things you must know. The IMAPI2 COM DLLs are included with Microsoft Vista, but if you are running Windows XP or Windows 2003, you can download the IMAPI2 Update packages from Microsoft's website. IMAPI2 is implemented using two COM DLLs; imapi2.dll and imapi2fs.dll. imapi2.dll handles most of the device and recording APIs, and imapi2fs.dll handles all of the file system and IStream APIs. Do not add the IMAPI2 COM references to your project. There is a conflict with the IStream interfaces, and if you try to use the IStream created with IMAPI2FS in the IMAPI2 interface, you will get errors similar to this:

Unable to cast object of type 'IMAPI2FS.FsiStreamClass' to type 'IMAPI2.IStream'

I go into more detail in my last article about this problem. You will need to use my Interop file, Imapi2interop.cs, which provides all of the interfaces and events for IMAPI2.

Using the Code

C#
using IMAPI2.Interop;
C#
[assembly: ComVisible(true)] 
  • Make sure that Windows XP and 2003 have the IMAPI2 updates mentioned at the top of the article.
  • Do not add the imapi2.dll and imapi2fs.dll COM DLLs to your project. You will receive the IStream error mentioned above.
  • Add the file imapi2interop.cs to your project, and define the 'IMAPI2.Interop' namespace in your app:
  • In order to receive notification to your event handler from COM, you need to open up the file AssemblyInfo.cs and change the ComVisible attribute to true:

WAV File Format

Red Book is the standard for CD audio, and it specifies that CD data is in the 44.1 Hz, 16-bit, stereo, uncompressed PCM format. This is usually WAV files with a WAV header. For the purpose of this article, the program is not going to convert other formats to this format, so you must have Wav audio files that are already in this format.

When you select files to be burned, I check to make sure they are in the correct format, with the MediaFile.IsWavProperFormat method:

C#
/// <summary>
/// Determines if the Wav file is the proper format to be written to CD
/// The proper format is uncompressed PCM, 44.1KHz, Stereo
/// </summary>
/// <param name="wavFile">the selected wav file</param>
/// <returns>true if proper format, otherwise false</returns>
public static bool IsWavProperFormat(string wavFile)
{
    FileStream fileStream = null;
    try
    {
       fileStream = File.OpenRead(wavFile);

       //
       // Read the header data
       //
       BinaryReader binaryReader = new BinaryReader(fileStream);
       byte[] byteData = binaryReader.ReadBytes(Marshal.SizeOf(typeof(WAV_HEADER)));
       GCHandle handle = GCHandle.Alloc(byteData, GCHandleType.Pinned);
       binaryReader.Close();
       fileStream.Close();

       //
       // Convert to the wav header structure
       //
       WAV_HEADER wavHeader = 
         (WAV_HEADER)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), 
                                            typeof(WAV_HEADER));

       //
       // Verify the WAV file is a 44.1KHz, Stereo, Uncompressed Wav file.
       //
       if ((wavHeader.chunkID == 0x46464952) && // "RIFF"
         (wavHeader.format == 0x45564157) && // "WAVE"
         (wavHeader.formatChunkId == 0x20746d66) && // "fmt "
         (wavHeader.audioFormat == 1) && // 1 = PCM (uncompressed)
         (wavHeader.numChannels == 2) && // 2 = Stereo
         (wavHeader.sampleRate == 44100)) // 44.1 KHz
       {
           return true;
       }

       MessageBox.Show(wavFile + " is not the correct format!");return false;
    }
    catch (Exception ex)
    {
       MessageBox.Show(ex.Message);
       return false;
    }
}

Preparing the Stream

The stream size must be a multiple of 2352, the sector size of an audio CD. We calculate it with a simple algorithm:

C#
private long SECTOR_SIZE = 2352;
.
.
.
public Int64 SizeOnDisc
{
    get
    {
        if (m_fileLength > 0)
        {
           return ((m_fileLength / SECTOR_SIZE) + 1) * SECTOR_SIZE;
        }
 
        return 0;
    }
}

Once we calculate the size, we copy the Wav's data, minus the header, to the allocated data, then we create the IStream:

C#
/// <summary>
/// Prepares a stream to be written to the media
/// </summary>
public void PrepareStream()
{
    byte[] waveData = new byte[SizeOnDisc];
 
    //
    // The size of the stream must be a multiple of the sector size 2352
    // SizeOnDisc rounds up to the next sector size
    //
    IntPtr fileData = Marshal.AllocHGlobal((IntPtr)SizeOnDisc);
    FileStream fileStream = File.OpenRead(filePath);
 
    int sizeOfHeader = Marshal.SizeOf(typeof(WAV_HEADER));
 
    //
    // Skip over the Wav header data, because it only needs the actual data
    //
    fileStream.Read(waveData, sizeOfHeader, (int)m_fileLength - sizeOfHeader);
 
    Marshal.Copy(waveData, 0, fileData, (int)m_fileLength - sizeOfHeader);
 
    CreateStreamOnHGlobal(fileData, true, out wavStream);
}

Writing the Tracks

I perform the burning of the CD in a BackgroundWorker's DoWork event. I prepare all the streams first, and then call the MsftDiscFormat2TrackAtOnce's AddAudioTrack method for each track:

C#
/// <summary>
/// The thread that does the burning of the media
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    e.Result = 0;
 
    MsftDiscMaster2 discMaster = new MsftDiscMaster2();
    MsftDiscRecorder2 discRecorder2 = new MsftDiscRecorder2();
 
    BurnData burnData = (BurnData)e.Argument;
    discRecorder2.InitializeDiscRecorder(burnData.uniqueRecorderId);
 
    MsftDiscFormat2TrackAtOnce trackAtOnce = new MsftDiscFormat2TrackAtOnce();
    trackAtOnce.ClientName = m_clientName;
    trackAtOnce.Recorder = discRecorder2;
    m_burnData.totalTracks = listBoxFiles.Items.Count;
    m_burnData.currentTrackNumber = 0;
 
    //
    // Prepare the wave file streams
    //
    foreach (MediaFile mediaFile in listBoxFiles.Items)
    {
        //
        // Check if we've cancelled
        //
        if (backgroundWorker.CancellationPending)
        {
            break;
        }
 
        //
        // Report back to the UI that we're preparing stream
        //
        m_burnData.task = BURN_MEDIA_TASK.BURN_MEDIA_TASK_PREPARING;
        m_burnData.filename = mediaFile.ToString();
        m_burnData.currentTrackNumber++;
 
        backgroundWorker.ReportProgress(0, m_burnData);
        mediaFile.PrepareStream();
    }
 

    //
    // Add the Update event handler
    //
    trackAtOnce.Update += new DiscFormat2TrackAtOnce_EventHandler(trackAtOnce_Update);
 
    trackAtOnce.PrepareMedia();
 
    //
    // Add Files and Directories to File System Image
    //
    foreach (MediaFile mediaFile in listBoxFiles.Items)
    {
        //
        // Check if we've cancelled
        //
        if (backgroundWorker.CancellationPending)
        {
           e.Result = -1;
           break;
        }
 
        //
        // Add audio track
        //
        m_burnData.filename = mediaFile.ToString();
        IStream stream = mediaFile.GetTrackIStream();
        trackAtOnce.AddAudioTrack(stream);
    }
 
    //
    // Remove the Update event handler
    //
    trackAtOnce.Update -= new DiscFormat2TrackAtOnce_EventHandler(trackAtOnce_Update);

 
    trackAtOnce.ReleaseMedia();
    discRecorder2.EjectMedia();
}

Updating the User Interface

I created an event handler for the Update event of the IDiscFormat2TrackAtOnce interface. When the event handler gets called, it passes in a IDiscFormat2TrackAtOnceEventArgs object that gives me values like the current track number, elapsed time, etc. I take these values and copy them to my BurnData object, and call the BackgroundWorker's ReportProgress method.

C#
/// <summary>
/// Update notification from IDiscFormat2TrackAtOnce
/// </summary>
/// <param name="sender"></param>
/// <param name="progress"></param>
void trackAtOnce_Update(object sender, object progress)
{
    //
    // Check if we've cancelled
    //
    if (backgroundWorker.CancellationPending)
    {
        IDiscFormat2TrackAtOnce trackAtOnce = (IDiscFormat2TrackAtOnce)sender;
        trackAtOnce.CancelAddTrack();
        return;
    }
    IDiscFormat2TrackAtOnceEventArgs eventArgs = (IDiscFormat2TrackAtOnceEventArgs)progress;
    m_burnData.task = BURN_MEDIA_TASK.BURN_MEDIA_TASK_WRITING;
 
    //
    // IDiscFormat2TrackAtOnceEventArgs Interface
    //
    m_burnData.currentTrackNumber = eventArgs.CurrentTrackNumber;
    m_burnData.elapsedTime = eventArgs.ElapsedTime;
    m_burnData.remainingTime = eventArgs.RemainingTime;
 
    //
    // IWriteEngine2EventArgs Interface
    //
    m_burnData.currentAction = eventArgs.CurrentAction;
    m_burnData.startLba = eventArgs.StartLba;
    m_burnData.sectorCount = eventArgs.SectorCount;
    m_burnData.lastReadLba = eventArgs.LastReadLba;
    m_burnData.lastWrittenLba = eventArgs.LastWrittenLba;
    m_burnData.totalSystemBuffer = eventArgs.TotalSystemBuffer;
    m_burnData.usedSystemBuffer = eventArgs.UsedSystemBuffer;
    m_burnData.freeSystemBuffer = eventArgs.FreeSystemBuffer;
 
    //
    // Report back to the UI
    //
    backgroundWorker.ReportProgress(0, m_burnData);
}

This causes the BackgroundWorker's ProgressChanged event to get fired, and allows the application to update the User Interface with the data in the UI's thread.

C#
/// <summary>
/// Update the user interface with the current progress
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker_ProgressChanged(object sender, 
                              ProgressChangedEventArgs e)
{
    BurnData burnData = (BurnData)e.UserState;
    if (burnData.task == BURN_MEDIA_TASK.BURN_MEDIA_TASK_PREPARING)
    {
        //
        // Notification that we're preparing a stream
        //
        labelCDProgress.Text = 
          string.Format("Preparing stream for {0}", burnData.filename);
        progressBarCD.Value = (int)burnData.currentTrackNumber;
        progressBarCD.Maximum = burnData.totalTracks;
    }
    else if (burnData.task == BURN_MEDIA_TASK.BURN_MEDIA_TASK_WRITING)
    {
        switch (burnData.currentAction)
        {
        case IMAPI_FORMAT2_TAO_WRITE_ACTION.IMAPI_FORMAT2_TAO_WRITE_ACTION_PREPARING:
            labelCDProgress.Text = string.Format("Writing Track {0} - {1} of {2}",
            burnData.filename, burnData.currentTrackNumber, burnData.totalTracks);
            progressBarCD.Value = (int)burnData.currentTrackNumber;
            progressBarCD.Maximum = burnData.totalTracks;
            break;
        case IMAPI_FORMAT2_TAO_WRITE_ACTION.IMAPI_FORMAT2_TAO_WRITE_ACTION_WRITING:
            long writtenSectors = burnData.lastWrittenLba - burnData.startLba;
            if (writtenSectors > 0 && burnData.sectorCount > 0)
            {
                int percent = (int)((100 * writtenSectors) / burnData.sectorCount);
                labelStatusText.Text = string.Format("Progress: {0}%", percent);
                statusProgressBar.Value = percent;
            }
            else
            {
                labelStatusText.Text = "Track Progress 0%";
                statusProgressBar.Value = 0;
            }
            break;
        case IMAPI_FORMAT2_TAO_WRITE_ACTION.IMAPI_FORMAT2_TAO_WRITE_ACTION_FINISHING:
            labelStatusText.Text = "Finishing...";
            break;
        }
    }
}

References

History

  • April 15, 2007 - Initial release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)