oss编程

一、概述

1.声卡

声卡有三个基本功能:一是音乐合成发音功能;二是混音器(Mixer)功能和数字声音效果处理器(DSP)功能;三是模拟声音信号的输入和输出功能。

混音器的作用是将来自音乐合成器、CD-ROM、话筒输入(MIC)等不同来源的声音组合在一起再输出,混音器是每种声音卡都有的。

模拟声音输入输出功能 主要是A/D、D/A转换。

2.MP3格式

MP3其中的VBR,ABR,CBR及相关解释 VBR(Variable Bitrate)动态比特率。也就是没有固定的比特率,压缩软件在压缩时根据音频数据即时确定使用什么

比特率。将一首歌的复 杂部分用高Bitrate编码,简单部分用低Bitrate编码。 

ABR(Average Bitrate)平均比特率,是VBR的一种插值参数。Lame针对CBR不佳的文件体积比和VBR生成文件大小不定的特点独创了这种编码模式。

ABR也 被称为“Safe VBR”,它是在指定的平均Bitrate内,以每50帧(30帧约1秒)为一段,低频和不敏感频率使用相对低的流量,高频和大动态表现时使用高

流量。举 例来说,当指定用192kbps ABR对一段wav文件进行编码时,Lame会将该文件的85%用192kbps固定编码,然后对剩余15%进行动态优化:复杂部

分用高于192kbps 来编码、简单部分用低于192kbps来编码。与192kbps CBR相比,192kbps ABR在文件大小上相差不多,音质却提高不少。ABR编码在速度

上是VBR编码的2到3倍,在128-256kbps范围内质量要好于CBR。可以做为 VBR和CBR的一种折衷选择。

CBR(Constant Bitrate),常数比特率,指文件从头到尾都是一种位速率。相对于VBR和ABR来讲,它压缩出来的文件体积很大,但音质却不会有明显的提高。

 对MP3来说Bitrate是最重要的因素,它用来表示每秒钟的音频数据占用了多少个bit(bit per second,简称bps)。这个值越高,音质就越好。

当音乐CD的数据被读到电脑后,他的存储将成为一大难题,我们可以来算一下,44100Hz采样率16bit位率的立体声数据数据一秒钟有多少?

44100Hz * (16 / 8)Byte * 2Channel = 176400字节,也就是0.17M,一分种就是10.5M,一般CD可以存70多分钟的音乐,那么一张CD算下来就是700多M,

对于一般百余G的硬盘空间,这还是比较难以承受的,所以很多年以前有人就提出了有损压缩的办法,采用了类似于快速傅利叶变换的算法,对数据进行处理,可以

把WAVE压缩到很小,一秒钟100-300K,这样就很好的解决了存储的问题,但这是以牺牲了一部分音质为代价的。

二.DSP(数字音频设备)

数字音频系统通过将声波的波型转换成一系列二进制数据,来实现对原始声音的重现,实现这一步骤的设备常被称为模/数转换器(A/D)。A/D转换器

以每秒钟上万次的速率对声波进行采样,每个采样点都记录下了原始模拟声波在某一时刻的状态,通常称之为样本(sample),而每一秒钟所采样的数

目则称为采样频率,通过将一串连续的样本连接起来,就可以在计算机中描述一段声音了。

数字声音效果处理器是对数字化的声音信号进行处理以获得所需要的音响效果(混响、延时、合唱等).

用于表示声卡性能的两个参数是采样率和模拟量转换成数字量之后的数据位数(简称量化位数)。采样率决定了频率响应范围,对声音进行采样的三种标准

以及采样频率分别为:语音效果(11 kHz)、音乐效果(22 kHz)、高保真效果(44.1 kHz),目前声卡的最高采样率为44.1KHz。

量化位数决定了音乐的动态范围,量化位数有8位和16位两种。8位声卡的声音从最低音到最高音只有256个级别,16位声卡有65536个高低音级别。

声道数 :反映音频数字化质量的另一个重要因素,双声道又称为立体声,在硬件中有两条线路,音质和音色都要优於单声道。

 位速说明 Kbps 表示 “每秒千字节数”,因此数值越大表示数据越多。如果您想把制作的 VCD 放在 DVD 播放器上播放,那么视频必须是 1150 Kbps,

音频必须是 224 Kbps。

DSP:数字音频设备(有时也称codec,PCM,ADC/DAC设备),播放或录制数字化的声音。它的指标主要有:采样速率(电话为8K,DVD为96K)、channel

数目(单声道,立体声)、采样分辨率(8-bit,16-bit)。对该设备操作时应注意一次写入数据块的大小,如果数据块过大会引起设备的block操作。 

