『楼 主』:
 [转贴]DOS下DSP播音的编程
 
使用 LLM 解释/回答一下
  
DOS下DSP播音的编程 
 
摘要 该文介绍了DSP编程的基本原则和方法,并给出程序实例帮助理解,读者可以此为基础来拓展、生成自己的实用程序。 
在DOS下编程,将声音转化为数据记录下来,或将数据转化为声音,通过声卡上配置的喇叭回放出来,是一项很有实用价值和开发魅力的技术。时下流行的声卡,如Sound Blaster Pro及其兼容卡,都配有数字声音处理器DSP芯片(Digital Sound Processor),专门用于对声音进行数字记录及回放,是声音数字处理的基础硬件。而WAV文件、VOC文件等,则都是这些数据记载的具体形式。Creative公司为了方便用户,提供了一组CT-Voice驱动程序,专门针对VOC文件,作为开发利用DSP功能的软接口,使用比较方便。但是,也造成了某些限制。对于开发者而言,直接对DSP硬件编程,实现其功能,也许是更有吸引力的。 
声音,无论是从扬声器输出的,还是从话筒输入的,都是模拟量。 
而数据,无论是内存里操作的,还是磁盘上存储的都是数字量。因此,微机处理声音,大多离不开ADC与DAC两种转换。由于声音数据的数据量比较大,在声音的数字处理中,除直接由CPU进行传输外,批量数据常采用DMA方式传输,以节省较多的CPU时间。 
总括起来,ADC与DAC两种转换方式,直接传输和DMA传输这两种传输方式,再加上不同的压缩方式,如喇叭控制、静寂等等,所有这些的不同组合,就构成了DSP的各种功能。根据DSP的硬件原理,其各种功能都规定了一定的操作步骤。 
 
一、DSP编程要点 
在DSP编程中,主要注意命令与端口两个层次的操作。 
 
1.DSP命令。DSP的功能一般以一个操作码(称作命令号)的写操作为中心,按规定的步骤,配合若干必要的辅助操作,构成一串操作的组合,称为DSP命令。如8位直接播放功能命令号为10h,8位直接录音功能命令号为20h,喇叭的通断功能命令号分别为d1h与d3h等等。 
 
2.端口操作。DSP命令主要靠端口操作来实现。端口操作包括DSP初始化、写DSP命令(即发DSP命令)、读DSP状态参数、DSP中断等。所涉及的端口地址及相应的用途如表1。 
表1 DSP端口及用途 
端口地址由基址2x0h加6、0ah、0ch、0eh等形成,其中,x可取值1、2、3、4、5、6等,具体情况随硬件设置而定,多数卡在出厂被默认设置为2,即基址为220h。通过跳线,可改变此值,避免与其它设备口地址冲突。 
 
二、编程实例 
DSP的功能是比较丰富的,限于篇幅,本文只简要介绍其中的8位直接播放功能,由此举一反三,其它功能的用法不难得知。各功能的规定操作可参考文献1和2。 
 
1.命令操作步骤。8位直接播放功能的操作步骤如下: 
·写命令号10h; 
·写数据字节(即播放声音的8位数据); 
·按采样率所需时间周期延时。 
以此三步操作为循环体,进行n次循环,即完成播放。其中,n为声音数据字节数。 
 
2.2xch端口写操作。在DSP编程中,无论是发送命令,还是发送数据,都是通过写端口2xch来完成的。在写端口2xch之前,应先读此端口,直到所得值的bit7为0,这才表明此端口处于可写状态,才能进行写操作。此过程的c语言形式如下: 
while (inportb(0x22c)&0x80); 
outportb(0x22c,byte); 
这里假定端口基址为220h。句中byte可以是命令号,也可以是数据。 
 
