2007年12月24日星期一

混音

Symbian 6.1上实现的混音是个比较麻烦的问题,因为程序只能同时播放一个音乐,实现混音就需要程序自己来实现。下面是我从newlc上找到的一个关于PCM脉冲编码的音频信号的混音实现,其中包含了一个关键的混音算法!

Hi !!!!

I am not sure weather I have fully understood your question or not, I persume that you are asking
"How can we mix two or more audio stream", If this is the question then I am explaning below the
mixing of the two audio stream (You Can Mix More Audio Stream),

Step 1,

Get the Raw data of the two files, (Example, of the sample 8bit and 8Kh, means one sample is of
8bit)


Step 2

Let the two audio signal be A and B respectively, the range is between 0 and 255. Where A and B are the
Sample Values (Each raw data) And store the resultant into the Y

If Both the samples Values are possitve

Y = A + B - A * B / 255

Where Y is the resultant signal which contains both signal A and B, merging two audio streams into single
stream by this method solves the problem of overflow and information loss to an extent.


If the range of 8-bit sampling is between -127 to 128

If both A and B are negative Y = A +B - (A * B / (-127))
Else Y = A + B - A * B / 128


Similarly for the nbit (ex 16bit data)

For n-bit sampling audio signal


If both A and B are negative Y = A + B - (A * B / (-(2 pow(n-1) -1)))
Else Y = A + B - (A * B / (2 pow(n-1))


Step 3.

Add the Header to the Resultant (mixed) data and play back.

If some thing is unclear and ambigious let me know.

Regards
Ranjeet Gupta.

还有简单C程序示意代码,但是其中包含了核心算法:
#include
#include
#include
#include

int main(int argc,char *argv[]) {
char mixname[255];
FILE *pcm1, *pcm2, *mix;
char sample1, sample2;
int value;

pcm1 = fopen(argv[1],"r");
pcm2 = fopen(argv[2],"r");

strcpy (mixname, argv[1]);
strcat (mixname, "_temp.wav");
mix = fopen(mixname, "w");

while(!feof(pcm1)) {

sample1 = fgetc(pcm1);
sample2 = fgetc(pcm2);

if ((sample1 < 0) && (sample2 < 0)) {
value = sample1 + sample2 - (sample1 * sample2 / -(pow(2,16-1)-1));
}else{
value = sample1 + sample2 - (sample1 * sample2 / (pow(2,16-1)-1));
}

fputc(value, mix);
}


fclose(pcm1);
fclose(pcm2);
fclose(mix);

return 0;
}

另外,如果只是在Symbian上实现混音,在Nokia 的官方网站上,也有一个SoundMixer的examples
http://www.forum.nokia.com/info/sw.nokia.com/id/70a2bde5-9b14-41b3-89ae-198b0d8d380d/SoundMixer_Example_v1_0.zip.html

2007年12月23日星期日

多路数字语音合成

摘要数字音频混音是嵌入式多媒体设备不可缺少的功能,本文介绍数字混音的基本原理,并提供了几种实际应用方法,并进行了实际的测试,取得了良好的效果。 ---- 关键词 多路语音合成,混音,衰减因子。 1.引言 ---- 随着嵌入式处理器的处理能力不断加强,网络技术的不断进展,嵌入式多媒体通讯已经迅速普及,其应用产品前途很越来越广泛,如IP电话,音频会议产品,多媒体教学产品。这些产品的一个基本功能就……
【文章正文】

摘要 数字音频混音是嵌入式多媒体设备不可缺少的功能,本文介绍数字混音的基本原理,并提供了几种实际应用方法,并进行了实际的测试,取得了良好的效果。
---- 关键词 多路语音合成,混音,衰减因子。
1.引言
---- 随着嵌入式处理器的处理能力不断加强,网络技术的不断进展,嵌入式多媒体通讯已经迅速普及,其应用产品前途很越来越广泛,如IP电话,音频会议产品,多媒体教学产品。这些产品的一个基本功能就是语音采集(数字化),语音传输和实时播放。但由于成本和设计资源的限制,一个终端一般只可能有一路(立体声)输出能力,这在一对一通话是不会产生任何问题。但在多个终端同时通话时(如音频会议),如果接收到的数字语音信号不经特殊处理,就很难模拟出多个与会者在一个会议室进行对话的情形。这就要求终端有能力将收到到多路数字语音合成(混合)成一路输出。因此必须采取多路音频流混合方案。
2.设计思想:
2.1 混音的合理性和必要性:
---- 传输话音这种连续的、时间要求严格的流与传输别的数据不同。这涉及声波转换为数字信号,数字信号恢复为声波。虽然AD/DA由硬件完成,但录音和播放由用户设置。特别需要注意的是,不象典型的视频流在空间/时间域占有唯一的位置,在同一时间和位置的图象元素叠加是无任何意义的,但人耳可以感知在同一空间/ 时间播放多个音频流。这就是混音的合理性和必要性。混音就是用一个输出设备播放多个音频流。
2.2 混音方案的理论依据:
---- 在时域上语音是短时平稳信号,对语音信号进行处理的一个基本概念就是对语音样本以缓冲区为单位处理,即对输入的语音样本分帧。为了满足合理的延时要求,语音帧的长度一般是选10~20ms。
---- 当多个音频源在一个房间播放时,人耳听到的声波是各个声源声波的线性叠加,这正是模拟混音的基础。该事实表明数字话音的混音也应线性相加。假设有n路输入音频流进行混音,Xi(t)是t时刻的第i路输入语音的采样值,则t时刻的混音值为:
---- m(t)=ΣXi(t),i=0,1,…,n-1
---- 数字音频的重要参数是采样率和采样位数,其中各路输入音频流的采样率相同是混音的前提。
3. 混音的实际算法:
考虑到数字音频的采样位数,其每点的的采用的最大最小值是被限定的,如8位采样,则采样值的范围是-127~+127。因此在混音计算时,m(t)=ΣXi (t)必须考虑其溢出的情况。简单的处理溢出的方法是m(t)=(ΣXi(t))/n, 其中n表示参与混音的路数,这样即可以保证每个采样点都不会溢出。
但这种算法的缺点也很明显,即每路音频的音量都被缩小了n倍,没有使每路的音频在不溢出的情况下达到最大的输出(与混音前的每路音频尽量接近)。要达到这种效果,就需要使用另外一种比较复杂的算法: 使用衰减因子f, m(t)=Σ(Xi(t)*f)。其中 (1/n)<= f <= 1, n为混音的路数。
如上面所说,一个语音帧是一个处理单元。初始化时令f=1,每次开始处理一个新的语音帧时,任何一个合成值m(t)超过了范围,将m(t)箝位,并且求得对应的新的f,在时序上位于此点之后的样本都采用新的f。可以看出这样一来,f有越来越小的趋势。为了避免语音不必要的被衰减,因此需要有让f变大的地方,这发生在每个新的语音帧开始处理的入口。新的语音帧样本仍然需要衰减的可能性是很大的,所以f不能每次都又从1开始,而是应该在某种程度上继承过去的值。即在每个新的样本缓冲区的入口处,只要f不等于1,就将其调整为比f稍大一些的值,让它成为新的衰减因子。若样本的确不需要衰减,经过若干帧后f会慢慢变回1。
流程图如下,每次处理语音帧样本长度在10~20ms内选取, 以16位采样为例, f可取1/16, 2/16,… 15/16,1 。 也可以根据不同的应用取不同的取值范围,和不同的 #1, #2 算法。


4.注意事项:
考虑到不同的处理器的处理能力,以及产品对时延的不同要求,我们可以合理的采用上述两种方法。值得注意的是,每点的采样值都是有符号数,不能用简单的无符号数进行处理和运算。
同时,随着需要混合的语音信号路数的增加,在用户不采取任何附加的预防措施的情况下,一些并不需要的信号(例如回声和噪音)就会累积起来并终将导致让人无法接受的质量劣化。这样,每个终端还可以进行输入音频的有声/无声检测,对无有效信号的音道直接丢弃。
5.结束语
---- 在笔者开发音频会议终端(采用ARM7处理器)上,对上述两种方法都进行了测试。结果证明两种方法都能取的比较满意的效果。

2007年12月21日星期五

c#调节音量

多媒体的应用,往往离不开音频文件,DELPHI偏偏不带音量控制的控件,虽然可以安装第三方的音量控制控件,不过对于我们这些编程的超级菜鸟来说,自己动手才是丰脑之道,这里,我利用API函数就实现了WAVE文件的总音量控制及左右声道音量分别控制。

为了调用Windows函数,先在Uses中加上MMSystem,Delphi提供了调用WaveOutSetVolume 函数的Pascal语言的接口,不信看看MMSystem.pas文件*&^@*<。

我编了演示程序,界面如下:
加入三个TrackBar,分别命名为TB_vol,TB_l,TB_r,将Max值设为255(即8位二进制数的最大值),Frequency的值设为10。

先看看总音量控制的实现:


procedure TForm1.TB_volChange(Sender: TObject);

var
pos,vol:longint;

begin
pos:=TB_vol.Position;

vol:=(pos shl 8)or(pos shl 24);

waveoutsetvolume(0,vol);

end;


首先我们得到TrackBar的位置,用这个值来记录音量的大小,第二句转换Windows函数中对应的正确形式,末一句设置音量。

WaveOutSetVolume函数有两个参数:

第一个参数为波形文件输出设备的标识符,这里的标识符为0。

第二个参数为音量的大小,他是一个32位的整数,低16位表示左声道的音量,高16位表示右声道的音量。

由于TrackBar只用8位二进制数表示音量,因而先把得到的8位音量值pos左移8位,变成左声道16为中的高8位,然后将pos右移24位,变成右声道16为中的高8位,将两个值进行or运算赋给vol,将vol加给WaveOutSetVolume函数的第二个参数,这样就可以实现同时改变两个声道的音量了。

理解上述方法后,分别控制左右声道就不难了,看看左声道的实现:


procedure TForm1.TB_lChange(Sender: TObject);

var
pos,vol:longint;

begin
pos:=TB_l.Position;

waveoutgetvolume(0,@vol);

vol:=vol and $ffff0000 or (pos shl 8);

waveoutsetvolume(0,vol);

end;


由于只修改左声道的值,为了保存右声道的音量,即保存得到音量值pos的高16位,我们将vol和$ffff0000(16进制数)进行or运算,这样,vol的低16位被清零,高16位保持不变,再将低16位左移8位表示新的左声道的音量。

右声道同理实现


procedure TForm1.TB_rChange(Sender: TObject);

var
pos,vol:longint;

begin
pos:=TB_r.Position;

waveoutgetvolume(0,@vol);

vol:=vol and $0000ffff or (pos shl 24);

waveoutsetvolume(0,vol);

end;


同时,我们为了程序一运行滑块就能到当前的音量(完善完善嘛),需要在Form的Create事件中加入以下语句:



procedure TForm1.FormCreate(Sender: TObject);

var
vol:longint;

begin
waveoutgetvolume(0,@vol);

TB_l.Position :=hi(vol); {设置左声道音量}

TB_r.Position :=hi(vol shr 16); {设置右声道音量}

end;

2007年12月18日星期二

Socket 错误描述

常数 值 描述
sckOutOfMemory 7 内存不足
sckInvalidPropertyValue 380 属性值无效。
sckGetNotSupported 394 属性不可读。
sckSetNotSupported 383 属性是只读的。
sckBadState 40006 所请求的事务或请求本身的错误协议或者错误连接状态。
sckInvalidArg 40014 传递给函数的参数格式不确定,或者不在指定范围内。
sckSuccess 40017 成功。
sckUnsupported 40018 不受支持的变量类型。
sckInvalidOp 40020 在当前状态下的无效操作
sckOutOfRange 40021 参数越界。
sckWrongProtocol 40026 所请求的事务或请求本身的错误协议
sckOpCanceled 1004 取消操作。
sckInvalidArgument 10014 所请求的地址是广播地址,但未设置标记。
sckWouldBlock 10035 套接字不成块,而指定操作将使之成块。
sckInProgress 10036 制造块的 Winsock 操作在进行之中。
sckAlreadyComplete 10037 完成操作。未进行制造块的操作。
sckNotSocket 10038 描述符不是套接字。
sckMsgTooBig 10040 数据报太大,不适于缓冲区的要求,因而被截断。
sckPortNotSupported 10043 不支持指定的端口。
sckAddressInUse 10048 地址在使用中。
sckAddressNotAvailable 10049 来自本地机器的不可用地址。
sckNetworkSubsystemFailed 10050 网络子系统失败。
sckNetworkUnreachable 10051 此时不能从主机到达网络。
sckNetReset 10052 在设置 SO_KEEPALIVE 时连接超时。
sckConnectAborted 11053 由于超时或者其它失败而中止连接。
sckConnectionReset 10054 通过远端重新设置连接。
sckNoBufferSpace 10055 没有可用的缓冲空间。
sckAlreadyConnected 10056 已连接套接字。
sckNotConnected 10057 未连接套接字。
sckSocketShutdown 10058 已关闭套接字。
sckTimedout 10060 已关闭套接字。
sckConnectionRefused 10061 强行拒绝连接。
sckNotInitialized 10093 应首先调用 WinsockInit。
sckHostNotFound 11001 授权应答:未找到主机。
sckHostNotFoundTryAgain 11002 非授权应答:未找到主机。
sckNonRecoverableError 11003 不可恢复的错误。
sckNoData 11004 无效名,对所请求的类型无数据记录。

2007年12月17日星期一

语音编程 学习资料

http://www.cnblogs.com/AndyHai/archive/2007/07/07/809883.html
http://www.vckbase.com/document/

MMIO编程

MMIO(MultiMedia file Input and Output多媒体文件的输入与输出)比标准操作系统的I/O服务提供更多的功能,包括缓冲区I/O、RIFF文件、内存文件和定制的存储系统等。另外,多媒体文件I/O服务还对性能敏感的应用程序进行了优化。
MMIO定义了2个数据类型和2个结构类型、并提供了18个函数和1个宏,还有7个专用消息。由于篇幅所限,本节只针对RIFF文件(主要是WAV格式)的读入,介绍其中若干常用的结构和函数。
本节先介绍RIFF文件的结构和相关定义,然后给出MMIO的若干基本I/O和RIFF I/O函数,最后编写读取WAV文件和资源的内容和信息的实例代码。
12.6.1 RIFF文件
RIFF(Resource Interchange File Format资源交换文件格式)是微软为Windows设计的一类多媒体文件格式,主要包括WAV(Waveform Audio File Format波形音频文件格式)和AVI(Audio/Video Interleaved File Format音频视频交互文件格式)两种。音频文件格式WAV已经在3.3.1中介绍过,而视频文件格式AVI也在6.2.3中介绍过。

 文件结构
RIFF文件由文件头、数据类型标识及若干块组成。块(chunk)是RIFF文件的基本结构,每个块都包括如下三个域:
 用于标识块类型的4字符代码,如’RIFF’、’LIST’和’fmt ’、’data’等;
 数据块大小的字节数
 数据域本身
RIFF文件中的块是可以嵌套的,块的数据域可含若干子块,子块又可含孙块。例如,RIFF文件本身就是一个RIFF块,其数据域可以是WAV子块或AVI子块,而WAV子块又包含格式块('fmt ‘)和数据块('data’),AVI文件的AVI子块,包含格式列表块(’LIST’)、数据列表块(’LIST’)和可选的索引块(’ind1’),……。
RIFF文件具有相同的结构:
’RIFF’<文件大小><格式类型><块>……
例如,WAV文件的结构为:
’RIFF’<文件大小>’WAVE’<格式块><数据块>
而AVI文件的结构为:
’RIFF’<文件大小>’AVI ’<格式列表块><数据列表块>[<索引块>]

为了读写和处理RIFF文件,在多媒体SDK和MMIO中,定义了若干数据类型、结构和宏。下面给出它们的定义和使用方法:

 四字符代码类型和其构造宏
为了读写和比较RIFF文件的由4字符组成的块类型标识代码,定义了一个FOURCC(FOUR Character Code,四字符代码)数据类型:
typedef DWORD FOURCC;
还定义了一个由4个字符来构造FOURCC类型值的宏mmioFOURCC:
FOURCC mmioFOURCC( // 构造四字符代码
CHAR ch0, // 四字符代码的第1个字符,typedef char CHAR;
CHAR ch1, // 四字符代码的第2个字符
CHAR ch2, // 四字符代码的第3个字符
CHAR ch3 // 四字符代码的第4个字符
);
该宏是利用MAKEFOURCC宏来定义的:
#define mmioFOURCC(ch0, ch1, ch2, ch3) MAKEFOURCC(ch0, ch1, ch2, ch3);
而MAKEFOURCC宏的定义为:
#define MAKEFOURCC(ch0, ch1, ch2, ch3) \
((DWORD)(BYTE)(ch0) | ((DWORD)(BYTE)(ch1) << 8) | \
((DWORD)(BYTE)(ch2) << 16) | ((DWORD)(BYTE)(ch3) << 24 ));
例如:
#define ID_RIFF mmioFOURCC('R', 'I', 'F', 'F')
#define ID_WAVE mmioFOURCC('W', 'A', 'V', 'E')
#define ID_fmt mmioFOURCC('f', 'm', 't', ' ')
#define ID_data mmioFOURCC('d', 'a', 't', 'a')
FOURCC id;
ar >> id;
if (id != ID_RIFF) ……

 块结构定义
typedef struct {
FOURCC ckid; // 块ID
DWORD cksize; // 块的字节大小
FOURCC fccType; // (RIFF和LIST块的)格式类型(如’WAVE’)
DWORD dwDataOffset; // 块的数据部分的偏移量
DWORD dwFlags; // 用于MMIO函数的标志,可取0或MMIO_DIRTY(脏)
} MMCKINFO;

12.6.2 基本与RIFF I/O函数
下面是若干通用的MMIO基本I/O函数和两个用于RIFF I/O的定位函数:

 打开/关闭文件
mmioOpen函数可以用来打开缓冲和非缓冲的I/O文件,这里的文件可以是标准文件、内存文件和定制存储系统的元素。该函数的原型为:
HMMIO mmioOpen( // 返回北打开文件的句柄,出错返回NULL
LPSTR szFilename, // 文件名串,对内存文件为NULL
LPMMIOINFO lpmmioinfo, // 指向包含额外参数的MMIOINFO结构的指针
// 一般为NULL,除非是打开内存文件,指定缓冲I/O的缓冲区大小
DWORD dwOpenFlags // 打开操作的标志位组合,取值见表12-11
);
其中,MMIOINFO结构的定义为:
typedef struct {
DWORD dwFlags; // 打开文件方式标志,取值见表12-11
FOURCC fccIOProc; // 标识文件IO过程的四字符代码,无IO过程时为NULL
LPMMIOPROC pIOProc; // 件IO过程的指针
UINT wErrorRet; // 扩展的错误值,只用于打开函数
HTASK hTask; // 局部IO过程的句柄
LONG cchBuffer; // 文件缓冲区的字节大小,对非缓冲文件为0
HPSTR pchBuffer; // 指向文件缓冲区的指针,对非缓冲文件为NULL
HPSTR pchNext; // 指向缓冲区的下一读/写位置的指针
HPSTR pchEndRead; // 指向缓冲区内的可读位置尾
HPSTR pchEndWrite; // 指向缓冲区内的可写位置尾
LONG lBufOffset; // 保留
LONG lDiskOffset; // 当前文件位置(从文件头的偏移字节数)
DWORD adwInfo[4]; // IO过程维护的状态信息
DWORD dwReserved1; // 保留
DWORD dwReserved2; // 保留
HMMIO hmmio; // 由打开函数返回的文件句柄
} MMIOINFO;
其中,HPSTR类型的定义为:
typedef char _huge * HPSTR; /* a huge version of LPSTR */
表12-11 打开文件操作的标志符号常量
=========================================================
值 含义
MMIO_ALLOCBUF 打开缓冲文件
MMIO_COMPAT 打开兼容模式文件
MMIO_CREATE 创建新文件
MMIO_DELETE 删除文件
MMIO_DENYNONE 打开共享文件
MMIO_DENYREAD 打开禁止其他过程读访问的文件
MMIO_DENYWRITE 打开禁止其他过程写访问的文件
MMIO_EXCLUSIVE 打开禁止其他过程读写访问的文件
MMIO_EXIST 判断指定文件是否存在,存在时返回TRUE
MMIO_GETTEMP 创建临时文件名
MMIO_PARSE 创建完整文件名
MMIO_READ 打开只读文件
MMIO_READWRITE 打开读写文件
MMIO_WRITE 打开只写文件
=========================================================
由mmioOpen函数打开的文件,在使用完后必须调用函数mmioClose关闭它。该关闭函数对缓冲文件,还会调用mmioFlush函数将缓冲区的数据写回磁盘。关闭函数的原型为:
MMRESULT mmioClose( // 成功返回MMSYSERR_NOERROR(= 0)
HMMIO hmmio, // 文件句柄
UINT wFlags // 关闭操作标志,可取0或MMIO_FHOPEN(对非MMIO文件句柄)
);
例如:
HMMIO hmmio = mmioOpen(L"sample.wav", NULL, MMIO_READ);
…… // 使用和处理数据
mmioClose(hmmio, 0);

 读/写数据
// 从文件中读取数据
LONG mmioRead( // 返回实际读出的字节数
HMMIO hmmio, // 文件句柄
HPSTR pch, // 存放读入数据的缓冲区指针
LONG cch // 读取数据的字节数
);
// 写入数据到文件
LONG mmioWrite( // 返回实际写入的字节数
HMMIO hmmio, // 文件句柄
HPSTR pch, // 存放写入数据的缓冲区指针
LONG cch // 写入数据的字节数
);
例如:
WAVEFORMATEX wfFormat;
mmioRead(hmmio, (HPSTR)&wfFormat, sizeof(WAVEFORMATEX));
又如:
Write(const char* pData, LONG lLen) {
ASSERT(m_hmmio != NULL);
return ::mmioWrite(m_hmmio, pData, lLen);
}

 定位文件
// 定位:改变文件的当前位置
LONG mmioSeek( // 返回新位置(相对于文件头的字节数),出错返回-1
HMMIO hmmio, // 文件句柄
LONG lOffset, // 改变位置的偏移量
int iOrigin // 偏移类型标志,可取值有:SEEK_CUR(从当前起算)、
// SEEK_END(从文件尾起算)、SEEK_SET(从文件头起算)

);
// 下降:进入RIFF文件中的(特定)块
MMRESULT mmioDescend( // 成功返回MMSYSERR_NOERROR(= 0)
HMMIO hmmio, // 文件句柄
LPMMCKINFO lpck, // 指向接收MMCKINFO结构的缓冲区指针
LPMMCKINFO lpckParent, // 指向指定父块之MMCKINFO结构的指针
// 无父块时取为NULL
UINT wFlags // 搜索标志,可取值有:0(当前位置)、MMIO_FINDCHUNK
// (指定块标识符)、MMIO_FINDLIST(列表块内的指定标识符块)、
// MMIO_FINDRIFF(RIFF块内的指定标识符块)
);
// 上升:移出RIFF文件中的当前块
MMRESULT mmioAscend( // 成功返回MMSYSERR_NOERROR(= 0)
HMMIO hmmio, // 文件句柄
LPMMCKINFO lpck, // 指向(已被mmioDescend填充的)MMCKINFO结构的指针
UINT wFlags // 保留,必须为0
);
例如:
MMCKINFO ckInfo; // 定义块信息结构变量
mmioSeek(hmmio, 12, SEEK_SET); // 定位到’fmt ‘块的开始处
mmioDescend(hmmio, &ckInfo, NULL, 0); // 获取’fmt ‘块信息
…… // 读取操作
mmioAscend(hmmio, &ckInfo, 0); // 跳出’fmt ‘块
LONG currentPosition = mmioSeek(hmmio, 0, SEEK_CUR); // 获取当前位置
又例如:
// Set the position to the begining of the file
if ( mmioSeek(hmmio, 0, SEEK_SET) == -1) {
MessageBox(L"Seek to the begining of file error!", L"Error");
return false;
}
// Find the RIFF chunk
MMCKINFO mmckRiffInfo;
mmckRiffInfo.ckid = mmioFOURCC('R','I','F','F');
if ( mmioDescend(hmmio, &mmckRiffInfo, NULL, MMIO_FINDCHUNK) != 0 ) {
MessageBox(L"Descend to RIFF chunk error!", L"Error");
return false;
}
// Find the WAVE chunk
MMCKINFO mmckWaveInfo;
mmckWaveInfo.ckid = mmioFOURCC('W','A','V','E');
if ( mmioSeek(hmmio, -4, SEEK_CUR) == -1) {
MessageBox(L"Seek to the begining of 'WAVE' error!", L"Error");
return false;
}
if ( mmioDescend(hmmio, &mmckWaveInfo, &mmckRiffInfo,
MMIO_FINDCHUNK) != 0 ) {
MessageBox(L"Descend to WAVE chunk error!", L"Error");
return false;
}
// Find and read the format subchunk
MMCKINFO mmckFmtInfo;
mmckFmtInfo.ckid = mmioFOURCC('f','m','t',' ');
if ( mmioSeek(hmmio, 12, SEEK_SET) == -1) {
MessageBox(L"Seek to the begining of 'fmt ' error!", L"Error");
return false;
}
if ( mmioDescend(hmmio, &mmckFmtInfo, &mmckWaveInfo,
MMIO_FINDCHUNK) != 0 ) {
MessageBox(L"Descend to fmt chunk error!", L"Error");
return false;
}
if ( mmioRead(hmmio, (HPSTR)&wfFormat, sizeof(WAVEFORMATEX))
< sizeof(WAVEFORMATEX) ) {
MessageBox(L"Read format struct error!", L"Error");
return false;
}
if ( mmioAscend(hmmio, &mmckFmtInfo, 0) != 0 ) {
MessageBox(L"Ascend from fmt chunk error!", L"Error");
return false;
}
12.6.3 读取WAV文件和资源
可以利用MMIO,来读取WAV文件和资源中的数据内容与参数信息。下面给出若干有关函数和代码段,这些内容,在后面的13.3节(DirectX声音编程)中会用到。

 读取WAV文件
函数OpenWave,用于打开WAV波形音频文件,获取文件的句柄:
// 打开WAV文件
HMMIO OpenWave(LPWSTR strFilePath) {// Open WAV file
return mmioOpen(strFilePath, NULL, MMIO_READ);
}

函数GetWaveData,利用文件句柄,来获取波形音频格式结构内容和数据块长度:
// 获取波形数据:格式结构和数据块长度
bool GetWaveData(HMMIO hmmio, WAVEFORMATEX& wfFormat,
DWORD& dwDataLen) {
// Find the RIFF chunk and WAVE form type
MMCKINFO mmckInfo;
if ( mmioDescend(hmmio, &mmckInfo, NULL, 0) != 0
|| mmckInfo.ckid != FOURCC_RIFF
|| mmckInfo.fccType != mmioFOURCC('W','A','V','E') ) {
MessageBox(L"Not a WAV file !", L"Error");
return false;
}
// Find and read the format subchunk
if ( mmioDescend(hmmio, &mmckInfo, NULL, 0) != 0
|| mmckInfo.ckid != mmioFOURCC('f','m','t',' ') ) {
MessageBox(L"fmt chunk error!", L"Error");
return false;
}
if ( mmioRead(hmmio, (HPSTR)&wfFormat, sizeof(WAVEFORMATEX))
< sizeof(WAVEFORMATEX) ) {
MessageBox(L"Read format struct error!", L"Error");
return false;
}
if ( mmioAscend(hmmio, &mmckInfo, 0) != 0 ) {
MessageBox(L"Ascend from fmt chunk error!", L"Error");
return false;
}
// Find and get the size of the data subchunk
mmckInfo.ckid = mmioFOURCC('d','a','t','a');
if ( mmioDescend(hmmio, &mmckInfo, NULL, MMIO_FINDCHUNK) != 0 ) {
MessageBox(L"data chunk error!", L"Error");
return false;
}
dwDataLen = mmckInfo.cksize;
return true;
}

函数GetWave,调用OpenWave和GetWaveData函数,由文件路径串来打开波形音频文件,获取文件句柄、格式结构和数据块长度:
// 获取波形:文件句柄、格式结构和数据块长度
bool GetWave(LPWSTR strFilePath, HMMIO& hmmio,
WAVEFORMATEX& wfFormat, DWORD& dwDataLen) {
// Open WAV file
hmmio = OpenWave(strFilePath);
if ( hmmio == NULL ) {
MessageBox(L"Open WAV file error!", L"Error");
return false;
}
// Get WAV data
if ( !GetWaveData(hmmio, wfFormat, dwDataLen) ) {
MessageBox(L"Get wave data error!", L"Error");
return false;
}
return true;
}

 读取WAV资源
为了读取WAV资源,需要调用若干SDK函数来定位资源位置、装入资源到全局内存、锁定资源并获得资源地址、获取资源大小等。下面逐个加以介绍:
// 确定指定模块中指定类型和名称之资源的位置
HRSRC FindResource( // 成功返回指定资源的信息块句柄,失败返回NULL
HMODULE hModule, // 其可执行文件包含资源之模块的句柄,NULL为当前模块
LPCTSTR lpName, // 资源名,一般取MAKEINTRESOURCE(资源ID)
LPCTSTR lpType // 指定资源类型,取值可为RT_BITMAP、RT_CURSOR、
// RT_ICON、……,对波形音频资源为L"WAVE"
);
其中,hModule也可取值为MFC的全局函数AfxGetInstanceHandle的返回值:
HINSTANCE AFXAPI AfxGetInstanceHandle( ); // 返回当前应用程序的实例句柄
// 将指定资源装入全局内存
HGLOBAL LoadResource( // 成功返回资源数据句柄,失败返回NULL
HMODULE hModule, // 模块的句柄,NULL为当前模块
HRSRC hResInfo // 资源信息块句柄
);
// 释放资源
BOOL FreeResource( // 成功返回0
HGLOBAL hglbResource // 资源数据句柄
);
// 锁定资源,获得资源地址
LPVOID LockResource( // 成功返回资源头指针,失败返回NULL
HGLOBAL hResData // 资源数据句柄
);
// 获取资源字节大小
DWORD SizeofResource( // 返回资源字节数,失败返回0
HMODULE hModule, // 模块的句柄,NULL为当前模块
HRSRC hResInfo // 资源信息块句柄
);

 读取WAV资源例子
下面利用这些SDK函数,来打开波形音频资源:
// 打开WAV资源
HMMIO OpenWave(UINT uiResID) { // Open WAV resource
HMMIO hmmio = NULL;
BYTE* m_pImageData;
DWORD m_dwImageLen;
// Find the wave resource
HRSRC hresInfo = FindResource(NULL, MAKEINTRESOURCE(uiResID),
L"WAVE");
if ( hresInfo == NULL) return NULL;
// Load the wave resource
HGLOBAL hgmemWave = LoadResource(NULL, hresInfo);
// Get wave file image
if (hgmemWave != NULL) {
// Get pointer to and length of the wave image data
m_pImageData= (BYTE*)LockResource(hgmemWave);
m_dwImageLen = SizeofResource(NULL, hresInfo);
} else return NULL;
// Set the MMIOINFO struct
MMIOINFO mmioInfo;
ZeroMemory(&mmioInfo, sizeof(MMIOINFO));
mmioInfo.pIOProc = NULL;
mmioInfo.fccIOProc = FOURCC_MEM;
mmioInfo.pchBuffer = (HPSTR)m_pImageData;
mmioInfo.cchBuffer = m_dwImageLen;
// Open MMIO memory file
hmmio = mmioOpen(NULL, &mmioInfo, MMIO_READWRITE);
if ( hmmio == NULL ) {
MessageBox(L"Open memory file error!", L"Error");
return NULL;
}
return hmmio;
}
其中,用到了一个SDK宏:
// 用0填充内存块
void ZeroMemory(
PVOID Destination, // 块头地址
SIZE_T Length // 块的字节大小
);
#define ZeroMemory RtlZeroMemory
#define RtlZeroMemory(Destination,Length) memset((Destination),0,(Length))

函数GetWave,调用OpenWave和GetWaveData函数,由资源ID来打开波形音频文件,获取文件句柄、格式结构和数据块长度:(与WAV文件的类似,其中的GetWaveData函数相同)
// 获取波形:文件句柄、格式结构和数据块长度
bool GetWave(UINT uiResID, HMMIO& hmmio, WAVEFORMATEX& wfFormat,
DWORD& dwDataLen) {
// Open WAV resource
hmmio = OpenWave(uiResID);
if ( hmmio == NULL ) {
MessageBox(L"Open WAV resource error!", L"Error");
return false;
}
// Get WAV data
if ( !GetWaveData(hmmio, wfFormat, dwDataLen) ) {
MessageBox(L"Get wave data error!", L"Error");
return false;
}
return true;
}

MIDI编程

本节先介绍使用MCI播放MIDI文件的方法,然后介绍MMAPI中的基本MIDI函数,最后通过一个虚拟电子琴的实例来演示MIDI的功能。
12.5.1 用MCI播放MIDI文件
MIDI所对应的MCI设备名为sequencer(音序器),似12.3节,也可以使用MCI的命令串方式:
mciSendString(命令串, NULL, 0, NULL);
来打开MIDI设备和播放MIDI文件。
打开MIDI设备的命令串常用格式为:
open MIDI文件路径 alias 别名 type sequencer
播放MIDI文件的命令串一般格式为:
play 别名 from 位置1 to 位置2
位置的单位为毫秒。若无“from 位置1”则从头播放,若无“to 位置2”则播放到文件尾。如“play 别名”是从头到尾播放。
例如:
CString str;
str += L"open \"" + ar.GetFile()->GetFilePath() + L"\" alias midi type sequencer";
wchar_t buf[256];
MCIERROR err = mciSendString(str, NULL, 0, NULL); // 打开MIDI文件
if (err) {
if (mciGetErrorString(err, buf, 256))
MessageBox(NULL, buf, L"Error", MB_OK); // 获得并显示错误串
return;
}
mciSendString(L"play midi", NULL, 0, NULL); // 播放MIDI文件
其他常用的命令还有:暂停播放(pause 别名)、恢复播放(resume 别名)、停止播放(stop 别名)、关闭设备(close 别名)等。
12.5.2 MMAPI中的基本MIDI函数
在12.4.2中已经列出了MMAPI中含有的41个MIDI函数的名称,下面将介绍其中常用的一些基本MIDI函数的功能、原型和参数。

 查询MIDI设备
可以利用midiOutGetNumDevs和midiOutGetDevCaps函数来查询当前系统的MIDI设备和性能:
UINT midiOutGetNumDevs(VOID); // 返回MIDI输出设备的数目(非0表示支持MIDI)
MMRESULT midiOutGetDevCaps( // 检查MIDI输出设备的性能
UINT_PTR uDeviceID, // 设备ID(0 ~ 设备数-1)
LPMIDIOUTCAPS lpMidiOutCaps, // 指向MIDI输出设备的性能结构的指针
UINT cbMidiOutCaps // 性能结构的大小(字节数)
); // 成功时返回MMSYSERR_NOERROR(=0)
typedef struct { // MIDI输出设备的性能结构
WORD wMid; // 厂商ID
WORD wPid; // 产品ID
MMVERSION vDriverVersion; // 驱动程序版本号(高/低位字节分别为主/次版本号)
CHAR szPname[MAXPNAMELEN]; // 产品名称
WORD wTechnology; // 设备类型
WORD wVoices; // 同时音色数
WORD wNotes; // 复音数
WORD wChannelMask; // 通道数
DWORD dwSupport; // 支持的可选功能
} MIDIOUTCAPS;
其中,typedef UINT MMVERSION;,设备类型wTechnology的取值见表12-7:
表12-7 MIDI设备类型
==============================================
wTechnology 数值 设备类型
MOD_MIDIPORT 1 MIDI硬件端口
MOD_SYNTH 2 合成器
MOD_SQSYNTH 3 方波合成器
MOD_FMSYNTH 4 调频合成器
MOD_MAPPER 5 微软MIDI映射器
MOD_WAVETABLE 6 硬件波表合成器
MOD_SWSYNTH 7 软件合成器
=============================================
支持的可选功能dwSupport的取值可为表12-8中值的组合。
======================================================
dwSupport 数值 功能
MIDICAPS_VOLUME 1 支持音量控制
MIDICAPS_LRVOLUME 2 支持分离的左右音量控制
MIDICAPS_CACHE 4 支持音色(patch)缓冲
MIDICAPS_STREAM 8 提供对midiStreamOut函数的直接支持
========================================================
例如:(查询并输出MIDI设备的性能)
UINT dn = midiOutGetNumDevs();
if (dn == 0) MessageBox(L"No MIDI device!", L"Error");
else { // dn > 0
CString str;
MIDIOUTCAPS moc;
for (UINT i = 0; i < dn; i++) {
if (midiOutGetDevCaps(i, &moc, sizeof(moc)) == MMSYSERR_NOERROR) {
str.Format(L"DevNum = %d, M-ID = %d, P-ID = %d, Ver = %.4X, \
P-Name = %s, Tech = %d, Voices = %d, Notes = %d, Channels = %d, Support = %lX",
i, moc.wMid, moc.wPid, moc.vDriverVersion,moc.szPname,
moc.wTechnology, moc.wVoices, moc.wNotes, moc.wChannelMask,
moc.dwSupport);
pDC->TextOut(10, 10 + i*20, str);
}
}
输出结果如:


例如,其中设备号为1的MIDI设备:制造商ID为1(创新公司)、产品ID为102、版本号为5.10、产品名为SB Live! MIDI Synth、采用硬件波表合成器技术、可同时有32种音色和32个复音、有64K个通道、并支持对[主]音量和左右音量的控制。

 播放MIDI数据
为了播放MIDI数据,必须首先调用函数midiOutOpen打开指定序号的MIDI输出设备,利用它所返回的设备句柄指针,再来调用函数midiOutPrepareHeader来准备信息头缓冲区,然后才可以调用函数midiOutLongMsg来播放MIDI数据。
在完成播放后,还必须调用函数midiOutUnprepareHeader来清除信息头缓冲区,最后调用函数midiOutClose来关闭MIDI输出设备。
在播放MIDI数据的过程中,还可以调用函数midiOutReset来停止播放。
这里所用的MIDI数据,可以是利用MMIO函数获取的MIDI文件中的数据,也可以是用户自己动态创建的定制MIDI数据。

下面是这些函数的原型和参数含义:
MMRESULT midiOutOpen( // 打开MIDI输出设备,成功返回MMSYSERR_NOERROR
LPHMIDIOUT lphmo, // 设备句柄指针(用于返回值)
UINT_PTR uDeviceID, // 设备ID(一般取为MIDI_MAPPER[=1])
DWORD_PTR dwCallback, // 回调函数指针(一般取为NULL[0])
DWORD_PTR dwCallbackInstance, // 回调函数使用的数据(一般取为NULL[0])
DWORD dwFlags // 打开设备标志(用于回调函数参数的类型,(一般取为0)
);
MMRESULT midiOutPrepareHeader( // 准备输出MIDI音频用的信息头
HMIDIOUT hmo, // (由midiOutOpen获得的)设备句柄指针
LPMIDIHDR lpMidiOutHdr, // 指向头结构的指针
UINT cbMidiOutHdr // 头结构的大小
);
typedef struct { // MIDI信息头结构
LPSTR lpData; // 指向MIDI数据缓冲区的指针
DWORD dwBufferLength; // 缓冲区的大小
DWORD dwBytesRecorded; // 录音时缓冲区中数据的实际数量
// 必须≤dwBufferLength
DWORD_PTR dwUser; // 用户定制的数据
DWORD dwFlags; // 关于缓冲区信息的标志
struct midihdr_tag far * lpNext;
DWORD_PTR reserved;
DWORD dwOffset;
DWORD_PTR dwReserved[4];
} MIDIHDR;
MMRESULT midiOutLongMsg( // 发送系统专用的MIDI消息到指定的MIDI输出设备
HMIDIOUT hmo, // 设备句柄指针
LPMIDIHDR lpMidiOutHdr, // 指向头结构的指针
UINT cbMidiOutHdr // 头结构的大小
);
MMRESULT midiOutUnprepareHeader( // 清除输出MIDI音频用的信息头
HMIDIOUT hmo, // 设备句柄指针
LPMIDIHDR lpMidiOutHdr, // 指向头结构的指针
UINT cbMidiOutHdr // 头结构的大小
);
MMRESULT midiOutReset( // 停止播放
HMIDIOUT hmo // 设备句柄指针
);
MMRESULT midiOutClose( // 关闭MIDI输出设备
HMIDIOUT hmo // 设备句柄指针
);

 发送短消息
在打开MIDI输出设备后,可以调用函数midiOutShortMsg来发送短消息,从而能够动态控制MIDI输出设备和播放乐音。
该函数的原型为:
MMRESULT midiOutShortMsg( // 发送短MIDI消息到指定的MIDI输出设备
HMIDIOUT hmo, // 设备句柄指针
DWORD dwMsg // MIDI消息
);
其中,MIDI消息为4字节无符号整数,其高字的高位字节保留为0、低位字节为MIDI数据的第2个字节,低字的高位字节MIDI数据的第1个字节、低位字节为MIDI的状态。因为在Intel CPU中,多字节整数的低位字节在前,所以实际的MIDI消息的字节顺序见图12-4。
====================================================
字节序号: 0 1 2 3
字节内容: 状态 数据1 数据2 保留为0
==================================================
图12-4 MIDI消息的字节
例如:
union {
DWORD dwData;
BYTE bData[4];
} midi;
midi.bData[0] = status;
midi.bData[1] = data1;
midi.bData[2] = data2;
midi.bData[3] = 0;
midiOutShortMsg(hmo, midi.dwData);
至于MIDI消息的具体内容,会在下面的12.6.3小节中进行介绍。

 其他常用函数
MMRESULT midiOutSetVolume( // 设置MIDI输出设备的音量
HMIDIOUT hmo, // 设备句柄指针
DWORD dwVolume // 低/高字为左[单]/右声道的音量 = 0 ~ 0xFFFF
);
MMRESULT midiOutGetVolume( // 获取MIDI输出设备的当前音量
HMIDIOUT hmo, // 设备句柄指针
LPDWORD lpdwVolume // 指向音量双字变量的指针
);

上面只是简单的介绍了对MIDI输出设备进行操作的一些基本MMAPI函数,还有对MIDI输入设备和MIDI流设备进行操作的各种MMAPI函数。有兴趣的读者,可以参看Visual Studio的帮助文档。
2.5.3 MIDI消息
MIDI应用程序,通过调用MMAPI函数midiOutShortMsg和midiOutLongMsg,向MIDI设备发送各种消息,来使用和控制MIDI设备、播放MIDI文件和输出MIDI数据。参见图12-5



MIDI消息分为两大类:通道消息和系统消息(参见图12-6),前者由函数midiOutShortMsg发送,后者则由函数midiOutLongMsg来发送。最常用的是通道声音消息中的音符开/关和程序变换(选择音色/乐器种类)消息。



图12-6 MIDI消息的分类
MIDI消息由若干(2~3个)字节组成,第一个字节为状态字节,后跟1~2个数据字节。为了能区分状态字节和数据字节,MIDI规定状态字节的高位为1(即值≥128[0x80]),而各数据字节的高位为0(即值<128)。
对通道声音消息,状态字节的高4位用来表示种类(8~E)、而其低4位则用来表示通道序号(n = 0~F/15)。表12-9为MIDI通道声音消息的状态字节类型和所对应的数据字节的个数和含义。
========================================
功能描述 状态字节 数据字节
英文 中文 (n = 0~F) 数量 含义
Note Off 音符关 0x8n 2 1:音高、2:速率(音量)
Note On 音符开 0x9n 2 1:音高、2:速率(音量)
Key Pressure 按键力度 0xAn 2 1:音高、2:速率(音量)
Control Change 控制改变 0xBn 2 1:控制器编号、2:控制值
Program Change 程序改变 0xCn 1 1:程序编号(音色/乐器种类)
Channel Pressure 通道力度 0xDn 1 1:压力值
Pitch Bend 音高弯度 0xEn 2 1:粗调、2:微调
========================================
下面只对其中最常用的音符开关(发出/停止乐音)和程序改变(选择音色/乐器种类)消息的具体内容加以说明:
 0x9n状态:音符开(发出乐音)——使MIDI通道n中的乐器发出指定音高和音强的乐音。
 通道序号n = 0~15,每个通道可选择一种当前音色(乐器种类)
 音高(pitch)= 0(C3 = 8.16Hz)~ 127(g6 = 12543.84Hz),采用12平均律,半音阶。如中音的1~7对应于60(c1 = 1)、62(d1 = 2)、64(e1 = 3)、65(f1 = 4)、67(g1 = 5)、69(a1 = 6)、71(b1 = 7) [72(c2 = )]
 音强(sound intensity)——键被按下的速率(velocity)常对应于音符的强度 = 0(无声) ~ 127(最强声)
例如:
midi.bData[0] = 0x90; // 由通道0发出乐音
midi.bData[1] = 60; // 中音C(c1 = 1)
midi.bData[2] = 127; // 最大音强
midi.bData[3] = 0; // 保留为0
midiOutShortMsg(hmo, midi.dwData); // 播放指定乐音
 0x8n状态:音符关(停止乐音)——停止MIDI通道n中的乐器所发出的指定音高和音强的乐音。通道n、音高和音强的含义同上。
 0xCn状态:程序改变(选择音色)——设置MIDI通道n中的乐器为指定的乐器种类(音色)。状态字节0xCn后跟的单个数据字节为音色/乐器种类。初始的乐器序号为0(三角钢琴)。
GM(General MIDI,通用MIDI)中定义了128种标准的音色/乐器(patch / timbre / voice),见表12-10。
下面只对其中最常用的音符开关(发出/停止乐音)和程序改变(选择音色/乐器种类)消息的具体内容加以说明:
 0x9n状态:音符开(发出乐音)——使MIDI通道n中的乐器发出指定音高和音强的乐音。
 通道序号n = 0~15,每个通道可选择一种当前音色(乐器种类)
 音高(pitch)= 0(C3 = 8.16Hz)~ 127(g6 = 12543.84Hz),采用12平均律,半音阶。如中音的1~7对应于60(c1 = 1)、62(d1 = 2)、64(e1 = 3)、65(f1 = 4)、67(g1 = 5)、69(a1 = 6)、71(b1 = 7) [72(c2 = )]
 音强(sound intensity)——键被按下的速率(velocity)常对应于音符的强度 = 0(无声) ~ 127(最强声)
例如:
midi.bData[0] = 0x90; // 由通道0发出乐音
midi.bData[1] = 60; // 中音C(c1 = 1)
midi.bData[2] = 127; // 最大音强
midi.bData[3] = 0; // 保留为0
midiOutShortMsg(hmo, midi.dwData); // 播放指定乐音
 0x8n状态:音符关(停止乐音)——停止MIDI通道n中的乐器所发出的指定音高和音强的乐音。通道n、音高和音强的含义同上。
 0xCn状态:程序改变(选择音色)——设置MIDI通道n中的乐器为指定的乐器种类(音色)。状态字节0xCn后跟的单个数据字节为音色/乐器种类。初始的乐器序号为0(三角钢琴)。
GM(General MIDI,通用MIDI)中定义了128种标准的音色/乐器(patch / timbre / voice),见表12-10。
下面只对其中最常用的音符开关(发出/停止乐音)和程序改变(选择音色/乐器种类)消息的具体内容加以说明:
 0x9n状态:音符开(发出乐音)——使MIDI通道n中的乐器发出指定音高和音强的乐音。
 通道序号n = 0~15,每个通道可选择一种当前音色(乐器种类)
 音高(pitch)= 0(C3 = 8.16Hz)~ 127(g6 = 12543.84Hz),采用12平均律,半音阶。如中音的1~7对应于60(c1 = 1)、62(d1 = 2)、64(e1 = 3)、65(f1 = 4)、67(g1 = 5)、69(a1 = 6)、71(b1 = 7) [72(c2 = )]
 音强(sound intensity)——键被按下的速率(velocity)常对应于音符的强度 = 0(无声) ~ 127(最强声)
例如:
midi.bData[0] = 0x90; // 由通道0发出乐音
midi.bData[1] = 60; // 中音C(c1 = 1)
midi.bData[2] = 127; // 最大音强
midi.bData[3] = 0; // 保留为0
midiOutShortMsg(hmo, midi.dwData); // 播放指定乐音
 0x8n状态:音符关(停止乐音)——停止MIDI通道n中的乐器所发出的指定音高和音强的乐音。通道n、音高和音强的含义同上。
 0xCn状态:程序改变(选择音色)——设置MIDI通道n中的乐器为指定的乐器种类(音色)。状态字节0xCn后跟的单个数据字节为音色/乐器种类。初始的乐器序号为0(三角钢琴)。
GM(General MIDI,通用MIDI)中定义了128种标准的音色/乐器(patch / timbre / voice),见表12-10。
下面只对其中最常用的音符开关(发出/停止乐音)和程序改变(选择音色/乐器种类)消息的具体内容加以说明:
 0x9n状态:音符开(发出乐音)——使MIDI通道n中的乐器发出指定音高和音强的乐音。
 通道序号n = 0~15,每个通道可选择一种当前音色(乐器种类)
 音高(pitch)= 0(C3 = 8.16Hz)~ 127(g6 = 12543.84Hz),采用12平均律,半音阶。如中音的1~7对应于60(c1 = 1)、62(d1 = 2)、64(e1 = 3)、65(f1 = 4)、67(g1 = 5)、69(a1 = 6)、71(b1 = 7) [72(c2 = )]
 音强(sound intensity)——键被按下的速率(velocity)常对应于音符的强度 = 0(无声) ~ 127(最强声)
例如:
midi.bData[0] = 0x90; // 由通道0发出乐音
midi.bData[1] = 60; // 中音C(c1 = 1)
midi.bData[2] = 127; // 最大音强
midi.bData[3] = 0; // 保留为0
midiOutShortMsg(hmo, midi.dwData); // 播放指定乐音
 0x8n状态:音符关(停止乐音)——停止MIDI通道n中的乐器所发出的指定音高和音强的乐音。通道n、音高和音强的含义同上。
 0xCn状态:程序改变(选择音色)——设置MIDI通道n中的乐器为指定的乐器种类(音色)。状态字节0xCn后跟的单个数据字节为音色/乐器种类。初始的乐器序号为0(三角钢琴)。
GM(General MIDI,通用MIDI)中定义了128种标准的音色/乐器(patch / timbre / voice),见表12-10。
表12-10 GM旋律乐器音色编号
分组 序号 英文名 中文名
Piano
================钢琴===============
0 Acoustic Grand Piano [非电声]三角钢琴
1 Bright Acoustic Piano 明亮[非电声]钢琴
2 Electric Grand Piano 电三角钢琴
3 Honky-Tonk Piano 酒吧钢琴
4 Electric Piano 1 (Rhodes) 电钢琴1(罗得斯/柔和)
5 Electric Piano 2 (Chorused) 电钢琴2(合唱电钢琴)
6 Harpsichord 大键琴(拨弦古钢琴)
7 Clavichord 翼琴(击弦古钢琴)
Acoustic
Percussion
=============[非电]打击乐器 ============
8 Celesta 钢片琴
9 Glockenspiel 钟琴
10 Music Box 八音盒
11 Vibraphone 颤音琴
12 Marimba 马林巴琴
13 Xylophone 木琴
14 Tubular Bells 管钟
15 Dulcimer 洋琴
Organ
==================风琴=================
16 Drawbar(Hammond) Organ 拉杆式(哈蒙德)管风琴
17 Percussive Organ 敲击式管风琴
18 Rock Organ 摇滚管风琴
19 Church Organ 教堂管风琴
20 Reed Organ 簧风琴
21 Accordion 手风琴
22 Harmonica 口琴
23 Tango Accordion 探戈口琴
Guitar
=================吉他=================
24 Nylon String Guitar 尼龙弦吉他
25 Steel String Guitar 钢弦吉他
26 Electric Jazz Guitar 电爵士吉他
27 Electric Clean Guitar 电清音吉他
28 Electric Muted Guitar 电闷音吉他
29 Overdriven Guitar 过激吉他
30 Distortion Guitar 失真吉他
31 Guitar Harmonics 吉他和声
Bass
===================贝司============
32 Acoustic Bass [非电声]贝司
33 Electric Bass(finger) 电贝司(指弹)
34 Electric Bass(pick) 电贝司(拨片)
35 Fretless Bass 无品贝司
36 Slap Bass 1 击弦贝司1
37 Slap Bass 2 击弦贝司2
38 Synth Bass 1 合成贝司1
39 Synth Bass 2 合成贝司2
Solo Strings
===============独奏弦乐器============
40 Violin 小提琴
41 Viola 中提琴
42 Cello 大提琴
43 Contrabass 低音大提琴
44 Tremolo Strings 颤音弦乐
45 Pizzicato Strings 弹拨弦乐
46 Orchestral Harp 竖琴
47 Timpani 定音鼓
Ensemble
=================合奏/合唱==============
48 String Ensemble 1 弦乐合奏1
49 String Ensemble 2 弦乐合奏2
50 Synth Strings 1 合成弦乐1
51 Synth Strings 2 合成弦乐2
52 Choir Aahs 合唱“啊”
53 Voice Oohs 人声“[u:]”
54 Synth Voice 合成人声
55 Orchestra Hit 管弦乐齐奏
Brass
===================铜管乐器==========
56 Trumpet 小号
57 Trombone 长号
58 Tuba 大号
59 Muted Trumpet 弱音小号
60 French Horn 法国号(圆号)
61 Brass Section 铜管乐合奏
62 SynthBrass 1 合成铜管乐1
63 SynthBrass 2 合成铜管乐2
Reed
============簧片乐器==============
64 Soprano Sax 高音萨克斯
65 Alto Sax 中音萨克斯
66 Tenor Sax 次中音萨克斯
67 Baritone Sax 低音萨克斯
68 Oboe 双簧管
69 English Horn 英国号
70 Bassoon 巴松(大管)
71 Clarinet 单簧管(黑管)
Pipe
============木管乐器==============
72 Piccolo 短笛
73 Flute 长笛
74 Recorder 竖笛
75 Pan Flute 排箫
76 Blown Bottle 吹瓶
77 Skakuhachi 日本笛
78 Whistle 口哨
79 Ocarina 洋埙
Synth Lead
===============合成导音================
80 Lead 1 (square) 导音1(方波)
81 Lead 2 (sawtooth) 导音2(锯齿波)
82 Lead 3 (calliope) 导音3(汽笛风琴)
83 Lead 4 (chiff) 导音4(夜莺)
84 Lead 5 (charang) 导音5(精灵)
85 Lead 6 (voice) 导音6(人声)
86 Lead 7 (fifths) 导音7(平行五度)
87 Lead 8 (bass+lead) 导音8(低音+导音)
Synth Pad
===================合成垫音=================
88 Pad 1 (new age) 垫音1(新时代)
89 Pad 2 (warm) 垫音2(温暖)
90 Pad 3 (polysynth) 垫音3(复音合成)
91 Pad 4 (choir) 垫音4(合唱)
92 Pad 5 (bowed) 垫音5(弓弦)
93 Pad 6 (metallic) 垫音6(金属)
94 Pad 7 (halo) 垫音7(光环)
95 Pad 8 (sweep) 垫音8(风声)
Synth Effects
================合成效果=================
96 FX 1 (rain) 效果1(雨声)
97 FX 2 (soundtrack) 效果2(音轨)
98 FX 3 (crystal) 效果3(水晶)
99 FX 4 (atmosphere) 效果4(大气)
100 FX 5 (brightness) 效果5(明亮)
101 FX 6 (goblins) 效果6(诡异)
102 FX 7 (echoes) 效果7(回声)
103 FX 8 (sci-fi) 效果8(科幻)
Ethnic
===================民族乐器==============
104 Sitar 西班达(印度)
105 Banjo 班卓琴(美洲)
106 Shamisen 三昧线(日本)
107 Koto 十三弦筝(日本)
108 Kalimba 卡林巴(非洲)
109 Bagpipe 风笛(苏格兰)
110 Fiddle 提琴
111 Shanai 山奈
Percussive
==================打击乐器================
112 Tinkle Bell 叮当铃
113 Agogo 摇摆舞铃
114 Steel Drums 钢鼓
115 Woodblock 木鱼
116 Taiko Drum 太科鼓
117 Melodic Tom 通通鼓
118 Synth Drum 合成鼓
119 Reverse Cymbal 铜钹
一Sound Eff2141265ects
===============声效=================
120 Guitar Fret Noise 吉他换把
121 Breath Noise 呼吸
122 Seashore 海浪
123 Bird Tweet 鸟鸣
124 Telephone Ring 电话铃
125 Helicopter 直升飞机
126 Applause 掌声
127 Gunshot 枪声

例如:
midi.bData[0] = 0xC0; // 设置通道0的音色/乐器种类
midi.bData[1] = 40; // 小提琴(缺省为0:三角钢琴)
midi.bData[2] = 0; // 未使用字节
midi.bData[3] = 0; // 保留为0
midiOutShortMsg(hmo, midi.dwData); // 设置通道0中的乐器为小提琴
12.5.4 虚拟电子琴实例
为了帮助大家理解MIDI消息的编程,本小节给出一个利用MIDI接口来模拟电子琴的简单VC实例的核心代码段。

 创建项目、修改配置
创建一个名为Midi的VC05的MFC单文档项目,在项目属性中添加对多媒体库winmm.lib链接。具体做法为:
在VS05中,选“项目/Midi属性”菜单项,打开项目的属性页窗口,先选“所有配置”,再选“配置属性/链接器/输入”项,在右边上部的“附加依赖项”栏的右边,键入winmm.lib(参见图12-7)后按“应用”钮,最后按“确定”钮关闭对话框。

图12-7 添加对多媒体库winmm.lib的链接
 包含多媒体头文件、添加类变量
在视图类CMidiView的头文件中,包含多媒体头文件,并添加若干类变量:
#include
#pragma once
class CMidiView : public CView {
……
protected:
UINT dn; // MIDI输出设备的数目
HMIDIOUT hmo; // MIDI输出设备的句柄
union { // MIDI消息联合
DWORD dwData;
BYTE bData[4];
} midi;
BYTE chann, patch, pitch, intens; // 通道、音色、音高、音强
DWORD vol; // [左右]音量
……
}

 初始化和清除
在视图类的构造函数中设置若干初值,如:
CMidiView::CMidiView() {
dn = 0;
chann = 0; // 通道0
patch = 40; // 小提琴
pitch = 60; // 中音1
intens = 127; // 最大音强
vol = 0xFFFFFFFFl; // 最大音量
}
在视图类的初始化函数OnInitialUpdate中,判断系统是否支持MIDI,并打开标准的MIDI输出设备(MIDI_MAPPER[=1]):
void CMidiView::OnInitialUpdate() {
CView::OnInitialUpdate();
dn = midiOutGetNumDevs(); // 获取MIDI设备数目
if (dn > 0) midiOutOpen(&hmo, MIDI_MAPPER, NULL, NULL, 0);
else MessageBox(L"No MIDI device!", L"Error"); // dn == 0
}
在视图类的析构函数中关闭MIDI输出设备:
CMidiView::~CMidiView() {
if (dn > 0) midiOutClose(hmo);
}

 设计对话框
可以自己设计若干对话框,让用户来选择MIDI通道、设置各通道的乐器种类和[左右]音量的大小等。

 添加键盘消息响应、播放/停止音符
为视图类CMidiView添加对Windows消息WM_KEYDOWN和WM_KEYUP的消息响应函数OnKeyDown和OnKeyUp。
在其中添加对用户按1~7键(对应的虚拟键值为0x31~0x37)的响应,并发送MIDI输出的短消息来播放乐音。设用户
 只按1~7键——播放中音1~7;
 在按1~7键的同时按住Shift键——播放高音 ;
 在按1~7键的同时按住Ctrl键——播放低音 ;
 在按1~7键的同时按住Caps Lock键——播放超高音 ;
 在按1~7键的同时按住左窗口键——播放超低音 。
在OnKeyDown函数中添加语句:
if (dn > 0) PlayStopPitch(0x90, nChar, nFlags); // play note
在OnKeyUp函数中添加语句:
if (dn > 0) PlayStopPitch(0x80, nChar, nFlags); // stop note
其中,PlayStopPitch是自己为视图类添加的成员函数:
void CMidiView::PlayStopPitch(BYTE status, UINT nChar, UINT nFlags) {
BYTE pitch0 = 60; // 初始音高(c1 = 中音1)
BYTE pitchs[7] = {0, 2, 4, 5, 7, 9, 11}; // 8度音阶(12个半音)
if (nChar >= 0x31 && nChar <= 0x37) { // paly or stop note
if (GetKeyState(VK_CAPITAL) & 1<<15) pitch0 += 24; // 高16度
else if (GetKeyState(VK_SHIFT) & 1<<15) pitch0 += 12; // 高8度
else if(GetKeyState(VK_CONTROL) & 1<<15) pitch0 -= 12; // 低8度
else if(GetKeyState(VK_LWIN) & 1<<15) pitch0 -= 24; // 低16度
midi.bData[0] = status + chann; // 播放/停止音符
midi.bData[1] = pitch0 + pitchs[nChar - 0x31]; // 音高
midi.bData[2] = intens; // 音强
midi.bData[3] = 0; // 保留为0
midiOutShortMsg(hmo, midi.dwData); // 发送短消息
}
}

 播放连续音阶
除了响应用户按键来播放音符外,还可以设计各种自动播放音符的方法,例如播放连续的音阶(音高从0到127)。
为此,可以给视图类添加类变量(循环音高)n和(老音高)pitch0,在某个菜单或按键的消息响应中,设置计时器来启动连续音阶的播放。如:
n = 0;
SetTimer(1, 1000, NULL);
并为视图类添加计时器消息的响应函数:
void CMidiView::OnTimer(UINT_PTR nIDEvent) {
BYTE pitchs[7] = {0, 2, 4, 5, 7, 9, 11};
if (n > 0) { // stop note
midi.bData[0] = 0x80;
midi.bData[1] = pitch0;
midi.bData[2] = 127;
midi.bData[3] = 0;
midiOutShortMsg(hmo, midi.dwData);
}
UINT pitch = 12 * (n / 7) + pitchs[n % 7];
if (pitch > 127) {KillTimer(1); return;} // stop timer
midi.bData[0] = 0x90; // play note
midi.bData[1] = pitch;
midi.bData[2] = 127;
midi.bData[3] = 0;
midiOutShortMsg(hmo, midi.dwData);
n++;
pitch0 = pitch;
CView::OnTimer(nIDEvent);
}