/dev/dsp与/dev/audio之间的区别在于采样的编码不同,/dev/audio使用μ律编码,/dev/dsp使用8-bit(无符号)线性编码,/dev/dspW使用16-bit(有符号)线

形编码。/dev/audio主要是为了与SunOS兼容,所以尽量不要使用。

在从DSP设备读取数据时,从声卡输入的模拟信号经过A/D转换器变成数字采样后的样本(sample),保存在声卡驱动程序的内核缓冲区中,当应用程序通过read

系统调用从声卡读取数据时,内核缓冲区内容被复制用户缓冲区中。

声卡采样频率是由内核中的驱动程序所决定的。如果应用程序读取数据的速度过慢,以致低于声卡的采样频率,那么多余的数据将会被丢弃;如果读取数据的速度过

快,以致高于声卡的采样频率,那么声卡驱动程序将会阻塞那些请求数据的应用程序,直到新的数据到来为止。

在向DSP设备写入数据时,数字信号会经过D/A转换器变成模拟信号,然后产生出声音。应用程序写入数据的速度同样应该与声卡的采样频率相匹配,否则过慢的话

会产生声音暂停或者停顿的现象,过快的话又会被内核中的声卡驱动程序阻塞,直到硬件有能力处理新的数据为止。声卡通常不会支持非阻塞的I/O操作。

无论是从声卡读取数据,或是向声卡写入数据,事实上都具有特定的格式(format),默认为8位无符号数据、单声道、8KHz采样率,如果默认值无法达到要求,

可以通过ioctl系统调用来改变它们。

1. 录音

int len = read(audio_fd, audio_buffer, count);

count为录音数据的字节个数(建议为2的指数),但不能超过audio_buffer的大小。从读字节的个数可以精确的测量时间,例如

8kHZ 16-bit stereo的速率为8000*2*2=32000bytes/second,这是知道何时停止录音的唯一方法。

注意:用户始终要读/写一个完整的采样。例如一个16-bit的立体声模式下,每个采样有4个字节,所以应用程序每次必须读/写4的倍数个字节。

还要注意读/写时的字节顺序。

2. 设置参数

设置采样格式

int format;

format = AFMT_S16_LE;

ioctl(audio_fd, SNDCTL_DSP_SETFMT, &format) ;

在设置采样格式之前,可以先测试设备能够支持那些采样格式,方法如下:

int mask;

ioctl(audio_fd, SNDCTL_DSP_GETFMTS, &mask) ;

if (mask & AFMT_MPEG) {

/* 本设备支持MPEG采样格式 ... */}

/*设置采样时的量化位数*/

arg = SIZE;

status = ioctl(fd, SOUND_PCM_WRITE_BITS, &arg);

/* 在继续录音前等待回放结束 */

status = ioctl(fd, SOUND_PCM_SYNC, 0); 

设置声卡工作时的声道(channel)数目

int channels = 0; // 0=mono 1=stereo

int result = ioctl(handle, SNDCTL_DSP_STEREO, &channels);

采样格式和采样频率是在进行音频编程时需要考虑的另一个问题,声卡支持的所有采样格式可以在头文件soundcard.h中找到.

int format = AFMT_U8;

int result = ioctl(handle, SNDCTL_DSP_SETFMT, &format);

声卡采样频率的设置。下面的代码示范了如何设置声卡的采样频率:

int rate = 22050;

int result = ioctl(handle, SNDCTL_DSP_SPEED, &rate);

44100hz

Sample rate of standard Red Book audio CDs.

88000hz

Sample rate of SACD high definition audio discs/downloads. It is rare that your motherboard will support this sample rate.

96000hz

Sample rate of most high definition audio downloads. If your motherboard is an AC'97 motherboard, this is likely to be your highest bitrate.

192000hz

Sample rate of BluRay, and some (very few) high definition downloads. Support for external audio receiver equipment is limited to high end 

audio. Not all motherboards support this. An example of a motherboard chipset that would support this includes HD Audio. 

设置通道数目

int channels = 2; /* 1=mono, 2=stereo */

ioctl(audio_fd, SNDCTL_DSP_CHANNELS, &channels) ;

音频设备通过分频的方法产生需要的采样时钟,因此不可能产生所有的频率。

/* 用于保存数字音频数据的内存缓冲区 */

unsigned char buf[LENGTH*RATE*SIZE*CHANNELS/8];

Telephone Quality8 KHz or 11.025 KHz

Radio Quality22.05 KHz

CD Quality44.1KHz 

DVD Quality98KHz

暂停

用两个bits去控制是否暂停,一个bit控制录音,一个bit控制拨音。bit1表正常录拨音,bit0表暂停录拨音。

