本文介紹如何使用 C++ 實作 CD 音軌抓取並轉換為 WAV 檔案。CD 抓軌有多種方法,這裡示範其中一種:透過 CreateFile 取得裝置控制代碼,再以 DeviceIoControl 存取裝置資訊,結合 WAVE 檔案結構 WAVEFORMATEX 將讀取的資料寫入檔案,生成標準 WAV 格式。
ntddcdrm.h (NTDDK 開發套件)winioctl.hMmreg.h使用 GetDriveType 判斷裝置類型,傳回值 5 代表 CD-ROM。詳細傳回類型請參考 MSDN。
以 CreateFile 取得裝置控制代碼:
HANDLE m_hDevice;
CString FileName = _T("F:");
m_hDevice = CreateFile(
_T("\\\\.\\") + FileName, // 裝置路徑
GENERIC_READ, // 讀取權限
FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享模式
NULL, // 預設安全性描述元
OPEN_EXISTING, // 開啟現有裝置
0, // 無特殊屬性
NULL // 無範本檔案
);
取得控制代碼後,使用 DeviceIoControl 獲取相關資訊。函數原型:
BOOL DeviceIoControl(
HANDLE hDevice, // 裝置控制代碼
DWORD dwIoControlCode, // 控制碼
LPVOID lpInBuffer, // 輸入緩衝區指標
DWORD nInBufferSize, // 輸入緩衝區長度
LPVOID lpOutBuffer, // 輸出緩衝區指標
DWORD nOutBufferSize, // 輸出緩衝區長度
LPDWORD lpBytesReturned, // 實際傳回位元組數
LPOVERLAPPED lpOverlapped // 重疊結構指標
);
使用 IOCTL_CDROM_READ_TOC 控制碼輸出 CDROM_TOC 結構:
BOOL bResult;
DWORD dwOutBytes;
CDROM_TOC CdromTOC; // 音軌資訊結構,詳見 MSDN
bResult = DeviceIoControl(
m_hDevice,
IOCTL_CDROM_READ_TOC,
NULL, 0,
&CdromTOC,
sizeof(CdromTOC),
&dwOutBytes,
(LPOVERLAPPED)NULL
);
DWORD GetStartSector(int track)
{
return (CdromTOC.TrackData[track-1].Address[1]*60*75 +
CdromTOC.TrackData[track-1].Address[2]*75 +
CdromTOC.TrackData[track-1].Address[3]) - 150;
}
DWORD GetEndSector(int track)
{
return (CdromTOC.TrackData[track].Address[1]*60*75 +
CdromTOC.TrackData[track].Address[2]*75 +
CdromTOC.TrackData[track].Address[3]) - 151;
}
使用 IOCTL_CDROM_RAW_READ 輸入 RAW_READ_INFO 結構,輸出取得磁區內容:
BOOL ReadSector(int sector, BYTE Buffer[], int NumSectors)
{
DWORD dwOutBytes;
RAW_READ_INFO rri; // 結構詳見 MSDN
rri.TrackMode = (TRACK_MODE_TYPE)2;
rri.SectorCount = (DWORD)NumSectors;
rri.DiskOffset = (DWORD64)(sector * CB_CDROMSECTOR);
return DeviceIoControl(
m_hDevice, IOCTL_CDROM_RAW_READ,
&rri, sizeof(rri),
Buffer, (DWORD)NumSectors * CB_AUDIO,
&dwOutBytes, (LPOVERLAPPED)NULL
);
}
WAV 是簡單的 RIFF 檔案,格式類型為 "WAVE"。RIFF 區塊包含兩個子區塊:"fmt " 與 "data"。"fmt " 子區塊由 WAVEFORMATEX 結構組成,大小為 sizeof(WAVEFORMATEX);"data" 子區塊存放實際聲音資料。
RIFF 標識 (4 bytes)
資料大小 (4 bytes)
格式類型 "WAVE" (4 bytes)
"fmt " 標識 (4 bytes)
fmt 區塊大小 (4 bytes)
WAVEFORMATEX 結構
"data" 標識 (4 bytes)
聲音資料大小 (4 bytes)
聲音資料
WAVEFORMATEX 結構定義:
typedef struct {
WORD wFormatTag; // 編碼格式,如 WAVE_FORMAT_PCM
WORD nChannels; // 聲道數:1=單聲道, 2=雙聲道
DWORD nSamplesPerSec; // 取樣頻率
DWORD nAvgBytesPerSec; // 每秒資料量
WORD nBlockAlign; // 區塊對齊
WORD wBitsPerSample; // 取樣位元數
WORD cbSize; // 擴充資訊大小
} WAVEFORMATEX, *PWAVEFORMATEX;
DWORD m_WaveHeaderSize = 38;
DWORD m_WaveFormatSize = 18;
DWORD m_AudioDataSize = 0;
DWORD m_WrittenBytes = 0;
WAVEFORMATEX m_WaveFormatEx;
m_WaveFormatEx.wFormatTag = WAVE_FORMAT_PCM;
m_WaveFormatEx.nSamplesPerSec = 48000;
m_WaveFormatEx.wBitsPerSample = 16;
m_WaveFormatEx.nChannels = 2;
m_WaveFormatEx.cbSize = 0;
m_WaveFormatEx.nBlockAlign = m_WaveFormatEx.nChannels * (m_WaveFormatEx.wBitsPerSample / 8);
m_WaveFormatEx.nAvgBytesPerSec = m_WaveFormatEx.nSamplesPerSec * m_WaveFormatEx.nBlockAlign;
CFile m_file;
CFileException fileException;
CString m_csFileName = m_SavePath;
m_file.Open(m_csFileName, CFile::modeCreate | CFile::modeReadWrite, &fileException);
int StartSect = GetStartSector(m_List.GetCurSel() + 1);
int EndSect = GetEndSector(m_List.GetCurSel() + 1);
DWORD Bytes2Read = (EndSect - StartSect) * CB_AUDIO;
m_AudioDataSize = Bytes2Read;
BYTE Data[CB_AUDIO * NSECTORS];
m_file.SeekToBegin();
m_file.Write("RIFF", 4);
unsigned int Sec = m_AudioDataSize + m_WaveHeaderSize;
m_file.Write(&Sec, sizeof(Sec));
m_file.Write("WAVE", 4);
m_file.Write("fmt ", 4);
m_file.Write(&m_WaveFormatSize, sizeof(m_WaveFormatSize));
m_file.Write(&m_WaveFormatEx.wFormatTag, sizeof(m_WaveFormatEx.wFormatTag));
m_file.Write(&m_WaveFormatEx.nChannels, sizeof(m_WaveFormatEx.nChannels));
m_file.Write(&m_WaveFormatEx.nSamplesPerSec, sizeof(m_WaveFormatEx.nSamplesPerSec));
m_file.Write(&m_WaveFormatEx.nAvgBytesPerSec, sizeof(m_WaveFormatEx.nAvgBytesPerSec));
m_file.Write(&m_WaveFormatEx.nBlockAlign, sizeof(m_WaveFormatEx.nBlockAlign));
m_file.Write(&m_WaveFormatEx.wBitsPerSample, sizeof(m_WaveFormatEx.wBitsPerSample));
m_file.Write(&m_WaveFormatEx.cbSize, sizeof(m_WaveFormatEx.cbSize));
m_file.Write("data", 4);
m_file.Write(&m_AudioDataSize, sizeof(m_AudioDataSize));
DWORD m_seek = 46; // 標頭長度 46 bytes,從此位置寫入資料
for (int sector = StartSect; sector < EndSect; sector += NSECTORS)
{
int Sectors2Read = ((sector + NSECTORS) < EndSect) ? NSECTORS : (EndSect - sector);
if (ReadSector(sector, Data, Sectors2Read))
{
m_file.Write(Data, CB_AUDIO * Sectors2Read);
m_file.Seek(m_seek += CB_AUDIO * Sectors2Read, CFile::begin);
}
}
m_file.Close();
以上程式在 VC7 + Windows 2000 + NTDDK 環境下測試通過。完整原始碼請至:https://www.vckbase.net/code/viewcode.asp?id=2278