void rexy() /*得到光标坐标*/
{ union REGS rg;
rg.h.ah=3;
rg.h.bh=0;
int86(0x10,&rg,&rg);
y0=rg.h.dl;
x0=rg.h.dh;
}
void prt(p) /*显示字符*/
{ union REGS rg;
rg.h.al=p;
rg.h.ah=14;
int86(0x10,&rg,&rg);
}
4.5 发声技术
播放歌曲就是意味着让计算机发声,由于声音是从 PC 内的扬声器发出的,因此要首先了 解一下计算机发声原理。在 PC 的系统板上装有定时与计数器 8253 芯片,还有 8255 可编程并 行接口芯片,由它们组成的硬件电路可用来产生 PC 内扬声器的声音,对于 286、386、486、
586 等 PC,由于采用了超大规模集成电路,因而看不到这些芯片,它们均集成在外围电路芯 片上了。当操作计算机时,常常听到的发声,就是由软件控制这些电路而产生的。声音的长短 和音调的高低,均可由程序进行控制。在扬声器电路中,定时器的频率决定了扬声器发音的频 率,所以可通过设定定时器电路的频率来使扬声器发出不同的声音。对定时器电路进行频率设 定时,首先对其命令寄存器(口地址为 0x43)写命令字,如写入 0xb6,这可用 outporb(0x43,0xb6);
来实现,则表示选择该定时器的第二个通道,计数频率先送低 8 位(二进制),后送高 8 位。
接着用口地址 0x42 送频率计数值,先送低 8 位,后送高 8 位,即用 outportb(0x42,低 8 位 频率计数值)和 outportb(0x42,高 8 位频率计数值)来实现。通过这两步使定时器电路产 生一系列方波信号,此信号是否能推动扬声器发声,还要看由 8255 产生的门控信号和送数信 号是否为 1,而它们也可编程,口地址为 0x61。为了不影响 8255 口地址 61H 中的其他高位,
应 先 输 入 口 地 址 61H 的 现 有 值 bits , 即 用 bits= inportb(0x61) 来 实 现 , 然 后 就 可 用 outportb(0x61,bits|3)来允许发声,而用 outportb(0x61,bits&0xfc)来禁止发声,且不改变 8255 其他位原来的值。
4.5.1 声音函数
编写音乐程序播放歌曲,最简单的方法是可以直接使用 Turbo C 2.0 在 dos.h 中提供的有 关发声的函数 sound()和 nosound()。
void sound(unsigned frequency);
该函数用于产生声音,其入口参数为扬声器要产生声音的频率。
void nosound(void);
该函数关闭扬声器,该函数没有入口和出口参数,它只是简单地把口地址 61H 中的低 2 位清 0。
在利用函数 sound 产生指定频率的声音后,一般要过一段时间后再调用函数 nosound 关 闭扬声器,这样才能清楚地听到一个声音。如果扬声器刚打开就关闭,是很难听到一个声音的。
某个频率的声音延续时间的长短是很重要的,它将直接影响音响效果。这需要使用 Turboc 提 供的专门的延时函数 delay,其原型说明如下:
void delay (unsigned milliseconds);
该函数中断程序的执行,中断的时间由 milliseconds 指定。
例 4-22 该程序每间隔 10000 milliseconds PC 扬声器发出不同频率的声音,直到频率为 于 5000Hz。
#include<dos.h>
main() { int freq;
for(freq=50;freq<5000;freq+=50) { sound (freq);
delay(10000);
}
nosound();
}
4.5.2 乐谱的计算机表示方法
音乐程序设计中有两个最重要的因素:音符和音长。进行音乐程序设计时必须要解决如 何用“曲调定义语言”来表示音符和如何控制音符的持续时间问题。
1.音符
音调由音符构成,音调的高低由音频决定,频率越高,音调也越高。音乐中使用的频率 一般为 131~1976Hz,它包括中央 C 调及前后的 4 个 8 度音程。这 4 个 8 度中各音符的频率 如表 4-15 所示。
表 4-15 频率与音阶的对照表
音调 低音 频率 中音 频率 高音 频率 最高音 频率
C 1 131 1 262 1 523 1 1047
D 2 147 2 296 2 587 2 1175
E 3 165 3 330 3 659 3 1319
F 4 176 4 349 4 699 4 1397
G 5 196 5 392 5 784 5 1568
A 6 220 6 440 6 880 6 1760
B 7 247 7 494 7 988 7 1976
为 了 在 程 序中 输 入 方便 , 常用 英 文 字母 表 示 音符的 频 率 。 用 asdfghj 表 示 中 音 段
CDEFGAB;zxcvbnm 代表低音段 CDEFGAB;qwertyu 代表高音段 CDEFGAB。对于很不常用 的最高音的 7 个音符,可以再用 7 个其他符号代替。
表示音符频率的另一个方案是用枚举类型常量来定义表 4-15 中各音符的频率。
例如,定义 enum music {
C0=131, D0=147, E0=165, F0=175, G0=196, A0=220, B0=247, C1=262, D1=294, E1=330, F1=349, G1=392, A1=440, B1=494, C2=523, D2=587, E2=659, F2=698, G2=784, A2=880, B2=988, C3=1047,D3=1175,E3=1319,F3=1397,G3=1568,A3=1760,B3=1976, }
2.音长
音长表示音符的持续时间。演奏时,每一个音符必须有一个频率用 sound 去发声,且必须 有适当的时间延时,形成拍子。在乐曲中,音长分全音符、半音符、4 分音符、8 分音符等。
通常以 4 分音符为一拍,这样可以用 1 拍的时间来推出其他节拍。
为了在程序中表示音长,一般可以有 2 个方案来解决这个问题。一是在曲谱中找出持续 最短的音符,把它作为一个字母写出,然后时长是其一倍的就用两个同样的字母表示,以此类 推,两倍的用 3 个同样的字母表示,4 倍的就用 4 个同样的字母表示。
例如,对于 2 . 3 1 1 6.|56.. 5.两小节乐谱,可以表示成:
s s s d a a n bb n n bbbb。
另一个方案是采用宏定义来解决:
# define m1 32
# define m2 m1/2
# define m4 m1/4
# define m8 m1/8
# define m16 m1/16
上述两小节乐谱,可以表示成如表 4-16 所示。
表 4-16 乐谱示例
音符 音高 音长
2 D1 m8+m16
3 E1 m16
1 C1 m8
1 C1 m16
6 A0 m16
5 G0 m8
6 A0 m8
5 G0 m4
知道了这些知识,就容易编制一个乐谱程序。
4.5.3 应用
例 4-23 《雪绒花》歌曲程序。
源代码如下:
# include<conio.h>
# include<dos.h>
# define speed 2
void sound1(int freq,int time);
void pause(int time);
main() { int i,freq;
int time=4*speed;
char *qm="iddgwwwqqgfffddddfghhhggg ddgwwwqqgfff\
ddgghjqqqqqwpggjhgddgqqq hhqwwqjjjggg\
ddgwwwqqgfffddgghjqqqqqq";
gotoxy(40,20);
cprintf("Snow Flower");
while(*qm++!='\0') { i=1;
switch(*qm)
{ case'k': time=1*speed; i=0; break;
case 'l': time=2*speed; i=0; break;
case 'i': time=4*speed; i=0; break;
case 'o': time=6*speed; i=0; break;
case 'p': pause(time); i=0; break;
case 'a': freq=523; break;
case 's': freq=587; break;
case 'd': freq=659; break;
case 'f': freq=698; break;
case 'g': freq=784; break;
case 'h': freq=880; break;
case 'j': freq=988; break;
case 'z': freq=262;break;
case 'x': freq=294;break;
case 'c': freq=330;break;
case 'v': freq=349;break;
case 'b': freq=392;break;
case 'n': freq=440;break;
case 'm': freq=494;break;
case 'q': freq=1047;break;
case 'w': freq=1175;break;
case 'e': freq=1319; break;
case 'r': freq=1397; break;
case 't': freq=2568; break;
case 'y': freq=1760; break;
case 'u': freq=1976; break;
default: i=0; break;
} if(i)
sound1(freq,time);
} }
void sound1(int freq,int time) { int n;
sound(freq);
n=time+clock();
while(n>clock());
nosound();
}
void pause(int time) { int n;
n=time+clock();
while(n>clock()) nosound();
}
注释:clock()测得从程序开始到调用处处理机所用的时间,其头文件为 time.h。
函数 sound()可以用指定的频率控制机内扬声器发出指定的声响,直到用函数 nosound()去 关闭它。另外,分别用 k、l、i 表示 1/2、1/4、3/2 节拍。P 表示休止符。演奏速度由 SPEED 控制。具体识音过程采用 switch case 语句。本例中 i=0 表示不发音;i=1 表示发音。
例 4-24 2 . 3 1 1 6.|56.. 5.两小节计算机乐谱。
# include <stdio.h>
#include<dos.h>
# define m1 4000000000
# define m2 4000000000/2
# define m4 4000000000/4
# define m8 4000000000/8
# define m16 4000000000/16 enum music
{ C0=131, D0=147, E0=165, F0=175, G0=196, A0=220, B0=247, C1=262, D1=294, E1=330, F1=349, G1=392, A1=440, B1=494, C2=523, D2=587, E2=659, F2=698, G2=784, A2=880, B2=988, C3=1047,D3=1175,E3=1319,F3=1397,G3=1568,A3=1760,B3=1976, }s1[ ]={D1,E1,C1,C1,A0,G0,A0,G0};
main() { int i ;
int s2[ ]={m8+m16,m16,m8,m16,m16,m8,m8,m4};
for(i=0;i<8;i++) { sound(s1[i]);
delay(s2[i]);
}
nosound();
}