PCM_ENABLE_OUTPUT

PCM_ENABLE_INPUT

int enable_bits = ~ PCM_ENABLE_OUTPUT;

ioctl(audio_fd, SNDCTL_DSP_SETTRIGGER, & enable_bits);

3.声卡缓冲区

声卡驱动专门维护了一个缓冲区,其大小会影响到放音和录音时的效果。调节驱动程序中缓冲区大小的操作不是必须的,如果没有特殊的要求,一般采用默认的缓冲区

大小也就可以了。但需要注意的是,缓冲区大小的设置通常应紧跟在设备文件

打开之后,这是因为对声卡的其它操作有可能会导致驱动程序无法再修改其缓冲区的大小。

int result = ioctl(handle, SNDCTL_DSP_SETFRAGMENT, &(int)0xx*);

在设置缓冲区大小时,参数setting实际上由两部分组成,其低16位标明缓冲区的尺寸,相应的计算公式为buffer_size = 2^ssss,即若参数setting低16

位的值为16,那么相应的缓冲区的大小会被设置为65536字节。参数setting的高16位则用来标明分片(fragment)的最大序号,它的取值范围从2一直

到0x7FFF,其中0x7FFF表示没有任何限制。

OSS却是采用multi-buffering,因为考虑real time performance一般情况下,一开始程式会等到一个buffer都满了之后才开始拨音,如果buffer size太大,

才会有一段时间的延迟,对于real time的程式来说是不可行的,因此OSSbuffer分成一堆小的fragment,而程式开始时只须等两个fragment满了就开

始拨音。一般的情况下我们并不需要去考虑fragment size的大小,OSS会计算出程式的data rate自动帮我们调整bufferfragment的大小。

Multi-buffering

audio CD quality : 172 KB/sec  à small buffer is likely to fail à

increase buffer size à latency increase à OSS solution : fragment the buffer (multi-buffering)

Small  fragment size  improves real-time performance.

Determine Buffering Parameters

Get Fragment Size:

int frag_size;

ioctl(audio_fd, SNDCTL_DSP_GETBLKSIZE, &frag_size)

Set Fragment Size:

int arg = 0xMMMMSSSS;

ioctl(audio_fd, SNDCTL_DSP_SETFRAGMENT, &arg)

fragment size = 2 ^SSSS  bytes ( if SSSS = 0008, fragment size = 2 ^8 = 256 )

fragment numbers = 0x MMMM

Ø Synchronization Issues

Get Played or recorded data bytes from the beginning of opening the device file:

count_info info;

ioctl(audio_fd, SNDCTL_DSP_GETIPTR, &info);

ioctl(audio_fd, SNDCTL_DSP_GETOPTR, &info);

struct count_info {

int bytes;    //number of bytes processed since opening the device

int blocks;  // number of fragment processed since previous call to ioctl

int ptr;   // offset of current play/record position from beginning of buffer

};

played_time=info.bytes / bytes_per_sec

now_frame = played_time * frame_per_sec

使用SNDCTL_DSP_GETOPTR / SNDCTL_DSP_GETIPTR的缺点是不够精确,例如如果录音时data  rate很快,而I/O不能及时将buffer的资料送给程

式,则会有一部份的data被覆盖(overrun),利用SNDCTL_DSP_GETIPTR所得到的byte所算出的时间则并不正确。而且错误会不断的累积,算出的时

间会越来越不准。

4.缓冲区设置的性能分析

驱动程序采用多缓冲(Multi-buffering)的方式,即将大的DMA buffer分割成多个小的缓冲区,称之为fragment,它们的大小相同。驱动程序开始时只需等待两个fragment满了就开始播放。这样

可以通过增加fragment的个数来增加缓冲区的大小,但同时每个fragment被限制在合适的大小,也不影响时延。音频驱动程序中的多缓冲机制一般会利用底层DMA控制器的scatter-gather功能。

在OSS的ioctl接口中,SNDCTL_DSP_SETFRAGMENT就是用来设置驱动程序内部缓冲区大小。

int param = ( 0x0004 << 16) + 0x000a;

ioctl(audio_fd, SNDCTL_DSP_SETFRAGMENT, ) ;

参数param由两部分组成:低16位为fragment的大小,此处0x000a表示fragment大小为2^0xa,即1024字节;高16 位为fragment的数量,此处为0x0004,即4个fragement。设置好fragment参数后

,通过ioctl的 SNDCTL_DSP_SETFRAGMENT命令调整驱动程序中的缓冲区。

