65.9K
CodeProject 正在变化。 阅读更多。
Home

关于读取音频 CD 的教程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (33投票s)

2006年9月26日

CPOL

7分钟阅读

viewsIcon

159805

downloadIcon

1927

这篇易于理解的教程详细介绍了您需要了解的关于音频 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=4492length=16110 扇区),对 DeviceIoControl 的调用将失败,GetLastError 设置为 87ERROR_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_MSFIOCTL_CDROM_PAUSE_AUDIOIOCTL_CDROM_RESUME_AUDIOIOCTL_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

© . All rights reserved.