打造Linux下的CD播放器

351阅读 0评论2012-11-09 黑曼巴snake
分类:

打造Linux下的CD播放器
2004年02月29日 16:00 来源:ChinaUnix文档频道 作者:HonestQiao 编辑:周荣茂
级别: 初级

肖文鹏 (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的帧格式
图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驱动器来讲一般是指向某个特定结构的指针,这些结构的具体定义可以在中找到。 为了对如何控制CD有一个更加感性的认识,下面先来看一个具体的例子。对于大多数CD驱动器来讲,除了在前面板上提供有一个退出光盘的按钮之外,一般还允许通过软件的方法来控制光盘的弹出,下面的代码示范了在Linux下如何实现这一功能:


点击(此处)折叠或打开

  1. /* * 代码清单1: eject.c */
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <fcntl.h>
  5. #include <sys/ioctl.h>
  6. #include <linux/cdrom.h>

  7. /* CD驱动器对应的设备文件 */
  8. #define DEVICE "/dev/cdrom"

  9. int main()
  10. {
  11.  int fd;
  12.  int status;

  13.  /* 打开设备 */
  14.  fd = open(DEVICE, O_RDONLY);
  15.  if (fd < 0) {
  16.  perror("unable to open " DEVICE);
  17.  exit(1);
  18.  }

  19.  /* 退出光盘 */
  20.  status = ioctl(fd, CDROMEJECT);
  21.  if (status != 0) {
  22.  perror("CDROMEJECT ioctl failed");
  23.  exit(1);
  24.  }

  25.  /* 关闭设备 */
  26.  status = close(fd);
  27.  if (status != 0) {
  28.  perror("unable to close " DEVICE);
  29.  exit(1);
  30.  }
  31.  return 0;
  32. }

上述程序的逻辑流程非常简单:首先是利用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编程时,最好只使用那些定义在文件中的ioctl命令,因为它们绝大部分都能够很好地工作在所有的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中的音轨数目:


点击(此处)折叠或打开

  1. /* * 代码清单2: track.c */
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <fcntl.h>
  5. #include <sys/ioctl.h>
  6. #include <linux/cdrom.h>

  7. /* CD驱动器对应的设备文件 */
  8. #define DEVICE "/dev/cdrom"

  9. int main()
  10. {
  11. int fd;
  12. int status;
  13. int number;
  14. int first_track, last_track;
  15. struct cdrom_tochdr header;

  16. /* 打开设备 */
  17. fd = open(DEVICE, O_RDONLY);
  18. if (fd < 0) {
  19.     perror("unable to open " DEVICE); exit(1);
  20. }

  21. /* 读取CD头部信息 */
  22. status = ioctl(fd, CDROMREADTOCHDR, &header);

  23. if (status != 0) { 
  24.     perror("CDROMREADTOCHDR ioctl failed"); exit(1);
  25. }

  26. /* 计算音轨数目 */
  27. first_track = header.cdth_trk0; last_track = header.cdth_trk1;
  28. number = last_track;

  29. printf("This CD has %d tracks.\n", number);

  30. /* 关闭设备 */
  31. status = close(fd);
  32. if (status != 0) {
  33.     perror("unable to close " DEVICE);
  34.     exit(1);
  35. }
  36. 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定位,其中一分钟内所包含的秒数以及一秒钟内所包含的帧数,是在文件中用CD_SECS和CD_FRAMES这两个宏来进行定义的,默认情况下它们的值依次为60和75。MSF地址在驱动程序中是用cdrom_msf0结构来进行表示的:

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秒,它在文件中是用CD_MSF_OFFSET宏来进行表示的。每张音频CD都在其头部提供了一张表格,用来描述每一个轨道的LBA或者MFS地址,因此程序中不难在这三种地址之间进行转换,一般说来MFS地址更加适合于对音频CD的处理。

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中读取出所有音轨的信息:


点击(此处)折叠或打开

  1. /* * 代码清单3: info.c */
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <fcntl.h>
  5. #include <sys/ioctl.h>
  6. #include <linux/cdrom.h>

  7. /* CD驱动器对应的设备文件 */
  8. #define DEVICE "/dev/cdrom"
  9. int main()
  10. {
  11.     int fd;
  12.     int status;
  13.     int track;
  14.     struct cdrom_tochdr header;    /* TOC头部 */
  15.     struct cdrom_tocentry entry; /* TOC条目 */

  16.     /* 打开设备 */
  17.     fd = open(DEVICE, O_RDONLY);
  18.     if (fd < 0) {
  19.     perror("unable to open " DEVICE); exit(1);
  20.     }
  21.     /* 计算音轨数目 */
  22.     status = ioctl(fd, CDROMREADTOCHDR, &header);
  23.     if (status != 0) {
  24.          perror("CDROMREADTOCHDR ioctl failed");
  25.         exit(1);
  26.     }
  27.     /* 输出CD中各条音轨的信息 */ 
  28.     for (track = 1; track <= header.cdth_trk1; track ++) { 
  29.         entry.cdte_track = track; 
  30.         entry.cdte_format = CDROM_MSF; /* 采用MSF格式地址 */ 
  31.         status = ioctl(fd, CDROMREADTOCENTRY, &entry); 
  32.         if (status != 0) { 
  33.             perror("CDROMREADTOCENTRY ioctl failed"); 
  34.             exit(1); 
  35.         } 
  36.     printf("Track%02d: %5d %02d:%02d.%03d %c \n", track, entry.cdte_track, entry.cdte_addr.msf.minute, entry.cdte_addr.msf.second, entry.cdte_addr.msf.frame, (entry.cdte_ctrl & CDROM_DATA_TRACK) ? 'D' : 'A'); 
  37.      } /* 输出起始(leadout)音轨的信息 */ 

  38.     entry.cdte_track = CDROM_LEADOUT; 
  39.     status = ioctl(fd, CDROMREADTOCENTRY, &entry); 
  40.     if (status != 0) { 
  41.         perror("CDROMREADTOCENTRY ioctl failed"); 
  42.         exit(1); 
  43.     } 
  44.     printf("Track leadout: %02d:%02d.%03d \n", entry.cdte_addr.msf.minute, entry.cdte_addr.msf.second, entry.cdte_addr.msf.frame); 
  45.     /* 关闭设备 */ 
  46.     status = close(fd); 
  47.     if (status != 0) { 
  48.         perror("unable to close " DEVICE); 
  49.         exit(1); 
  50.     } 
  51. return 0; 
  52. }





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的播放:


点击(此处)折叠或打开

  1. /* * 代码清单4: play.c */
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <fcntl.h>
  5. #include <sys/ioctl.h>
  6. #include <linux/cdrom.h>

  7. /* CD驱动器对应的设备文件 */
  8. #define DEVICE "/dev/cdrom"
  9. int main()
  10. {
  11.      int fd;
  12.      int status;
  13.      int start_min, start_sec, start_frame;/* 播放起始地址 */
  14.      int end_min, end_sec, end_frame;    /* 播放结束地址 */
  15.      struct cdrom_tocentry entry; /* TOC条目 */
  16.      struct cdrom_msf region; /* 播放区域 */

  17.      /* 打开设备 */
  18.      fd = open(DEVICE, O_RDONLY);
  19.      if (fd < 0) {
  20.          perror("unable to open " DEVICE);
  21.          exit(1);
  22.      }
  23.      /* 计算播放起始点的地址 */
  24.      entry.cdte_track = 1;
  25.      entry.cdte_format = CDROM_MSF;
  26.      /* 采用MSF格式地址 */
  27.      status = ioctl(fd, CDROMREADTOCENTRY, &entry);
  28.      if (status != 0) {
  29.          perror("CDROMREADTOCENTRY ioctl failed");
  30.          exit(1);
  31.      }
  32.      start_min = entry.cdte_addr.msf.minute;
  33.      start_sec = entry.cdte_addr.msf.second;
  34.      start_frame = entry.cdte_addr.msf.frame;

  35.      /* 计算播放结束点的地址 */
  36.      entry.cdte_track = 2;
  37.      status = ioctl(fd, CDROMREADTOCENTRY, &entry);
  38.      if (status != 0) {
  39.           perror("CDROMREADTOCENTRY ioctl failed");
  40.           exit(1);
  41.      }
  42.      end_min = entry.cdte_addr.msf.minute;
  43.      end_sec = entry.cdte_addr.msf.second;
  44.      end_frame = entry.cdte_addr.msf.frame;
  45.      /* 指明播放区域 */
  46.      region.cdmsf_min0 = start_min;
  47.      region.cdmsf_sec0 = start_sec;
  48.      region.cdmsf_frame0 = start_frame;
  49.      region.cdmsf_min1 = end_min;
  50.      region.cdmsf_sec1 = end_sec;
  51.      region.cdmsf_frame1 = end_frame;
  52.     
  53.      /* 播放 */
  54.      status = ioctl(fd, CDROMPLAYMSF, &region);
  55.      if (status != 0) {
  56.          perror("CDROMPLAYMSF ioctl failed");
  57.          exit(1);
  58.      }
  59.      /* 关闭设备 */
  60.      status = close(fd);
  61.      if (status != 0) {
  62.          perror("unable to close " DEVICE);
  63.          exit(1);
  64.      }
  65.     
  66.      return 0;
  67. }


4.2 停止(stop)

控制CD播放的底层细节是由驱动程序来完成的,因此一旦CD开始播放,就不再需要应用程序做任何操作,此时即便程序退出也不会对CD的播放产生任何影响。如果应用程序需要停止CD的播放,可以通过ioctl的CDROMSTOP命令来实现,该命令只有当CD正在播放时才有效。下面的代码示范了如何停止CD的播放:


点击(此处)折叠或打开

  1. /* * 代码清单5: stop.c */
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <fcntl.h>
  5. #include <sys/ioctl.h>
  6. #include <linux/cdrom.h>

  7. /* CD驱动器对应的设备文件 */
  8. #define DEVICE "/dev/cdrom"

  9. int main()
  10. {
  11.      int fd;
  12.      int status;

  13.      /* 打开设备 */
  14.      fd = open(DEVICE, O_RDONLY);
  15.      if (fd < 0) {
  16.          perror("unable to open " DEVICE);
  17.          exit(1);
  18.      }

  19.      /* 停止播放 */
  20.      status = ioctl(fd, CDROMSTOP);
  21.      if (status != 0) {
  22.          perror("CDROMSTOP ioctl failed");
  23.          exit(1);
  24.      }
  25.      /* 关闭设备 */
  26.      status = close(fd);
  27.      if (status != 0) {
  28.          perror("unable to close " DEVICE);
  29.          exit(1);
  30.      }
  31.     
  32.      return 0;
  33. }

4.3 暂停(pause)

处于播放状态的CD在需要的时候可以暂时停止播放,这是通过ioctl的CDROMPAUSE命令来实现的,该命令也只有当CD正在播放时才有效。下面的代码示范了如何暂停CD的播放:


点击(此处)折叠或打开

  1. /* * 代码清单5: pause.c */
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <fcntl.h>
  5. #include <sys/ioctl.h>
  6. #include <linux/cdrom.h>

  7. /* CD驱动器对应的设备文件 */
  8. #define DEVICE "/dev/cdrom"

  9. int main()
  10. {
  11.  int fd;
  12.  int status;

  13.  /* 打开设备 */
  14.  fd = open(DEVICE, O_RDONLY);
  15.  if (fd < 0) { perror("unable to open " DEVICE); exit(1); }

  16.  /* 暂停播放 */
  17.  status = ioctl(fd, CDROMPAUSE);
  18.  if (status != 0) { perror("CDROMPAUSE ioctl failed"); exit(1); }

  19.  /* 关闭设备 */
  20.  status = close(fd);
  21.  if (status != 0) { perror("unable to close " DEVICE); exit(1); }
  22.  return 0;
  23. }

4.4 继续(resume)

处于暂停状态的CD在需要的时候可以继续播放,这是通过ioctl的CDROMRESUME命令来完成的,下面的代码示范了当CD进入暂停状态之后,如何控制其继续播放:


点击(此处)折叠或打开

  1. /* * 代码清单6: resume.c */
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <fcntl.h>
  5. #include <sys/ioctl.h>
  6. #include <linux/cdrom.h>

  7. /* CD驱动器对应的设备文件 */
  8. #define DEVICE "/dev/cdrom"
  9. int main()
  10. {
  11.  int fd;
  12.  int status;

  13.  /* 打开设备 */
  14.  fd = open(DEVICE, O_RDONLY);
  15.  if (fd < 0) { perror("unable to open " DEVICE); exit(1); }

  16.  /* 继续播放 */
  17.  status = ioctl(fd, CDROMRESUME);
  18.  if (status != 0) { perror("CDROMRESUME ioctl failed"); exit(1); }

  19.  /* 关闭设备 */
  20.  status = close(fd);
  21.  if (status != 0) { perror("unable to close " DEVICE); exit(1); }

  22.  return 0;
  23. }





音量调节

大部分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的输出音量进行调节是一个很基本的要求,下面的代码示范了如何实现这一功能:


点击(此处)折叠或打开

  1. /* * 代码清单7: volume.c */
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <fcntl.h>
  5. #include <sys/ioctl.h>
  6. #include <linux/cdrom.h>

  7. /* CD驱动器对应的设备文件 */
  8. #define DEVICE "/dev/cdrom"
  9. int main()
  10. {
  11.  int fd;
  12.  int status;
  13.  struct cdrom_volctrl volume;

  14.  /* 打开设备 */
  15.  fd = open(DEVICE, O_RDONLY);
  16.  if (fd < 0) { perror("unable to open " DEVICE); exit(1); }

  17.  /* 设备音量 */
  18.  volume.channel0 = 128;
  19.  volume.channel1 = 128;
  20.  volume.channel2 = 0;
  21.  volume.channel3 = 0;
  22.  status = ioctl(fd, CDROMVOLCTRL, &volume);
  23.  if (status != 0) { perror("CDROMVOLCTRL ioctl failed"); exit(1); }

  24.  /* 关闭设备 */
  25.  status = close(fd);
  26.  if (status != 0) { perror("unable to close " DEVICE); exit(1); }

  27.  return 0;
  28. }

除了能够对音量进行调节之外,通过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相关的信息:


点击(此处)折叠或打开

  1. /* * 代码清单8: subchannel.c */
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <stdio.h>
  5. #include <fcntl.h>
  6. #include <sys/ioctl.h>
  7. #include <linux/cdrom.h>

  8. /* CD驱动器对应的设备文件 */
  9. #define DEVICE "/dev/cdrom"

  10. /* 返回CD当前状态的字符串 */
  11. char* audiostatus(int status)
  12. {
  13.  switch(status) {
  14.  case CDROM_AUDIO_INVALID:
  15.              return "Invalid";
  16.  case CDROM_AUDIO_PLAY:
  17.              return "Playing";
  18.  case CDROM_AUDIO_PAUSED:
  19.              return "Paused";
  20.  case CDROM_AUDIO_COMPLETED:
  21.              return "Completed";
  22.  case CDROM_AUDIO_ERROR:
  23.              return "Error";
  24.  case CDROM_AUDIO_NO_STATUS:
  25.              return "Stopped";
  26.  defautl:
  27.              return "Unknown";
  28.  }
  29. }

  30. int main()
  31. {
  32.  int fd;
  33.  int status;
  34.  struct cdrom_subchnl channel;

  35.  /* 打开设备 */
  36.  fd = open(DEVICE, O_RDONLY);
  37.  if (fd < 0) { perror("unable to open " DEVICE); exit(1); }

  38.  while(1) {
  39.      /* 读取子通道数据 */
  40.      channel.cdsc_format = CDROM_MSF;
  41.      /* 采用MSF格式地址 */
  42.      status = ioctl(fd, CDROMSUBCHNL, &channel);
  43.      if (status != 0) { perror("CDROMSUBCHNL ioctl failed"); exit(1); }
  44.      /* 输出CD的当前状态 */
  45.      printf("Status: %s \n", audiostatus(channel.cdsc_audiostatus));
  46.      printf("Track: %d \n", channel.cdsc_trk);
  47.      printf("Postion: %02d:%02d:%02d(%02d:%02d:%02d) \n\n", channel.cdsc_reladdr.msf.minute, channel.cdsc_reladdr.msf.second, channel.cdsc_reladdr.msf.frame, channel.cdsc_absaddr.msf.minute, channel.cdsc_absaddr.msf.second, channel.cdsc_absaddr.msf.frame);
  48.      fflush(stdout);
  49.      usleep(100000);
  50.  }
  51.      /* 关闭设备 */
  52.      status = close(fd);
  53.      if (status != 0) { perror("unable to close " DEVICE); exit(1); }
  54.     
  55.      return 0;
  56. }




小结

为Linux编写CD播放器的关键是如何充分利用驱动程序提供的各种功能,由于长久以来缺乏相应的标准,因此并不是所有CD驱动程序提供的功能接口都是一致的,这给应用程序的编写造成了一定的困难。David van Leeuwen正在试图对Linux下所有的CD驱动程序进行标准化,他希望通过提供一个与硬件无关的软件抽象层,来为应用程序提供一致的编程接口。

上一篇:GStreamer框架
下一篇:视频处理基础