第一种情况的缓冲区很小,每个fragment只有512字节,总共的缓冲区大小为2 x 512 = 1024字节。1024字节只能播放5.8ms。不推荐使用fragment大小小于256字节的设置。从测试结果中看到

,不管使用那种系统负载,

第二种情况的缓冲区要大得多,总共的缓冲区大小为4 x 2048 = 8192字节。8192字节可以播放0.046秒。从测试的图形来看,结果比较理想,即使在系统负载较重的情况,仍然能够基本保证

播放时延的要求,而且没有出现一次欠载的现象。

并不是说缓冲区越大越好,如果继续选择更大的缓冲区,将会产生比较大的时延,对于实时性要求比较高的音频流来说,是不能接受的。在一般情况下,驱动程序会根据硬件的情况,选择

一个缺省的缓冲区配置,播放程序通常不需要修改驱动程序的缓冲区配置,而可以获得较好的播放效果。

5.非阻塞写(non-blocking write)

如果播放程序写入的速度超过了DAC的播放速度,DMA buffer就会充满了音频数据。应用程序调用write时就会因为没有空闲的DMA buffer而被阻塞,直到DMA buffer出现空闲为止。此时,

从某种程度来说,应用程序的推进速度依赖于播放的速度,不同的播放速度就会产生不同的推进速度。有时我们不希望 write被阻塞,这就需要我们能够知道DMA buffer的使用情况。

audio_buf_info info

;/* Ask OSS if there is any free space in the buffer. */

