关于读取音频 CD 的教程






4.79/5 (33投票s)
这篇易于理解的教程详细介绍了您需要了解的关于音频 CD 的所有知识以及如何提取音轨。
引言
我非常生气,因为找不到有关音频 CD 的教程。我找到了一些关于音频 CD 结构的文件,但没有能用于我的“将音频音轨保存到文件”类的东西。所以,正如您所见,我终于做到了……在编写代码的过程中,我主要参考了两篇文章。其中一篇由 Idael Cardoso 撰写,并在此 CodeProject 上发布,不幸的是它是用 C# 编写的,并且没有解释音频 CD 提取技术,代码也没有得到很好的注释…… 另一篇文章,实际上是 Larry Osterman 的一系列文章,并不那么容易重建代码。所以我写了我的类,具有基本功能(其中一些是从 Idael 和 Larry 那里抄来的)。考虑到我花费数小时搜索也没有找到任何可靠的 C++ 音频 CD 提取源代码,我决定发布我的类并附带一些解释。
音频格式
在整篇文章中,我讨论的是 PCM 格式的未压缩波形音频。所以不要问我关于 MP3 之类的问题。波形数据有一些属性。决定波形数据外观的属性是
- 每样本位数:每样本位数决定声音包含的频率的准确性和带宽。常用值为 8 位和 16 位。正如您可能猜到的,单个 16 位频率需要一个 16 位字变量来存储。
- 声道数:指定波形数据使用的声道数。常用值为 1(单声道)和 2(立体声)。立体声音频数据的大小是单声道数据的两倍。立体声音频的频率以块的形式存储,例如 [左声道频率] [右声道频率] [左声道频率] ...
- 采样率:一个块包含所有声道在某个时刻的频率数据。因此,一个使用 16 位立体声的波形样本的块大小为 4 字节(
声道数 * (每样本位数 / 8)
)。采样率决定了用于显示 1 秒钟的块数。波形中常用的值是 44100Hz、22050Hz、11025Hz 和 8000Hz。
给定这三个属性(每秒位数、声道数、采样率),您就可以计算出波形音频所需的空间。例如,音频 CD 格式始终是 44100Hz、16 位、立体声。一秒钟的音频需要 44100 * 2 * 2 字节。如果您一秒钟需要 176400 字节,那么 80 分钟(音频 CD 的最大数据容量)将需要 846720000 字节(807MB)。
因此,MP3 格式如此受欢迎也就不足为奇了!一张 CD 质量的 4 分钟音频样本在硬盘上大约需要 40MB。MP3 文件质量几乎相同,但只需要大约 4MB!
顺便说一句:“频率”这个词并不是音频数据的真实含义。它代表了接近它的东西,但我认为这样想象是可以的。
关于音频 CD
存储在 CD 上的数据由扇区确定。“普通”CD 扇区的大小为 2048 字节(2KB)。音频 CD 的特别之处在于,它们的音频数据存储在大小为 2352 字节的扇区中。这是因为一个扇区应该存储 1/75 秒的音频数据。一秒钟需要 176400 字节,所以 1/75 需要 2352 字节。
每个音频 CD 都包含一个目录(TOC)。它保存有关音轨数量和 CD 上每个音轨地址的信息。通常,Windows 在插入 CD 时会加载 TOC,并在 CD 更改时更新。您可以通过一次调用 CD-ROM 驱动器来检索 TOC。因为 Windows 拥有 TOC,所以 CD 驱动器不会启动来获取数据,您将直接从操作系统获取。在这里您可以看到它的结构。
typedef struct _TRACK_DATA
{
UCHAR Reserved;
UCHAR Control : 4;
UCHAR Adr : 4;
UCHAR TrackNumber;
UCHAR Reserved1;
UCHAR Address[4];
} TRACK_DATA;
typedef struct _CDROM_TOC
{
UCHAR Length[2];
UCHAR FirstTrack;
UCHAR LastTrack;
TRACK_DATA TrackData[100];
} CDROM_TOC;
CDROM_TOC
结构包含 FirstTrack
(1)和 LastTrack
(最大音轨号)。CDROM_TOC::TrackData[0]
包含 CD 上第一个音轨的信息。
每个音轨都有一个地址。它表示音轨的播放时间,使用小时、分钟、秒和帧的独立成员。“帧”值(Address[3]
)以秒的 1/75 部分给出 -> 请记住:75 帧构成一秒,一帧占用一个扇区。
要指定波形音轨的扇区大小,请使用以下函数
ULONG AddressToSectors( UCHAR Addr[4] )
{
ULONG Sectors = Addr[1]*75*60 + Addr[2]*75 + Addr[3];
return Sectors - 150;
}
您可能已经注意到,地址的小时值没有使用。我看不出它的意义,但如果 CD 音轨超过 60 分钟,小时值将保持未使用状态,分钟将超过 60 的标记。减去 150 的值是因为,如我所说,第一个可访问地址比 CD 开始晚 2 秒(150 帧)。
要读取音轨数据,我们需要知道音轨的地址和长度,两者都以扇区为单位。
对于我的类,我选择了一个非常小的结构来保存音轨信息。
struct CDTRACK
{
ULONG Address;
ULONG Length;
};
要计算地址,只需将 TRACK_DATA::Address
值传递给 AddressToSectors
。要计算长度,请从下一个音轨的扇区地址减去当前音轨的扇区地址。
CDROM_TOC Toc;
CDTRACK SmallData;
SmallData.Address = AddressToSectors( Toc.TrackData[x].Address );
SmallData.Length = AddressToSectors( Toc.TrackData[x+1].Address )
- SmallData.Address;
访问光盘驱动器
一旦您知道,访问光盘驱动器就非常简单了。您使用 CreateFile
创建一个句柄,使用 DeviceIoControl
与 CD 驱动器通信,并通过 CloseHandle
关闭该句柄。
使用 "CreateFile"
许多人会知道 CreateFile
的用法,所以这里只是一个关于如何创建句柄的简短代码示例。
char Fn[8] = { '\\', '\\', '.', '\\', Drive, ':', '\0' };
HANDLE hCD = CreateFile( Fn, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL );
注意 CreateFile
的路径参数。它必须采用 \\.\F: 的形式(如果 F 是您的 CD 驱动器)。
使用 "DeviceIoControl"
请注意:DeviceIoControl
在 Win2000/XP 上运行良好。要在 Win95/98/Me 上使用它,请点击 此处。
BOOL DeviceIoControl(
HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped);
hDevice
接受我们 CD 的句柄。dwIoControlCode
获取一个 IOCTL_
... 消息,接下来的参数指定输入、输出及其大小。此外,还有一个虚拟参数(lpBytesReturned
),我们将始终传递一个 ULONG
。我们不会使用 lpOverlapped
参数,因此将其设置为 NULL
。
我们感兴趣的有几个 IOCTL
消息
IOCTL_CDROM_READ_TOC
:如上所述,读取 TOC。将两个输入参数设置为 0,输出参数设置为CDROM_TOC*
和sizeof(CDROM_TOC)
IOCTL_CDROM_RAW_READ
:从 CD 驱动器读取原始数据。您必须传递一个RAW_READ_INFO*
和sizeof(RAW_READ_INFO)
作为输入,并将一个有效的缓冲区指针以及它可以包含的字节作为输出。
此时我的代码失败了很长时间。在RAW_READ_INFO
中,您指定要从哪些扇区以及读取多少扇区。仅在RAW_READ_INFO
中指定歌曲的扇区数据(例如,address=4492
,length=16110
扇区),对DeviceIoControl
的调用将失败,GetLastError
设置为87
,ERROR_INVALID_PARAMETER
。
一次可以读取的扇区数量存在上限!我没有找到最大数量,也许它取决于驱动器。但请确保<= 1000
的值应该有效,而20
左右的值则非常安全。
这是一个如何使用 IOCTL_CDROM_RAW_READ
的示例。考虑到我们不允许一次读取一个波形音轨,我们需要分块读取。这是代码的摘录
CDTRACK Track; // Filled with valid info
char* pBuf = new char [Track.Length*2352];
RAW_READ_INFO ReadInfo;
ReadInfo.TrackMode = CDDA; // Always use CDDA (numerical: 2)
ReadInfo.SectorCount = 20; // We'll read 20 sectors with each operation.
// Read the track-data in a loop. Read 20*2352 bytes per pass.
for ( ULONG i=0; i<Track.Length/20; i++ )
{
// Calculate the new offset from where to read.
ReadInfo.DiskOffset.QuadPart = (Track.Address + i*20) * 2048;
// Call DeviceIoControl and read the audio-data to out buffer.
ULONG Dummy;
if ( 0 == DeviceIoControl( hCD, IOCTL_CDROM_RAW_READ,
&ReadInfo, sizeof(ReadInfo),
pBuf+i*20*2352,
20*2352,
&Dummy, NULL ) )
{
delete [] pBuf;
return FALSE;
}
}
// Read the remaining sectors.
ReadInfo.SectorCount = Track.Length % 20;
ReadInfo.DiskOffset.QuadPart = (Track.Address + i*20) * 2048;
ULONG Dummy;
if ( 0 == DeviceIoControl( hCD, IOCTL_CDROM_RAW_READ,
&ReadInfo, sizeof(ReadInfo),
pBuf+i*20*2352,
ReadInfo.SectorCount*2352,
&Dummy, NULL ) )
{
delete [] pBuf;
return FALSE;
}
delete [] pBuf;
看起来很简单,是吧?我唯一纠结的是数字 2048
。这是整个 CD 提取过程中唯一我真正不理解的东西!如果将数字 2048
替换为 2352
会更有意义!这很奇怪……但这是唯一有效的方式,并且是一个巨大的障碍(如果您没有好的教程)。
其余代码应该是不言自明的。首先,数据以循环方式读取,每次 20 个扇区(20 * 2352 字节)。然后读取剩余的扇区。在读取过程中,会计算正确的 cd-offset
和 buf-offset
,并将音频数据存储到计算出的缓冲区偏移量。
此外,还有一些更值得关注的 IOCTL_
...-消息
IOCTL_STORAGE_CHECK_VERIFY
:检查您的 CD 驱动器是否可访问IOCTL_STORAGE_LOAD_MEDIA
:如果打开,则注入 CD 驱动器IOCTL_CDROM_EJECT_MEDIA
:弹出 CD 驱动器IOCTL_CDROM_GET_CONFIGURATION
:检索光盘类型(CD-ROM/CD-R/CD-RW/DVD-ROM/DVD-R/...)IOCTL_CDROM_PLAY_AUDIO_MSF
、IOCTL_CDROM_PAUSE_AUDIO
、IOCTL_CDROM_RESUME_AUDIO
、IOCTL_CDROM_STOP_AUDIO
:播放音频数据。控制起来非常简单!
使用代码
CAudioCD
类用于将音频音轨从 CD 提取到硬盘。这就是为什么该类只能做这么多事情。它能够
- 获取有关音轨数量和每个音轨播放长度的一些信息
- 将音轨读入内存
- 直接从 CD 将音轨读取到波形文件中
- 在单独的线程中执行读取
- 通知您当前进度(回调)
- 插入和弹出 CD,这是我的最爱 :D
使用代码应该非常简单。主类是 CAudioCD
。以下是使用该类的一个示例
#include "CAudioCD.h"
#include <stdio.h>
#define MY_CDROM_DRIVE 'F'
void OnAudioCDProgress( ULONG Track, ULONG Percentage, VOID* Param )
{
printf( "Ripping track nr. %i\n", Track );
printf( ": Progress at %i%%\n", Percentage );
}
int main( ... )
{
CAudioCD AudioCD;
if ( ! AudioCD.Open( MY_CDROM_DRIVE ) )
{
printf( "Cannot open cd-drive!\n" );
return 0;
}
ULONG TrackCount = AudioCD.GetTrackCount();
printf( "Track-Count: %i\n", TrackCount );
for ( ULONG i=0; i<TrackCount; i++ )
{
ULONG Time = AudioCD.GetTrackTime( i );
printf( "Track %i: %i:%.2i; %i bytes of size\n", i+1,
Time/60, Time%60, AudioCD.GetTrackSize(i) );
}
// Prepare param for reading...
AUDIOCD_READTRACK ReadInfo;
ReadInfo.Track = 7;
ReadInfo.SaveToFile = "C:\\Song.wav" );
ReadInfo.ProgressCb = OnAudioCDProgress;
if ( ! AudioCD.ReadTrack( &ReadInfo ) )
printf( "Cannot start reading track: %i\n", GetLastError() );
return 0;
}
我不会解释这段代码的任何内容,阅读它您就会明白。
最后...
希望这篇文章对您有所帮助,并希望您能原谅我糟糕的英语,我是德国人,上次写英语作文是在两年前的学校。所以,如果您发现任何语法或拼写错误,或者我在音频方面写错了什么,请联系我。
致敬,Michel