3.定时器。为使播放按一定的采样率进行,需对数据发送进行定时控制。这一般是借用主机定时中断int8,将其调用频率提高到与采样率相当的程度,利用其监视、控制数据发送的时间,来满足播音频率的要求。关于定时中断的编程技术已有过许多介绍,限于篇幅,不再赘述,读读文后的程序清单,即一目了然。应该说明的是,对于CPU较慢的机型如386,由于计时代码本身的执行时间可能已经超过采样率对应的时间周期,定时控制就达不到预期的效果。这种情况下,用一个空循环来定时,调整循环次数,即可满足频率要求。此法的缺点是定时精度差,参数因CPU速度而异。所幸的是,目前多数配置多媒体的PC机,其CPU都在486以上。 
 
4.内存利用。人耳可辨声音的最高频率可达20kHz以上,因此DSP的采样率至少也要达到与此相当的水平,而为了容纳立体声双声道信息,采样率还要再翻一倍。常见的WAV声音的采样率有44100、22050、11025等。在这么高的采样率下,声音的数据量自然很大,如44k采样率下,20秒的录音数据长达800多k。为在DOS常规内存内处理这种规模的数据,实例程序采取了分块处理的方式,将数据分成以当前剩余自由内存大小为单位的块,将其逐次读入,逐次处理。同时,由于C语言的read()函数每次读操作的字节数最多不过64k-1,因此,每一个分块又需分 
若干次读入。实例表明,经此法处理的播放程序不受WAV文件长度的限制,笔者在Windows下录制的长达5M多的WAV文件(11k采样率,约8分钟)也照播不误。 
 
5.声音文件。本文提供的程序实例其声音数据取自WAV文件,其实,对于VOC文件,本播放技术也一样适用,只不过数据的读取格式有所不同而已。关于WAV文件的格式,可参考文献3,VOC文件的格式参考献1和2。 
实例程序用Borland C++ 3.1编译,在配置OPTI 386主板、海洋48 
6主板及多种与SoundBlaster Pro兼容声卡的兼容机上运行通过。 
 