(ioctl(dsp,SNDCTL_DSP_GETOSPACE,&info);

/* Any empty fragments? */

if (info.fragments > 0) break;

/* Not enough free space in the buffer. Waste time. */

usleep(100);

以上的代码不停的查询驱动程序中是否有空的fragment(SNDCTL_DSP_GETOSPACE),如果没有,则进入睡眠(usleep(100))。如果有空闲的fragment(info.fragments > 0),则退出循环,接

着就可以进行非阻塞的write了。

6.DMA buffer的直接访问(mmap)

将音频数据输出到音频设备通常使用系统调用write,但是这会带来性能上的损失,因为要进行一次从用户空间到内核空间的缓冲区拷贝。这时,可以考虑利用mmap系统调用,获得直接访问

DMA buffer的能力。DMA控制器不停的扫描DMA buffer,将数据发送到DAC。这有点类似于显卡对显存的操作,大家都知道,GUI可以通过mmap将framebuffer(显存)映射到自己的地址空间,

然后直接操纵显存。这里的DMA buffer就是声卡的framebuffer。

在使用mmap之前,要查看驱动程序是否支持这种模式(SNDCTL_DSP_GETCAPS)。使用SNDCTL_DSP_GETOSPACE得知驱动选择的framgment大小和个数,就可以计算出全部DMA buffer的

大小dmabuffer_size。

mmap将dmabuffer_size大小的DMA buffer映射到调用进程的地址空间,DMA buffer在应用进程的起始地址为dmabuffer。以后就可以直接使用指针dmabuffer访问DMA buffer了。这里需要对mmap

中的参数做些解释。

音频驱动程序针对播放和录音分别有各自的缓冲区,mmap不能同时映射这两组缓冲,具体选择映射哪个缓冲取决于mmap的prot参数。 PROT_READ选择输入(录音)缓冲,PROT_WRITE选

择输出(播放)缓冲,代码中使用了PROT_WRITE|PROT_READ,也是选择输出缓冲。(这是BSD系统的要求,如果只有PROT_WRITE,那么每次对缓冲的访问都会出现segmentation/bus error)。

一旦DMA buffer被mmap后,就不能再通过read/write接口来控制驱动程序了。只能通过SNDCTL_DSP_SETTRIGGER打开DAC的使能位,当然,先要关闭使能位。

DMA一旦启动后,就会周而复始的扫描DMA buffer。当然我们总是希望提前为DMA准备好新的数据,使得DMA的播放始终连续。因此,PlayerDMA函数将mmap后的DMA buffer分割成前后两块

,中间设置一个界限。当DMA扫描前面一块时,就填充后面一块。一旦DMA越过了界限,就去填充前面一块。

使用mmap的问题是,不是所有的声卡驱动程序都支持mmap方式。因此,在出现不兼容的情况下,应用程序要能够转而去使用传统的方式。

audio_mmap()是实现mmap接口的函数,它首先根据mmap调用的prot参数(vma->vm_flags),选择合适的缓冲(输入还是输出);vma->vm_end - vma->vm_start为需要映射到应用进程地址空

间的大小,必须和DMA buffer的大小(s->fragsize * s->nbfrags)一致;如果DMA buffer还没有建立,则调用audio_setup_buf(s)建立;接着对所有的fragment,从映射起始地址开始(vma->vm_start)

,建立实际物理地址与映射的虚拟地址之间的对应关系(remap_page_range)。最后设置mmap标志(s->mapped = 1)。

三、Mixer编程

混音器电路通常由两个部分组成:input mixer和ouput mixer.

输入混音器负责从多个不同的信号源接收模拟信号,这些信号源有时也被称为混音通道或者混音设备。模拟信号通过增益控制器和由软件控制的音量调节器后,在不同的混音通道中进

行级别(level)调制,然后被送到输入混音器中进行声音的合成。混音器上的电子开关可以控制哪些通道中有信号与混音器相连,有些声卡只允许连接一个混音通道作为录音的音源,

而有些声卡则允许对混音通道做任意的连接。经过输入混音器处理后的信号仍然为模拟信号,它们将被送到A/D转换器进行数字化处理。 

 用来控制多个输入、输出的音量,也控制输入(microphone,line-in,CD)之间的切换。对Mixer的控制,包括调节音量(volume)、选择录音音源(microphone,line-in)、

查询mixer的功能和状态,选择mixer的录音通道。主要是对声卡进行设置,比如设置speaker、mic和midi的音量等等。这些设置主要是通过ioctl进行,比如可以用

SNDCTL_DSP_SETPLAYVOL设置speaker的音量,用SNDCTL_DSP_SETRECVOL设置mic的音量,用SNDCTL_DSP_SET_PLAYTGT选择输出的speaker(比如机身的speaker或者earphone)。

SOUND_MIXER_VOLUME 主音量调节

SOUND_MIXER_BASS 低音控制

SOUND_MIXER_TREBLE 高音控制

SOUND_MIXER_SYNTH FM合成器

SOUND_MIXER_PCM 主D/A转换器

SOUND_MIXER_SPEAKER PC喇叭

SOUND_MIXER_LINE 音频线输入

SOUND_MIXER_MIC 麦克风输入

SOUND_MIXER_CD CD输入

SOUND_MIXER_IMIX 回放音量

SOUND_MIXER_ALTPCM 从D/A 转换器

SOUND_MIXER_RECLEV 录音音量

SOUND_MIXER_IGAIN 输入增益

SOUND_MIXER_OGAIN 输出增益

SOUND_MIXER_LINE1 声卡的第1输入

SOUND_MIXER_LINE2 声卡的第2输入

SOUND_MIXER_LINE3 声卡的第3输入

soundcard.h中已定义的channel数量。

1.调节音量

对声卡的输入增益和输出增益进行调节是混音器的一个主要作用,大部分声卡采用的是8位或者16位的增益控制器。

应用程序通过ioctl的SOUND_MIXER_READ和SOUND_MIXER_WIRTE功能号来读取/设置音量。在OSS中,音量的大小范围在0-100之间。例如在获取麦克风的输入增益时使用方法如下:

int vol;

if (ioctl(mixer_fd, SOUND_MIXER_READ(SOUND_MIXER_MIC), &vol) == -1) {

/* 访问了没有定义的mixer通道... */

SOUND_MIXER_MIC是通道参数,表示读microphone通道的音量,结果放置在vol中。如果通道是立体声,那么vol的最低有效字节为左声道的音量值,接着的字节为右声道的音量

值,另外的两个字节不用。如果通道是单声道,vol中左声道与右声道具有相同的值。

对于只有一个混音通道的单声道设备来说,返回的增益大小保存在低位字节中。而对于支持多个混音通道的双声道设备来说,返回的增益大小实际上包括两个部分,分别代表左、右两

个声道的值,其中低位字节保存左声道的音量,而高位字节则保存右声道的音量。下面的代码可以从返回值中依次提取左右声道的增益大小:

int left, right;

left = vol & 0xff;

right = (vol & 0xff00) >> 8;

如想设置混音通道的增益大小,则可以通过SOUND_MIXER_WRITE宏来实现下面的语句可以用来设置麦克风的输入增益:

vol = (right << 8) + left;

ioctl(fd, SOUND_MIXER_WRITE(SOUND_MIXER_MIC), &vol);

2.查询mixer的能力

int mask;

if (ioctl(mixer_fd, SOUND_MIXER_READ_xxxx, &mask) == -1) {

/* Mixer 的没有此能力... */

}

SOUND_MIXER_READ_xxxx 中的xxxx代表具体要查询的内容,比如检查可用的mixer通道用SOUND_MIXER_READ_DEVMASK;

检查可用的录音设备,用SOUND_MIXER_READ_RECMASK;检查单声道/立体声,用SOUND_MIXER_READ_STEREODEVS;

检查mixer的一般能力,用SOUND_MIXER_READ_CAPS等等。

所有通道的查询的结果都放在mask中,所以要区分出特定通道的状况,使用mask& (1 << channel_no)。

3.选择mixer的录音通道

首先可以通过SOUND_MIXER_READ_RECMASK检查可用的录音通道,然后通过SOUND_MIXER_WRITE_RECSRC选择录音通道。可以随时通过SOUND_MIXER_READ_RECSRC查询当前声

卡中已经被选择的录音通道。在使用mixer之前,首先通过API的查询功能检查声卡的能力。

声卡驱动程序提供了多个ioctl系统调用来获得混音器的信息,它们通常返回一个整型的位掩码(bitmask),其中每一位分别代表一个特定的混音通道,如果相应的位为1,则说明与之对应的混

音通道是可用的。例如通过SOUND_MIXER_READ_DEVMASK返回的位掩码,可以查询出能够被声卡支持的每一个混音通道,而通过SOUND_MIXER_READ_RECMAS返回的位掩码,则可以查

询出能够被当作录音源的每一个通道。下面的代码可以用来检查CD输入是否是一个有效的混音通道:

ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask);

如果进一步还想知道其是否是一个有效的录音源,则可以使用如下语句:

ioctl(fd, SOUND_MIXER_READ_RECMASK, &recmask);

大多数声卡提供多个录音源,通过SOUND_MIXER_READ_RECSRC可以查询出当前正在使用的录音源,同一时刻能够使用几个录音源是由声卡硬件决定的。使用SOUND_MIXER_WRITE_RECSRC

可以设置声卡当前使用的录音源,例将CD输入作为声卡的录音源使用:

devmask = SOUND_MIXER_CD;

ioctl(fd, SOUND_MIXER_WRITE_DEVMASK, &devmask);

所有的混音通道都有单声道和双声道的区别,如果需要知道哪些混音通道提供了对立体声的支持,可以通过SOUND_MIXER_READ_STEREODEVS来获得。

三.MIDI

MIDI是数字乐器接口的国际标准,它定义了电子音乐设备与计算机的通讯接口,规定了使用数字编码来描述音乐乐谱的规范。常见的MIDI设备有电子琴等。计算机中以MID为扩展名的文件称为

MIDI文件,其中存放的是对MIDI设备的命令,即每个音符的频率、音量、通道号等指示信息。最后播出的声音是由MIDI设备根据这些信息产生的。MIDI声音可以用于配音。

MIDI与声波形式的声音不同,MIDI技术不是对声波进行编码,而是把MIDI乐器上产生的每一活动编码记录下来存储在MIDI文件中,放在MIDI消息中。MIDI 传输的不是声音信号, 而是音符、控制参

数等指令, 它指示MIDI 设备要做什么,怎么做, 如演奏哪个音符、多大音量等。它们被统一表示成MIDI 消息(MIDI Message) .MIDI技术的优点是可以节省大量的存储空间,并可方便地配乐。

/dev/dmmidi:是MIDI设备(MIDI device)的原始接口,它提供直接TTY方式访问MIDI端口,主要是给一个特殊应用程序使用。(不知道它与/dev/midi的差别到底在哪里。)

MIDI接口是为了连接舞台上的synthesizer(合成器,通过一些预先定义好的波形来合成声音,有时用在游戏中声音效果的产生)、键盘、道具、灯光控制器的一种串行接口。该文件是MIDI总

线端口的比较底层的接口,它的工作方式很像一个TTY (character terminal),所有发送给它的数据立即传递到MIDI端口。

MIDI是Musical Instrument Digital Interface的缩写。说白了,MIDI只一个通信协议,用来在乐器之间进行通信的,让所有的乐器都说同一种语言。这种通信通常是通过一个高速串口来实现

的,速率为31250bps,8bits的数据,外加一个起始位和一个停止位。下面简单介绍一下这个协议的内容:

MIDI的消息由状态(Status,或者称为命令更好)和数据两部分组成,状态由一个字节表示,所有状态值的最高位都为1,即大于等于128。数据由多个字节表示,数据长度视状态而定,但所有数

据的最高位都为0,即小于128。状态的8bits,又分为两个4bits,高的4bits代表状态的类型,低四位代表通道。MIDI的状态有:

8 = Note Off

9 = Note On

A = AfterTouch (ie, key pressure)

B = Control Change

C = Program (patch) change

D = Channel Pressure

E = Pitch Wheel

F = System Exclusive

Note On: 它是一个抽象的动作,当演奏者按下钢琴的键,拉动小提琴的弦或者拨动吉它的弦,这个动作称为note on。

Note Off: 它是一个抽象的动作,当演奏者放开钢琴的键,停止拉动小提琴或者手指离开吉它的弦,这个动作称为note off。

AfterTouch: 同是按键动作,力度的差异产生效果也不一样,即使在保持按键的过程中,压力也会有变化,这由AfterTouch状态来调整。

Control Change:用来对MIDI设备进行设置,比如设置音量和立体声平衡值等等,它有128种取值(0-127)。

Program (patch) change:一个Program 一般与一种乐器对应,比如Piano、 Guitar和Trumpet,要换乐器就用这个状态。

Channel Pressure: 和AfterTouch的功能类似,但它不只影响一个note,而是影响一个通道,通常用来设置默认值。

Pitch Wheel:设置Pitch Wheel的值,好像是设置乐器的基准音调吧,不太懂。

 播放midi

unsigned char note_on[] = { 0xc0, 0, /* Program change */0x90, 60, 60}; /* Note on */

unsigned char note_off[] = { 0x80, 60, 60 }; /* Note off */

int fd = open  "/dev/midi" , O_WRONLY, 0));

