使用 IMAPI2 创建音频 CD






4.86/5 (18投票s)
使用 Image Mastering API 创建 Red Book Audio CD
引言
这是我关于使用 IMAPI2(Microsoft 映像掌握 API 的第二个版本)刻录媒体的第三篇文章。在我上一篇文章《使用 C# 和 IMAPI2 刻录和擦除 CD/DVD/Blu-ray 媒体》之后,我收到了一些关于创建音频 CD 的咨询,所以我决定写这篇后续文章。本文将更详细地介绍 IDiscFormat2TrackAtOnce
接口,这是 IMAPI2 用于创建音频 CD 的接口。
背景
阅读上一篇文章会有帮助,但即使不读,也有几点必须了解。IMAPI2 COM DLL 已包含在 Microsoft Vista 中,但如果您使用的是 Windows XP 或 Windows 2003,可以从微软网站下载 IMAPI2 更新包。IMAPI2 通过两个 COM DLL 实现:imapi2.dll 和 imapi2fs.dll。imapi2.dll 处理大部分设备和刻录 API,而 imapi2fs.dll 处理所有文件系统和 IStream
API。**不要**将 IMAPI2 COM 引用添加到您的项目中。这会导致 IStream
接口发生冲突,如果您尝试在 IMAPI2 接口中使用由 IMAPI2FS 创建的 IStream
,您将遇到类似以下内容的错误
Unable to cast object of type 'IMAPI2FS.FsiStreamClass' to type 'IMAPI2.IStream'
我在上一篇文章中详细介绍了这个问题。您需要使用我的互操作文件 Imapi2interop.cs,它提供了 IMAPI2 的所有接口和事件。
Using the Code
using IMAPI2.Interop;
[assembly: ComVisible(true)]
- 请确保 Windows XP 和 2003 已安装文章开头提到的 IMAPI2 更新。
- 不要将 imapi2.dll 和 imapi2fs.dll COM DLL 添加到您的项目中。您将收到上述
IStream
错误。 - 将 imapi2interop.cs 文件添加到您的项目,并在您的应用程序中定义 '
IMAPI2.Interop
' 命名空间。 - 为了能够从 COM 接收事件处理器的通知,您需要打开 AssemblyInfo.cs 文件并将
ComVisible
属性更改为true
。
WAV 文件格式
Red Book 是 CD 音频的标准,它规定 CD 数据必须是 44.1 KHz、16 位、立体声、未压缩的 PCM 格式。这通常是带 WAV 头的文件。在本篇文章中,程序不会将其他格式转换为此格式,因此您必须拥有已采用此格式的 WAV 音频文件。
当您选择要刻录的文件时,我使用 MediaFile.IsWavProperFormat
方法来检查它们是否为正确的格式。
/// <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;
}
}
准备流
流的大小必须是 2352 的倍数,这是音频 CD 的扇区大小。我们使用一个简单的算法来计算它。
private long SECTOR_SIZE = 2352;
.
.
.
public Int64 SizeOnDisc
{
get
{
if (m_fileLength > 0)
{
return ((m_fileLength / SECTOR_SIZE) + 1) * SECTOR_SIZE;
}
return 0;
}
}
计算出大小后,我们将 WAV 的数据(减去头部)复制到分配的数据区域,然后创建 IStream
。
/// <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);
}
写入音轨
我在 BackgroundWorker
的 DoWork
事件中执行 CD 刻录。我首先准备好所有流,然后为每个音轨调用 MsftDiscFormat2TrackAtOnce
的 AddAudioTrack
方法。
/// <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();
}
更新用户界面
我为 IDiscFormat2TrackAtOnce
接口的 Update
事件创建了一个事件处理程序。当事件处理程序被调用时,它会传递一个 IDiscFormat2TrackAtOnceEventArgs
对象,该对象提供了当前音轨号、经过时间等值。我将这些值复制到我的 BurnData
对象中,并调用 BackgroundWorker
的 ReportProgress
方法。
/// <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);
}
这会导致 BackgroundWorker
的 ProgressChanged
事件被触发,从而允许应用程序在 UI 线程中更新用户界面。
/// <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;
}
}
}
参考文献
历史
- 2007 年 4 月 15 日 - 首次发布