三、源程序清单 
#include<io.h> 
#include<dos.h> 
#include<conio.h> 
#include<stdio.h> 
#include<fcntl.h> 
#include<stdlib.h> 
#include<string.h> 
#include<alloc.h> 
#include"timer.h" 
#define n1 20 
#define n2 100 
struct WavHead 
{ 
char riff[4]; 
long size0; 
char wavefmt[8]; 
long size1; 
int fmttag; 
int channel; 
long samplespersec; 
long bytespersec; 
int blockalign; 
int bitspersample; 
char flg[4]; 
}whead; 
unsigned Port=0x210; 
char Found=0; 
unsigned cnt1,cnt2; 
void PortReset(); 
void outwave(un 
 
signed char huge *,long); 
void WritePortC(unsigned char); 
void errexit(char *); 
void main() 
{ 
int fp; 
unsigned n,r,nn,i,j; 
char name[32]; 
long fermem,rr,datasize; 
unsigned char huge *data,huge *p; 
if(argc<2)errexit("miss file name\n"); 
strcpy(name,argv[1]);strcat(name,".wav"); 
fp=-open(name,0-RDONLY);if(fp=-1)errexit("Error open fil 
e\n"); 
-read(fp,&whead,sizeof(WavHead)); 
if(whead.blockalign=1 && strncmp(whead.flg,"data",4)==0) 
{ 
-read(fp,&datasize,4);//单声道WAV数据 
} 
else if(whead.blockalign=2 && strncmp(whead.flg,"fact" 
,4)==0) 
{ 
lseek(fp,12l,1); 
-read(fp,&datasize,4);//双声道WAV数据 
} 
else errexit("Error file struct\n"); 
farmem=farcoreleft(); 
PortReset();//初始化DSP端口 
Counter=0;//开始计时 
SetTimer(NewTimer,44100);//调整时间中断频率 
WritePortC(0xd1);//接通喇叭 
if(farmem≥datasize)//数据量不超过内存容量 
{ 
p=data=(unsigned char huge *)farmalloc(datasize); 
n=datasize/32768;r=datasize%32768; 
for(i=0;i<n;i++,p+=32768) -read(fp,p,32768); 
-read(fp,p,r); 
outwave(data,datasize); 
} 
else//数据量超过内存容量 
{ 
nn=datasize/farmem;//分块操作的块数 
rr=datasize%farmem;//最后一块的大小 
n=farmem/32768;//每块read次数 
r=farmem%32768;//read余零尾数 
data=(unsigned char huge *)farmalloc(farmem); 
for(i=0;i<nn;i++)//逐块处理 
{ 
p=data; 
for(j=0;j<n;j++,p+=32768)-read(fp,p,32768); 
-read(fp,p,r); 
//读入内存 
outwave(data,farmem);//发送声音数据 
} 
p=data; 
n=rr/32768;r=rr%32768;//最后块的操作 
for(i=0;i<n;i++,p+=32768)-read(fp,p,32768); 
-read(fp,p,r); 
//读入 
outwave(data,rr);//发送 
} 
WritePortC(0xd3);//断开喇叭 
RestoreTimer();//恢复时间中断 
farfree(data); 
-close(fp); 
} 
void PortReset()//初始化DSP端口 
{ 
cnt1=n1; 
while(Port≤0x260)&&!Found) 
{//测端口基址 
outportb(Port+6,1); 
outportb(Port+6,0); 
cnt2=n2; 
while(cnt2>2 && inportb(Port+0xe)<128)--cnt2; 
if(cnt2=0inportb(Port+0xa)!=oxaa) 
{ 
--cnt1; 
if(cnt1==0) 
{ 
cnt1=n1; 
Port=Port+0x10; 
} 
} 
else Found=1;//找到基址 
} 
if(!Found)errexit("Reset failed\n");//找不到基址 
} 
void outwave(unsigned char huge *p,long len) 
{//发送声音数据 
long i; 
int smpl; 
smpl=44100/whead.samplespersec/whead.blockalign; 
//采样周期系数 
for(i=0;i<len;i++) 
{ 
WritePortC(0x10);//发送命令 
WritePortC(p[i]);//发送数据 
while(Counter<smpl);Counter=0;//定时 
} 
} 
void WritePortC(unsigned char v) 
{ 
while(inportb(Port+0xc)&0x80);//等待写有效状态 
outportb(Port+0xc,v);//写端口(发送) 
} 
void errexit(char *msg) 
{ 
-AX=3; 
asm int 10h 
printf(msg); 
exit(0); 
} 
//Timer.h 
#includ<dos.h> 
#define OldTimerInt 0x60 
unsigned long Counter; 
unsigned CounterInt8,fpI8; 
void SetTimer(void interrupt(*Rout)(…),unsigned freq) 
{//设置新频率的定时中断 
int ICnt; 
fpI8=(freq+9)/18;//新旧频率的倍数 
asm cli 
ICnt=1193180/freq; 
outportb(0x43,0x36); 
outportb(0x40,ICnt & 255); 
outportb(0x40,ICnt》8); 
setvect(OldTimerInt,getvect( 
 
8));//保存旧定时中断 
setvect(8,rout);//置新的定时中断 
sam sti; 
} 
void RestoreTimer() 
{ 
asm cli 
outportb(0x43,0x36); 
outportb(0x40,0); 
outportb(0x40,0); 
setvect(8,getvect(OldTimerInt));//恢复原定时中断 
asm sti 
} 
void interrupt NewTimer(…) 
{//新定时中断 
REGPACK R; 
Counter++;//给应用程序提供新频率的计数 
if(--CounterInt8=0) 
{ 
intr(OldTimerInt,&R);//按原频率走动时钟 
CounterInt8=fpI8;//用新旧频率的倍数分频 
} 
else outportb(0x20,0x20);//退出中断 
}  
 
参考文献 
1 阎小兵等.多媒体开发工具.北京:电子工业出版社,1994. 
2 Josha Munnik等著,敬万钧等译.声霸--原理与应用.北京:电子工业出版社,1995. 
3 石宁等.在DOS下使用Windows *.WAV文件.计算机世界月刊,1995(3)44-46. 
 
    
 
  
 |