write (fd, note_on, sizeof (note_on)) ;

sleep (1); /* Delay one second */

write (fd, note_off, sizeof (note_off)) ;

四.其他设备

1. /dev/sndstat

这主要是用于debug的,cat /dev/sndstat可以输出一些OSS驱动程序检测的设备信息,这些信息是给人读的而不是程序使用的。

2./dev/oss/sblive0/pcm0是第一个声卡。 

关于PCM的

PCM是Pulse code modulation的缩写,它是对波形最直接的编码方式。Sampling rate:从模拟信号到数字信号,即从连续信号到离散信号的转换都是通过离散采样完成的,Sampling rate

就是每秒种采样的个数。根据香农采样定理,要保证信号不失真,Sampling rate要大于信号最高频率的两倍。我们知道人的耳朵能听到的频率范围是20hz – 20khz,所以Sampling rate达到

40k就够了,再多了也只是浪费。但是有时为了节省带宽和存储资源,可以降低Sampling rate而损失声音的质量,所以我们常常见到小于40k采样率的声音数据。Sample size:用来量化一个

采样的幅度,一般为8 bits、16 bits和24 bits。8 bits只有早期的声卡支持,而24 bits只有专业的声卡才支持,我们用的一般都是16 bits的。Number of channels:声音通道个数,单声道为

