Click here to Skip to main content
Click here to Skip to main content

Creating Audio CDs using IMAPI2

, 14 Apr 2008
Rate this:
Please Sign up or sign in to vote.
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

  • 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:
  • using IMAPI2.Interop;
  • 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:
  • [assembly: ComVisible(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:

/// <span class="code-SummaryComment"><summary></span>
/// Determines if the Wav file is the proper format to be written to CD
/// The proper format is uncompressed PCM, 44.1KHz, Stereo
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="wavFile">the selected wav file</param></span>
/// <span class="code-SummaryComment"><returns>true if proper format, otherwise false</returns></span>
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:

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:

/// <span class="code-SummaryComment"><summary></span>
/// Prepares a stream to be written to the media
/// <span class="code-SummaryComment"></summary></span>
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:

/// <span class="code-SummaryComment"><summary></span>
/// The thread that does the burning of the media
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="sender"></param></span>
/// <span class="code-SummaryComment"><param name="e"></param></span>
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.

/// <span class="code-SummaryComment"><summary></span>
/// Update notification from IDiscFormat2TrackAtOnce
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="sender"></param></span>
/// <span class="code-SummaryComment"><param name="progress"></param></span>
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.

/// <span class="code-SummaryComment"><summary></span>
/// Update the user interface with the current progress
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="sender"></param></span>
/// <span class="code-SummaryComment"><param name="e"></param></span>
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)

Share

About the Author

Eric Haddan
Software Developer (Senior) Tranxition Corp
United States United States
Thank you for voting on my articles!
 
MCSD.NET in C#
 
LinkedIn[^]
 
The Tranxition Developer's Blog[^]

Comments and Discussions

 
QuestionNo Progress Update Pinmemberprtsoft30-Sep-12 15:45 
QuestionVolume Name to Cd Pinmembergauri_21vaidya19-Sep-12 23:20 
QuestionA few problems..... Pinmemberdavebuk3-Apr-12 4:48 
First of all great work !
 
1) Will this work on windows 7, if not can you suggest any modifications or alternatives?
 
2) I am getting the error
 
HRESULT: 0xC0AA0007
DID NOT PASS BURN VERIFICATION
 
I am not sure what is causing this, I have checked that the wav files I am adding are of the correct format but still not sure how to handle this problem.
 
Thanks for any suggestions as I am quite new to visual studio (I am using Visual Studio 2010 on Windows 7)
GeneralMy vote of 5 Pinmembere1lvin30-Nov-11 8:50 
QuestionRecorder not supported Pinmemberstankomix127-Nov-11 21:32 
QuestionPrepareStream PinmemberNickm3248-Nov-11 12:35 
QuestionLoud Pop Between Tracks PinmemberMickey Marshall25-Jul-11 13:56 
AnswerRe: Loud Pop Between Tracks PinmemberMickey Marshall10-Aug-11 13:09 
GeneralRe: Loud Pop Between Tracks PinmemberStefan Haglund3-Apr-13 0:37 
QuestionOn the Fly Stream Pinmemberstixoffire25-Jun-11 20:44 
GeneralCD-TEXT Pinmemberdjayme819-Apr-11 10:59 
QuestionAccess Denied Errors - When using virtual CD/DVD [modified] Pinmemberstixoffire7-Dec-10 2:19 
Generalconvert them to 44.1Khz Stereo uncompressed PCM Pinmemberprogrammerdon25-Aug-10 6:01 
GeneralTrack Names and Disc Name... Pinmembermcgin15911-Oct-09 7:53 
GeneralRe: Track Names and Disc Name... PinmemberEric Haddan1-Oct-09 9:33 
GeneralRe: Track Names and Disc Name... Pinmembermcgin15911-Oct-09 9:36 
GeneralPermission request Pinmemberacosano31-Aug-09 16:15 
GeneralRe: Permission request PinmemberEric Haddan31-Aug-09 17:19 
QuestionBurned CDs Playback at high speed Pinmemberdc_chant4-Jul-09 4:58 
AnswerRe: Burned CDs Playback at high speed PinmemberEric Haddan4-Jul-09 10:32 
QuestionCool project, couple issues. PinmemberGladely3-May-09 8:27 
AnswerRe: Cool project, couple issues. PinmemberEric Haddan3-May-09 15:22 
GeneralCOM problem in "trackAtOnce.AddAudioTrack(stream);" Pinmemberhenur24-Nov-08 21:50 
QuestionWhen i add the.... PinmemberAmrykid2-Oct-08 13:24 
QuestionSpeed and Gapless CDs Pinmemberradio4231-Aug-08 21:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140902.1 | Last Updated 14 Apr 2008
Article Copyright 2008 by Eric Haddan
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid