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
using IMAPI2.Interop;
[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:
public static bool IsWavProperFormat(string wavFile)
{
FileStream fileStream = null;
try
{
fileStream = File.OpenRead(wavFile);
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();
WAV_HEADER wavHeader =
(WAV_HEADER)Marshal.PtrToStructure(handle.AddrOfPinnedObject(),
typeof(WAV_HEADER));
if ((wavHeader.chunkID == 0x46464952) &&
(wavHeader.format == 0x45564157) &&
(wavHeader.formatChunkId == 0x20746d66) &&
(wavHeader.audioFormat == 1) &&
(wavHeader.numChannels == 2) &&
(wavHeader.sampleRate == 44100))
{
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
:
public void PrepareStream()
{
byte[] waveData = new byte[SizeOnDisc];
IntPtr fileData = Marshal.AllocHGlobal((IntPtr)SizeOnDisc);
FileStream fileStream = File.OpenRead(filePath);
int sizeOfHeader = Marshal.SizeOf(typeof(WAV_HEADER));
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:
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;
foreach (MediaFile mediaFile in listBoxFiles.Items)
{
if (backgroundWorker.CancellationPending)
{
break;
}
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();
}
trackAtOnce.Update += new DiscFormat2TrackAtOnce_EventHandler(trackAtOnce_Update);
trackAtOnce.PrepareMedia();
foreach (MediaFile mediaFile in listBoxFiles.Items)
{
if (backgroundWorker.CancellationPending)
{
e.Result = -1;
break;
}
m_burnData.filename = mediaFile.ToString();
IStream stream = mediaFile.GetTrackIStream();
trackAtOnce.AddAudioTrack(stream);
}
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.
void trackAtOnce_Update(object sender, object progress)
{
if (backgroundWorker.CancellationPending)
{
IDiscFormat2TrackAtOnce trackAtOnce = (IDiscFormat2TrackAtOnce)sender;
trackAtOnce.CancelAddTrack();
return;
}
IDiscFormat2TrackAtOnceEventArgs eventArgs = (IDiscFormat2TrackAtOnceEventArgs)progress;
m_burnData.task = BURN_MEDIA_TASK.BURN_MEDIA_TASK_WRITING;
m_burnData.currentTrackNumber = eventArgs.CurrentTrackNumber;
m_burnData.elapsedTime = eventArgs.ElapsedTime;
m_burnData.remainingTime = eventArgs.RemainingTime;
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;
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.
private void backgroundWorker_ProgressChanged(object sender,
ProgressChangedEventArgs e)
{
BurnData burnData = (BurnData)e.UserState;
if (burnData.task == BURN_MEDIA_TASK.BURN_MEDIA_TASK_PREPARING)
{
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