一个,立体声为两个,还有更多的(如8个声道的7.1格式)。一般来说,每个声道都来源于一个独立的mic,所以声道多效果会更好(更真实),当然代价也更大。Frame: Frame是指包含了所

有通道的一次采样数据,比如对于16bits的双声道来说,一个frame的大小为4个字节(2 * 16)。

/dev/dsp_spdifout:Default digital audio(默认的数字信号) (S/PDIF) output device

3. /dev/sequencer

主要是给电子(MIDI)音乐应用程序使用的,也利用它来实现游戏中的音效。通过该文件可以访问声卡内部和外部(即子卡)中的声音合成(synthesizer)设备。声音合

成(synthesizer)设备的功能是把MIDI转成波型数据,一般通过调频(FM)和波表(wavetable)来实现。

4. /dev/music

该文件类似于/dev/sequencer,可以以同样的方式处理声音合成(synthesizer)设备和MIDI设备(可能是指MIDI键盘吧),而且具有更好的设备无关性,但是不能像

/dev/sequencer那可以精确的控制单个音符。

5. /dev/dmfm

该文件是调频合成器(FM synthesizers)的原始接口,通过它可以访问FM的一些底层寄存器。

五.FFMPEG

ffmpeg提取音频播放器总结;音频播放器过程如下所示:打开文件--分析文件格式--打开对应解码器--读取一音频帧--解码音频帧--音频数据写入音频设备--循环读取音频帧--

再解码。。。如此循环下去;

整个播放器实现原理详细说明为,采用ffmpeg提供的API函数先用av_open_input_file打开音频文件,分析文件得到格式信息,然后识别格式,并查找对应的解码器,再得到针对此音频文件的解码

器之后,用av_read_frame从音频文件中读取一帧,然后对其用 avcodec_decode_audio函数进行解码,在将解码之后的PCM音频数据直接写到audio设备(/dev/dsp)上,根据linux音频设备的原

理,此时你就应该听到悦耳的歌声了;1、不同音频文件格式对音频压缩率不同,导致对于同一个音频包,你解压出来的音频数据大小也是不一样的,这一点无需惊奇,但是对于这些解压出来

