肖文鹏 (xiaowp@263.net), 自由软件爱好者
2004 年 3 月 01 日
本文详细介绍了音频CD的基本知识,以及如何在Linux下编写实用的CD播放软件,内容涵盖音轨处理、播放控制和音量调节等诸多方面。
在目前的多媒体应用中,CD所承担的重要作用早已勿庸置疑,本文详细介绍了音频CD的基本知识,以及如何在Linux下编写实用的CD播放软件,内容涵盖音轨处理、播放控制和音量调节等诸多方面。
音频CD
CD是目前正在被广泛使用的一种高效信息存储系统,它从最初起步到逐步成熟大约经历了十年左右的时间,期间涌现出来的行业标准和技术规范非常多,而影响最大的当数由Philips和Sony公司共同推出的CD音频(CD-Audio)和CD数字音频(CD-DA)规范,这就是人们经常提到的红皮书,它被包含在IEC 908标准中。
音频CD有足够的能力来提供高保真的声音,它的采样频率为44.1kHz,并且每个采样点都使用16 bit的量化级,这样CD播放器在输出音频数据时的速率将高达1.4 Mbps。除了最重要的音频数据之外,为了进行必要的纠错、同步或者调制,还需要在CD上存储其它一些额外数据,因此存储在光盘上的数据通常是原来的3倍左右,也就是说信道比特率(从CD中读出数据的速率)可能会达到4.3128 Mbps。
精密的光学设计和高效的数据编码,是CD具有很高存储密度的原因所在,而要想在Linux应用程序中对音频CD进行控制,关键是要理解音频数据在光盘上的编码方法和存储形式。音频CD采用EFM调制来对要存储的数据进行编码,虽然在调制过程中会产生需要额外存储的信道比特,但总的效果却可以使音频CD的容量提高25%左右。帧(frame)是在音频CD上可以读取的最小单位,它详细规定了音频数据、校验位、同步位、子码等是如何在光盘上存储的,如图1所示:
图1 CD的帧格式
音频CD上的每帧数据中都包含一个8 bit的子码,其中包含的信息有音轨的起止位置、音轨数目、光盘时钟、索引位置等,如果能够在程序中充分地利用它们,无疑将会更好地控制音频CD的播放。子码偶尔也会被用来存储一些与CD相关的文本数据,但与DVD这类新格式有所不同,CD在最初设计时并没有考虑到要用来保存大量的文本数据,因此红皮书只允许将专辑名称、歌曲名称、演唱者、作曲者、制片人等一些与唱片本身相关的文本数据附加到光盘上,不过这些对于普通的CD播放器来讲已经完全够用了。
设备控制
Linux内核将所有的硬件设备都表示成设备文件,并且提供与操作磁盘文件类似的方法来操作硬件设备,应用程序如果想对CD驱动器进行控制,最直接的办法是用open()、read()、close()等系统调用来操作/dev/cdrom这一设备文件。例如,下面的命令可以读取CD上的原始数据流,并将其在终端上显示出来:
| [xiaowp@linuxgam cd]$ cat /dev/cdrom |
在Linux上进行CD编程的原理其实非常简单,关键是要借助系统调用ioctl()来对设备文件/dev/cdrom进行各种控制。作为应用程序和设备驱动之间的接口,ioctl()负责将用户请求转换成对硬件设备的操作,它在调用时需要指定三个参数。第一个参数是要对其进行操作的设备描述符;第二个参数是一个整型的数值,它可以用来指定将对硬件进行何种请求;第三个参数是可选的,通常情况下是一个void型的指针,其主要作用是在应用程序和设备驱动之间交换一定数量的信息,具体到CD驱动器来讲一般是指向某个特定结构的指针,这些结构的具体定义可以在
点击(此处)折叠或打开
|
上述程序的逻辑流程非常简单:首先是利用open()系统调用以只读(O_RDONLY)方式打开CD对应的设备文件;然后通过ioctl()系统调用执行CDROMEJECT命令,请求驱动器退出光盘;最后再借助close()系统调用来关闭已经打开的设备文件,从而完成对硬件的操作。需要注意的是,大多数CD驱动器都不允许光盘在已经被加载(mount)的状态下弹出,因此在运行上述程序之前请先卸载(umount)那些加载上了的光盘。
Linux内核目前能够支持的CD驱动器非常多,包括绝大部分采用IDE或者SCSI接口的光驱,由于各种CD驱动器在硬件设计上存在着或多或少的差异,因此与之对应的驱动程序也就会有所不同,其中最直接的表现莫过于各自采用了互不相同的ioctl命令集。例如,大部分采用SCSI接口的CD驱动器会提供一组额外的ioctl命令,以便能够充分利用SCSI命令来提高操作性能,这些ioctl命令对于IDE接口的CD驱动器来讲显然是无法适用的。出于兼容性方面的考虑,在Linux下进行CD编程时,最好只使用那些定义在文件
表1列出了一些经常用到的ioctl命令:
音轨处理
CD上的数据在逻辑上是按照音轨来进行组织的,如果想在程序中控制CD的播放,首先必须理解音轨在CD上是如何分布的,以及如何对特定的音轨进行定位,这些都是之后对CD进行各种操作的基础。
3.1 计算音轨数目
一张音频CD通常包含两个主要部分:头部(header)和主体(body),其中头部主要用来描述音轨在CD上是如何组织的,而主体则主要用来保存实际的音频数据。CD头部中包含的信息非常重要,这当中就包括第一个和最后一个音轨的序号。通常说来,CD上第一个音轨的序号总是0,而最后一个音轨的序号则等于碟片上的有效音轨数,因此即便CD上只有一个音轨,第一个和最后一个音轨的序号也将分别为0和1。之所以会产生这样的结果,原因在于CD上还存在一个空白的起始(leadout)音轨,虽然它不包含任何有效的音频数据,但对于CD处理来讲却是非常重要的。
对CD进行控制的第一步是获取第一个和最后一个音轨的序号,从而计算出CD上所有音轨的数目,这可以通过调用ioctl()的CDROMREADTOCHDR命令来完成。在获取音轨数目时,向ioctl()函数传递的第三个参数应该是指向cdrom_tochdr结构的指针,该结构中包含两个成员,分别为第一个和最后一个音轨的序号:
| struct cdrom_tochdr
{ __u8 cdth_trk0; /* start track */ __u8 cdth_trk1; /* end track */ }; |
下面的代码示范了如何获取音频CD中的音轨数目:
点击(此处)折叠或打开
- /* * 代码清单2: track.c */
- #include <unistd.h>
- #include <stdlib.h>
- #include <fcntl.h>
- #include <sys/ioctl.h>
- #include <linux/cdrom.h>
- /* CD驱动器对应的设备文件 */
- #define DEVICE "/dev/cdrom"
- int main()
- {
- int fd;
- int status;
- int number;
- int first_track, last_track;
- struct cdrom_tochdr header;
- /* 打开设备 */
- fd = open(DEVICE, O_RDONLY);
- if (fd < 0) {
- perror("unable to open " DEVICE); exit(1);
- }
- /* 读取CD头部信息 */
- status = ioctl(fd, CDROMREADTOCHDR, &header);
- if (status != 0) {
- perror("CDROMREADTOCHDR ioctl failed"); exit(1);
- }
- /* 计算音轨数目 */
- first_track = header.cdth_trk0; last_track = header.cdth_trk1;
- number = last_track;
- printf("This CD has %d tracks.\n", number);
- /* 关闭设备 */
- status = close(fd);
- if (status != 0) {
- perror("unable to close " DEVICE);
- exit(1);
- }
- return 0; }
3.2 定位音轨位置
在音频CD上进行定位有三种可行的办法,其中最简单的一种方式是使用轨道(track)和索引(index)。轨道指的是CD上的逻辑位置,它一般对应于碟片上的某一首歌曲,每条轨道上都可能存在有多个索引点,CD播放器可以借助它们来进行定位。与硬盘上的磁道有所不同,CD上的轨道在物理上并不存在,所有的音频数据在CD上都是连续存放的,不同的歌曲之间没有真正意义上的分隔点,所谓轨道只是为了方便CD定位而提出来的一种逻辑概念。
第二种在CD上进行定位的方法是使用分/秒/帧(MSF)地址,MSF地址中的每个分量都代表某种具体的偏移量,其中M代表的是CD从开始播放到定位点所经历的分钟(minute)数,S代表是从M所确定的地址到定位点所经历的秒钟(second)数,而F代表的则是从M和S所确定的地址到定位点所间隔的帧(frame)数。使用MSF地址能够比较精确地进行CD定位,其中一分钟内所包含的秒数以及一秒钟内所包含的帧数,是在
| struct cdrom_msf0
{
__u8 minute; __u8 second; __u8 frame; }; |
最后一种进行CD定位的方法是使用逻辑块地址(LBA),它是用从CD起始位置到定位点间的帧数来进行计算的。LBA地址和MFS地址之存在着一定的换算关系,如果给定了MFS地址,则可以使用如下的公式来计算LBA地址:
| lba = minutes * 60 * 75 + seconds * 75 + frames - 150 |
公式中的150是CD上第一个逻辑帧的有效偏移,通常来讲是2秒,它在文件
3.2 获取音轨信息
CD的主体部分仅仅用来保存连续的音频数据,它在某种程度上可以看成是一个没有任何间隔的音乐流,也就是说其间不会有信息来指示某条音轨的开始和结束位置,所有对音轨进行描述的信息都保存在CD头部。内容描述表(TOC)是CD头部保存的最有价值的信息,它用来对CD中的每一条音轨进行详细地描述,包括它们各自的起始位置和长度等,使用ioctl的CDROMREADTOCENTRY命令可以读出这些数据。在获取音轨信息时,向ioctl()函数传递的第三个参数应该是指向cdrom_tocentry结构的指针,它可以用保存从CD头部获取的音轨信息:
| struct cdrom_tocentry
{
__u8 cdte_track; __u8 cdte_adr :4; __u8 cdte_ctrl :4; __u8 cdte_format; union cdrom_addr cdte_addr; __u8 cdte_datamode; }; union cdrom_addr { struct cdrom_msf0 msf; int lba; }; |
在将cdrom_tocentry结构传递给ioctl()系统调用之前,需要先在cdte_format域中指明期望返回的地址格式,此时使用CDROM_LBA将返回LBA格式的地址,而使用CDROM_MSF则将返回MSF格式的地址。除了地址格式之外,在调用ioctl()前还需要在cdte_track域中指明要返回哪一条音轨的相应信息,需要注意的是,如果想返回第一条有效音轨的信息,应该使用1而不是0,而如果想返回起始(leadout)音轨的信息,则应该使用CDROM_LEADOUT宏来实现。
一旦ioctl()系统调用成功完成,cdrom_tocentry结构中的其它域就会被正确地填充,此时若用掩码CDROM_DATA_TRACK来对cdte_ctrl域进行查询,就可以知道该轨道中保存的究竟是数据还是音乐,这对于那些混合模式的CD来讲非常有用。返回的轨道地址将保存在联合体cdte_addr中,但具体值则要取决于所采用的地址类型:使用LBA格式的地址时是一个整型数,而使用MFS格式的地址时将是一个cdrom_mfs0结构。
下面的代码示范了如何从CD中读取出所有音轨的信息:
点击(此处)折叠或打开
|
CD播放
对于一个Linux下的CD播放器软件来讲,最核心的功能莫过于如何控制CD的播放了,而具体说来又可以细分成回放、停止、暂停、继续四种基本操作,由于位于Linux内核中的CD驱动程序已经做了非常好的封装,所以要在程序中实现这些功能其实并不复杂。
4.1 回放(play)
要对音频CD中的指定部分进行播放,可以使用ioctl的CDROMPLAYTRKIND或者CDROMPLAYMSF命令来实现,两者的功能基本相似,都是对指定的音轨进行播放,差别仅在于各自使用的地址格式有所不同,由于MFS格式更适合于对CD进行控制,所以下面主要介绍一下ioctl的CDROMPLAYMSF命令。在使用CDROMPLAYMSF命令控制CD播放时,需要指明播放的起始位置和终止位置,这是通过向ioctl()系统调用中的第三个参数传递一个指向cdrom_msf结构的指针来完成的,该结构的定义如下所示:
| struct cdrom_msf
{
__u8 cdmsf_min0; /* start minute */ __u8 cdmsf_sec0; /* start second */ __u8 cdmsf_frame0; /* start frame */ __u8 cdmsf_min1; /* end minute */ __u8 cdmsf_sec1; /* end second */ __u8 cdmsf_frame1; /* end frame */ }; |
在将cdrom_msf结构传递给ioctl()系统调用之前,需要在cdmsf_min0、cdmsf_sec0和cdmsf_frame0域中指明播放的起始位置,并在cdmsf_min1、cdmsf_sec1和cdmsf_frame1域中指明播放的终止位置。一旦ioctl()系统调用成功完成,CD驱动程序就将从指定的位置处开始播放,并在到达终止位置时自动停止。通常CD驱动程序都不会直接提供播放指定音轨的方法,程序必须自己负责找到该音轨的起始位置和终止位置,并将它们传递给ioctl()系统调用。
下面的代码示范了如何控制CD的播放:
点击(此处)折叠或打开
|
4.2 停止(stop)
控制CD播放的底层细节是由驱动程序来完成的,因此一旦CD开始播放,就不再需要应用程序做任何操作,此时即便程序退出也不会对CD的播放产生任何影响。如果应用程序需要停止CD的播放,可以通过ioctl的CDROMSTOP命令来实现,该命令只有当CD正在播放时才有效。下面的代码示范了如何停止CD的播放:
点击(此处)折叠或打开
|
4.3 暂停(pause)
处于播放状态的CD在需要的时候可以暂时停止播放,这是通过ioctl的CDROMPAUSE命令来实现的,该命令也只有当CD正在播放时才有效。下面的代码示范了如何暂停CD的播放:
点击(此处)折叠或打开
- /* * 代码清单5: pause.c */
- #include <unistd.h>
- #include <stdlib.h>
- #include <fcntl.h>
- #include <sys/ioctl.h>
- #include <linux/cdrom.h>
- /* CD驱动器对应的设备文件 */
- #define DEVICE "/dev/cdrom"
- int main()
- {
- int fd;
- int status;
- /* 打开设备 */
- fd = open(DEVICE, O_RDONLY);
- if (fd < 0) { perror("unable to open " DEVICE); exit(1); }
- /* 暂停播放 */
- status = ioctl(fd, CDROMPAUSE);
- if (status != 0) { perror("CDROMPAUSE ioctl failed"); exit(1); }
- /* 关闭设备 */
- status = close(fd);
- if (status != 0) { perror("unable to close " DEVICE); exit(1); }
- return 0;
- }
4.4 继续(resume)
处于暂停状态的CD在需要的时候可以继续播放,这是通过ioctl的CDROMRESUME命令来完成的,下面的代码示范了当CD进入暂停状态之后,如何控制其继续播放:
点击(此处)折叠或打开
- /* * 代码清单6: resume.c */
- #include <unistd.h>
- #include <stdlib.h>
- #include <fcntl.h>
- #include <sys/ioctl.h>
- #include <linux/cdrom.h>
- /* CD驱动器对应的设备文件 */
- #define DEVICE "/dev/cdrom"
- int main()
- {
- int fd;
- int status;
- /* 打开设备 */
- fd = open(DEVICE, O_RDONLY);
- if (fd < 0) { perror("unable to open " DEVICE); exit(1); }
- /* 继续播放 */
- status = ioctl(fd, CDROMRESUME);
- if (status != 0) { perror("CDROMRESUME ioctl failed"); exit(1); }
- /* 关闭设备 */
- status = close(fd);
- if (status != 0) { perror("unable to close " DEVICE); exit(1); }
- return 0;
- }
音量调节
大部分Linux下的CD驱动程序都向上层应用提供了相应的接口,用来对CD播放时的音量进行调节,这可以通过ioctl的CDROMVOLCTRL命令来完成。在设置CD音量时,向ioctl()函数传递的第三个参数应该是指向cdrom_volctrl结构的指针,它可以用来指明各个声道的音量:
| struct cdrom_volctrl
{
__u8 channel0; __u8 channel1; __u8 channel2; __u8 channel3; }; |
cdrom_volctrl结构中一共有四个成员,但目前只用到channel0和channel1两个域,它们分别代表左声道和右声道的音量大小,而channel2和channel3则是为今后的四声道CD或者四声道声卡来提供扩展的,实际运用时这两个值都应该设置成0。cdrom_volctrl结构中每个成员的取值范围都是0到255,它们会影响到CD驱动器最终输出音频时的增益大小。在CD播放软件中,对CD的输出音量进行调节是一个很基本的要求,下面的代码示范了如何实现这一功能:
点击(此处)折叠或打开
- /* * 代码清单7: volume.c */
- #include <unistd.h>
- #include <stdlib.h>
- #include <fcntl.h>
- #include <sys/ioctl.h>
- #include <linux/cdrom.h>
- /* CD驱动器对应的设备文件 */
- #define DEVICE "/dev/cdrom"
- int main()
- {
- int fd;
- int status;
- struct cdrom_volctrl volume;
- /* 打开设备 */
- fd = open(DEVICE, O_RDONLY);
- if (fd < 0) { perror("unable to open " DEVICE); exit(1); }
- /* 设备音量 */
- volume.channel0 = 128;
- volume.channel1 = 128;
- volume.channel2 = 0;
- volume.channel3 = 0;
- status = ioctl(fd, CDROMVOLCTRL, &volume);
- if (status != 0) { perror("CDROMVOLCTRL ioctl failed"); exit(1); }
- /* 关闭设备 */
- status = close(fd);
- if (status != 0) { perror("unable to close " DEVICE); exit(1); }
- return 0;
- }
除了能够对音量进行调节之外,通过ioctl的CDROMVOLREAD命令还可以读取CD当前音量的大小,但并不是所有Linux下的CD驱动程序都支持这一功能。
子码信息
在前面介绍音频CD时曾经提到,CD上的每帧数据中都包含一个8 bit的子码,子码中的每一位都具有特定的含义,而所有帧中的子码合在一起则构成了8条子通道(subchannel),它们可以用来保存一些与CD相关的信息。通过ioctl的CDROMSUBCHNL命令可以获得CD子通道中的一些信息,包括当前正在播放的音轨地址,以及CD的当前状态等,这些信息通常被保存在Q通道中。
CD播放器如果想输出一些与CD相关的信息,或者想查询CD的当前状态,都可以通过ioctl的CDROMSUBCHNL命令来实现。在读取CD子通道中的信息时,向ioctl()函数传递的第三个参数应该是指向cdrom_subchnl结构的指针:
| struct cdrom_subchnl
{
__u8 cdsc_format; __u8 cdsc_audiostatus; __u8 cdsc_adr: 4; __u8 cdsc_ctrl: 4; __u8 cdsc_trk; __u8 cdsc_ind; union cdrom_addr cdsc_absaddr; union cdrom_addr cdsc_reladdr; }; |
在将cdrom_subchnl结构传递给ioctl()系统调用之前,需要先在cdsc_format域中指明期望返回的地址格式,使用CDROM_LBA将返回LBA格式的地址,而使用CDROM_MSF则将返回MSF格式的地址。一旦ioctl()系统调用成功完成,当前正在播放的轨道和索引就将保存在cdsc_trk和cdsc_ind域中,而当前播放点的绝对位置(从当前CD起始处算起)和相对位置(从当前音轨起始处算起)则保存在cdsc_absaddr和cdsc_reladdr域中。此外,还可以从cdsc_audiostatus域中获得CD的当前状态,包括回放、停止和暂停等。
下面的代码示范了如何从子通道中获得一些与CD相关的信息:
点击(此处)折叠或打开
|
小结
为Linux编写CD播放器的关键是如何充分利用驱动程序提供的各种功能,由于长久以来缺乏相应的标准,因此并不是所有CD驱动程序提供的功能接口都是一致的,这给应用程序的编写造成了一定的困难。David van Leeuwen正在试图对Linux下所有的CD驱动程序进行标准化,他希望通过提供一个与硬件无关的软件抽象层,来为应用程序提供一致的编程接口。