的音频数据,一定要保证全部写到声卡当中去,这样才能够作为你能听到悦耳歌声的基础. 有一些写音频数据的太快的原故;2、在确认了解码后的数据是完整的之后,可以将数据写入到音频

设备当中了(/dev/dsp),这里很关键的一点就是要对音频设备进行设置,否则你也听不到你想听到的声音:

(对音频设备的设置主要是四个方面,这不代表其他方面不设置哦:

设置采样率

ioctl (fd, SNDCTL_DSP_SPEED, &(pCodecCtx->sample_rate));

设置音频声道数(这个很好理解,一般都是立体声了)

// set channels;

i = pCodecCtx->channels;

ioctl (fd, SNDCTL_DSP_CHANNELS, &i);

这里需要说明的一点是,如果是立体声,则此处i应该等于2,而不是1.

设置量化位数(这个量化位数是指对声音的振幅进行采样的位数,以前一般是8位,现在以16位居多,更高位数对于普通用户用不着,只能在专业音乐中才有价值)

i = AFMT_S16_LE; 

(16位,小端存储,也即intel的倒序数据存储)

ioctl (fd, SNDCTL_DSP_SETFMT, &i);

设置音频驱动级缓存

i = (0x0004 << 16) + 0x000b; 

// four 2kb buffer;

你看着对应改就行了(这里是四个2kb缓存)

ioctl (fd, SNDCTL_DSP_SETFRAGMENT, &i);

这里也可以不设置,用系统默认自定义也可;

Q:播放音频和pts有关系么?需要他来调整播放的快慢么?就像视频那样?

A:基本没有关系,至少我目前没有用到这个咚咚,pts应该实在视频当中采用到的,pts是显示时间戳,dts是解码时间戳;对于写音频数据,系统,或者更准确的说驱动会自动调整,写得快,

他会阻塞你的写,写的慢?增加缓存可以解决慢的问题;

Q:如何调试音频播放器?

这里需要注意两点,一点是你要保证解码后的数据确实是PCM数据;

第二点是你要确定数据准确无误,全部写入音频文件;有关这两点你可以分别调试;

第一点,可以将解码后的数据写入一个文件当中,然后利用一些音频分析软件(能够分析PCM数据),播放即可,看你解码的数据是否正确,完整;这里向大家推荐windows下的cooledit软件,

不用找注册码,反正能试用,没问题,功能绝对够用,而且分析声音频播非常形象,郑重推荐.

下面将这个音频播放器的源代码贴出来,以便大家互相学习;

我的编译环境是硬件:普通pc机;

// manipulations for file//

 open file file_name and return the file descriptor;

int fd = open (file_name, mode);

// set the properties of audio device with pCodecCtx;

/* 设置适当的参数,使得声音设备工作正常 */

/* 详细情况请参考Linux关于声卡编程的文档 */

i = 0;

ioctl (fd, SNDCTL_DSP_RESET, &i);

i = 0;

ioctl (fd, SNDCTL_DSP_SYNC, &i);

i = 1;

ioctl (fd, SNDCTL_DSP_NONBLOCK, &i);

// set sample rate;

i = pCodecCtx->sample_rate;

ioctl (fd, SNDCTL_DSP_SPEED, &i);

// set channels;

i = pCodecCtx->channels;

ioctl (fd, SNDCTL_DSP_CHANNELS, &i)) ;

// set bit format;

i = AFMT_S16_LE;

ioctl (fd, SNDCTL_DSP_SETFMT, &i) ;

// set application buffer size;

i = 1;

ioctl (fd, SNDCTL_DSP_PROFILE, &i);

// set the sched priority

// 这是为了提高音频优先级;不晓得起作用没;

int policy = SCHED_FIFO;

sched_setscheduler(0, policy, NULL);

int write_buf_size = 4196;

int written_size;

while (av_read_frame (pFormatCtx, &packet) >= 0)

{

// Is this a packet from the audio stream?

// 判断是否音频帧;

if (packet.stream_index == audioStream)

{

// Decode audio frame

// 解码音频数据为pcm数据;

len = avcodec_decode_audio (pCodecCtx, (int16_t *)decompressed_audio_buf, &decompressed_audio_buf_size, // it is the decompressed frame in BYTES 解码后的数据大小,字节为单位;packet.data, packet.size );

// 重点是这一部分,使用oss播放的代码,之前的数据写是否完整的问题就是出在这里,或者是前面的set_audio函数设置不正确;

// audio_buf_info info;

p_decompressed_audio_buf = decompressed_audio_buf;while ( decompressed_audio_buf_size > 0 )

{

// 解码后数据不为零,则播放之,为零,则;

written_size = write(fd, p_decompressed_audio_buf, decompressed_audio_buf_size);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章