⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 22. 声音与音乐.txt

📁 本书介绍了在Microsoft Windows 98、Microsoft Windows NT 4.0和Windows NT 5.0下程序写作的方法
💻 TXT
📖 第 1 页 / 共 5 页
字号:
        
这种情况下,直到函数执行结束,也就是说,直到播放完「Born to Run」的前10秒,mciSendString函数才传回。

现在很明显,一般来说,在单线程的应用程序中这不是一件好事。如果不小心键入:

play cdaudio wait
        
直到整个唱片播放完以后,mciSendString函数才将控制权传回给程序。如果必须使用wait选项(在只要执行MCI描述文件而不管其它事情的时候,这么做很方便,与我将展示的一样),首先使用break命令。此命令可设定一个虚拟键码,此码将中断mciSendString命令并将控制权传回给程序。例如,要设定Escape键来实作此目的,可用:

break cdaudio on 27
        
这里,27是十进制的VK_ESCAPE值。

比wait选项更好的是notify选项:

play cdaudio from 5:0:0 to 5:0:10 notify
        
这种情况下,mciSendString函数立即传回,但如果该操作在MCI命令的尾部定义,则mciSendString函数的最后一个参数所指定句柄的窗口会收到MM_MCINOTIFY消息。TESTMCI程序在MM_MCINOTIFY框中显示此消息的结果。为避免与其它可能键入的命令混淆,TESTMCI程序在5秒后停止显示MM_MCINOTIFY消息的结果。

您可以同时使用wait和notify关键词,但没有理由这么做。不使用这两个关键词,内定的操作就既不是wait,也不是您通常所希望的notify。

用这些命令结束播放时,可键入下面的命令来停止CD:

stop cdaudio
        
如果在关闭之前没有停止CD-ROM设备,那么甚至在关闭设备之后还会继续播放CD。

另外,您还可以试试您的硬件允许或者不允许的一些命令:

eject cdaudio
        
最后按下面的方法关闭设备:

close cdaudio
        
虽然TESTMCI自己不能储存或加载文本文件,但可以在编辑控件和剪贴簿之间复制文字:先从TESTMCI选择一些内容,将其复制到剪贴簿(用Ctrl-C),再将这些文字从剪贴簿复制到「记事本」,然后储存。相反的操作,可以将一系列的MCI命令加载到TESTMCI。如果选择了一系列命令然后按下「OK」按钮(或者Enter键),则TESTMCI将每次执行一条命令。这就允许您编写MCI的「描述文件」,即MCI命令的简单列表。

例如,假设您想听歌曲「Jungleland」(唱片中的最后一首)、「Thunder Road」和「Born to Run」,并要按此顺序听,可以编写如下的描述命令:

open cdaudio
        
set cdaudio time format tmsf
        
break cdaudio on 27
        
play        cdaudio from 8 wait
        
play        cdaudio from 1 to 2 wait
        
play        cdaudio from 5 to 6 wait
        
stop        cdaudio
        
eject       cdaudio
        
close       cdaudio
        
不用wait关键词,就不能正常工作,因为mciSendString命令会立即传回,然后执行下一条命令。

此时,如何编写仿真CD播放程序的简单应用程序,就应该相当清楚了。程序可以确定乐曲数量、每个乐曲的长度并能显示允许使用者从任意位置开始播放(不过,请记住:mciSendString总是传回文字字符串信息,因此您需要编写解析处理程序来将这些字符串转换成数字)。可以肯定,这样的程序还要使用Windows定时器,以产生大约1秒的时间间隔。在WM_TIMER消息处理期间,程序将呼叫:

status cdaudio mode
        
来查看CD是暂停还是在播放。

status cdaudio position
        
命令允许程序更新显示以给使用者显示目前的位置。但可能还存在更令人感兴趣的事:如果程序知道音乐音调部分的节拍位置,那么就可以使屏幕上的图形与CD同步。这对于音乐指令或者建立自己的图形音乐视讯程序极为有用。

波形声音


波形声音是最常用的Windows多媒体特性。波形声音设备可以通过麦克风捕捉声音,并将其转换为数值,然后把它们储存到内存或者磁盘上的波形文件中,波形文件的扩展名是.WAV。这样,声音就可以播放了。

声音与波形


在接触波形声音API之前,具备一些预备知识很重要,这些知识包括物理学、听觉以及声音进出计算机的程序。

声音就是振动。当声音改变了鼓膜上空气的压力时,我们就感觉到了声音。麦克风可以感应这些振动,并且将它们转换为电流。同样,电流再经过放大器和扩音器,就又变成了声音。传统上,声音以模拟方式储存(例如录音磁带和唱片),这些振动储存在磁气脉冲或者轮廓凹槽中。当声音转换为电流时,就可以用随时间振动的波形来表示。振动最自然的形式可以用正弦波表示,它的一个周期如图5-5所示。

正弦波有两个参数-振幅(也就是一个周期中的最大振幅)和频率。我们已知振幅就是音量,频率就是音调。一般来说人耳可感受的正弦波的范围是从20Hz(每秒周期)的低频声音到20,000Hz的高频声,但随着年龄的增长,对高频声音的感受能力会逐年退化。

人感受频率的能力与频率是对数关系而不是线性关系。也就是说,我们感受20Hz到40Hz的频率变化与感受40Hz到80Hz的频率变化是一样的。在音乐中,这种加倍的频率定义为八度音阶。因此,人耳可感觉到大约10个八度音阶的声音。钢琴的范围是从27.5 Hz到4186 Hz之间,略小于7个八度音阶。

虽然正弦波代表了振动的大多数自然形式,但纯正弦波很少在现实生活中单独出现,而且,纯正弦波并不动听。大多数声音都很复杂。

任何周期的波形(即,一个循环波形)可以分解成多个正弦波,这些正弦波的频率都是整倍数。这就是所谓的Fourier级数,它以法国数学家和物理学家Jean Baptiste Joseph Fourier(1768-1830)的名字命名。周期的频率是基础。级数中其它正弦波的频率是基础频率的2倍、3倍、4倍(等等)。这些频率的声音称为泛音。基础频率也称作一级谐波。第一泛音是二级谐波,以此类推。

正弦波谐波的相对强度给每个周期的波形唯一的声音。这就是「音质」,它使得喇叭吹出喇叭声,钢琴弹出钢琴声。

人们一度认为电子合成乐器仅仅需要将声音分解成谐波并且与多个正弦波重组即可。不过,事实证明现实世界中的声音并不是这么简单。代表现实世界中声音的波形都没有严格的周期。乐器之间谐波的相对强度是不同的,并且谐波也随着每个音符的演奏时间改变。特别是乐器演奏音符的开始位置-我们称作起奏(attack)-相当复杂,但这个位置又对我们感受音质至关重要。

由于近年来数字储存能力的提高,我们可以将声音直接以数字形式储存而不用复杂的重组。

脉冲编码调制(Pulse Code Modulation)


计算机处理的是数值,因此要使声音进入计算机,就必须设计一种能将声音与数字信号相互转换的机制。

不压缩数据就完成此功能的最常用方法称作「脉冲编码调制」(PCM:pulse code modulation)。PCM可用在光盘、数字式录音磁带以及Windows中。脉冲编码调制其实只是一种概念上很简单的处理步骤的奇怪代名词而已。

利用脉冲编码调制,波形可以按固定的周期频率取样,其频率通常是每秒几万次。对于每个样本都测量其波形的振幅。完成将振幅转换成数字信号工作的硬件是模拟数字转换器(ADC:analog-to-digital converter)。类似地,通过数字模拟转换器(DAC:digital-to-analog converter)可将数字信号转换回波形电子信号。但这样转换得到的波形与输入的并不完全相同。合成的波形具有由高频组成的尖锐边缘。因此,播放硬件通常在数字模拟转换器后还包括一个低通滤波器。此滤波器滤掉高频,并使合成后的波形更平滑。在输入端,低通滤波器位于ADC前面。

脉冲编码调制有两个参数:取样频率,即每秒内测量波形振幅的次数;样本大小,即用于储存振幅级的位数。与您想象的一样:取样频率越高,样本大小越大,原始声音的复制品才更好。不过,存在一个提高取样频率和样本大小的极点,超过这个极点也就超过了人类分辨声音的极限。另外,如果取样频率和样本大小过低,将导致不能精确地复制音乐以及其它声音。

取样频率


取样频率决定声音可被数字化和储存的最大频率。尤其是,取样频率必须是样本声音最高频率的两倍。这就是「Nyquist频率(Nyquist Frequency)」,以30年代研究取样程序的工程师Harry Nyquist的名字命名。

以过低的取样频率对正弦波取样时,合成的波形比最初的波形频率更低。这就是所说的失真信号。为避免失真信号的发生,在输入端使用低通滤波器以阻止频率大于半个取样频率的所有波形。在输出端,数字模拟转换器产生的粗糙的波形边缘实际上是由频率大于半个取样频率的波形组成的泛音。因此,位于输出端的低通滤波器也阻止频率大于半个取样频率的所有波形。

声音CD中使用的取样频率是每秒44,100个样本,或者称为44.1kHz。这个特有的数值是这样产生的:

人耳可听到最高20kHz的声音,因此要拦截人能听到的整个声音范围,就需要40kHz的取样频率。然而,由于低通滤波器具有频率下滑效应,所以取样频率应该再高出大约百分之十才行。现在,取样频率就达到了44kHz。这时,我们要与视讯同时记录数字声音,于是取样频率就应该是美国、欧洲电视显示格速率的整数倍,这两种视讯格速率分别是30Hz和25Hz。这就使取样频率升高到了44.1kHz。

取样频率为44.1kHz的光盘会产生大量的数据,这对于一些应用程序来说实在是太多了,例如对于录制声音而不是录制音乐时就是这样。把取样频率减半到22.05 kHz,可由一个10 kHz的泛音来简化复制声音的上半部分。再将其减半到11.025 kHz就向我们提供了5 kHz频率范围。44.1 kHz、22.05 kHz和11.025 kHz的取样频率,以及8 kHz都是波形声音设备普遍支持的标准。

因为钢琴的最高频率为4186 Hz,所以您可能会认为给钢琴录音时,11.025 kHz的取样频率就足够了。但4186 Hz只是钢琴最高的基础频率而已,滤掉大于5000Hz的所有正弦波将减少可被复制的泛音,而这样将不能精确地捕捉和复制钢琴的声音。

样本大小


脉冲编码调制的第二个参数是按位计算的样本大小。样本大小决定了可供录制和播放的最低音与最高音之间的区别。这就是通常所说的动态范围。

声音强度是波形振幅的平方(即每个正弦波一个周期中最大振幅的合成)。与频率一样,人对声音强度的感受也呈对数变化。

两个声音在强度上的区别是以贝尔(以电话发明人Alexander Graham Bell的名字命名)和分贝(dB)为单位进行测量的。1贝尔在声音强度上呈10倍增加。1dB就是以相同的乘法步骤成为1贝尔的十分之一。由此,1dB可增加声音强度的1.26倍(10的10次方根),或者增加波形振幅的1.12倍(10的20次方根)。1分贝是耳朵可感觉出的声强的最小变化。从开始能听到的声音极限到让人感到疼痛的声音极限之间的声强差大约是100 dB。

可用下面的公式来计算两个声音间的动态范围,单位是分贝:


 



其中A1和A2是两个声音的振幅。因为只可能有一个振幅,所以样本大小是1位,动态范围是0。

如果样本大小是8位,则最大振幅与最小振幅之间的比例就是256。这样,动态范围就是:


 



或者48分贝。48的动态范围大约相当于非常安静的房屋与电动割草机之间的差别。将样本大小加倍到16位产生的动态范围是:


 



或者96分贝。这非常接近听觉极限和疼痛极限,而且人们认为这就是复制音乐的理想值。

Windows同时支持8位和16位的样本大小。储存8位的样本时,样本以无正负号字节处理,静音将储存为一个值为0x80的字符串。16位的样本以带正负号整数处理,这时静音将储存为一个值为0的字符串。

要计算未压缩声音所需的储存空间,可用以秒为单位的声音持续时间乘以取样频率。如果用16位样本而不是8位样本,则将其加倍,如果是录制立体声则再加倍。例如,1小时的CD声音(或者是在每个立体声样本占2字节、每秒44 ,100个样本的速度下进行3 600秒)需要635MB,这快要接近一张CD-ROM的储存量了。

在软件中产生正弦波


对于第一个关于波形声音的练习,我们不打算将声音储存到文件中或播放录制的声音。我们将使用低阶的波形声音API(即,前缀是waveOut的函数)来建立一个称作SINEWAVE的声音正弦波生成器。此程序以1 Hz的增量来生成从20Hz(人可感觉的最低值)到5,000Hz(与人感觉的最高值相差两个八度音阶)的正弦波。

我们知道,标准C执行时期链接库包括了一个sin函数,该函数传回一个弧度角的正弦值(2π弧度等于360度)。sin函数传回值的范围是从-1到1(早在第五章,我们就在SINEWAVE程序中使用过这个函数)。因此,应该很容易使用sin函数生成输出到波形声音硬件的正弦波数据。基本上是用代表波形(这时是正弦波)的数据来填充缓冲区,并将此缓冲区传递给API。(这比前面所讲的稍微有些复杂,但我将详细介绍)。波形声音硬件播放完缓冲区中的数据后,应将第二个缓冲区中的数据传递给它,并且以此类推。

第一次考虑这个问题(而且对PCM也一无所知)时,您大概会认为将一个周期的正弦波分成若干固定数量的样本-例如360个-才合理。对于20 Hz的正弦波,每秒输出7,200个样本。对于200 Hz的正弦波,每秒则要输出72,000个样本。这有可能实作,但实际上却不能这么做。对于5,000 Hz的正弦波,就需要每秒输出1,800,000个样本,这的确会增大DAC的负担!更重要的是,对于更高的频率,这种作法会比实际需要的精确度还高。

就脉冲编码调制而言,取样频率是个常数。假定取样频率是SINEWAVE程序中使用的11,025Hz。如果要生成一个2,756.25Hz(确切地说是四分之一的取样频率)的正弦波,则正弦波的每个周期就有4个样本。对于25Hz的正弦波,每个周期就有441个样本。通常,每周期的样本数等于取样频率除以要得到的正弦波频率。一旦知道了每周期的样本数,用2π弧度除以此数,然后用sin函数来获得每周期的样本。然后再反复对一个周期进行取样,从而建立一个连续的波形。

问题是每周期的样本数可能带有小数,因此在使用时这种方法并不是很好。每个周期的尾部都会有间断。

使它正常工作的关键是保留一个静态的「相位角」变数。此角初始化为0。第一个样本是0度正弦。随后,相位角增加一个值,该值等于2π乘以频率再除以取样频率。用此相位角作为第二个样本,并且按此方法继续。一旦相位角超过2π弧度,则减去2π弧度,而不要把相位角再初始化为0。

例如,假定要用11,025Hz的取样频率来生成1,000Hz的正弦波。即每周期有大约11个样本。为便于理解,此处相位角按度数给出-大约前一个半周期的相位角是:0、32.65、65.31、97.96、130.61、163.27、195.92、228.57、261.22、293.88、326.53、359.18、31.84、64.49、97.14、129.80、162.45、195.10,以此类推。存入缓冲区的波形数据是这些角度的正弦值,并已缩放到每样本的位数。为后来的缓冲区建立数据时,可继续增加最后的相位角,而不要将它初始化为0。

如程序22-2所示,FillBuffer函数完成这项工作-与SINEWAVE程序的其余部分一起完成。

程序22-2 SINEWAVE 
        
SINEWAVE.C
        
/*-------------------------------------------------------------------------
        
  SINEWAVE.C --         Multimedia Windows Sine Wave Generator
        
                                                                (c) Charles Petzold, 1998
        
--------------------------------------------------------------------------*/
        
#include <windows.h>
        
#include <math.h>
        
#include "resource.h"
        

#define            SAMPLE_RATE                   11025 
        
#define            FREQ_MIN                 20
        
#define            FREQ_MAX                     5000  
        
#define            FREQ_INIT                     440
        
#define            OUT_BUFFER_SIZE          4096
        
#define            PI                    3.14159
        

BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ;
        
TCHAR szAppName [] = TEXT ("SineWave") ;
        
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
        
                                                                PSTR szCmdLine, int iCmdShow)
        
{
        
           if (-1 == DialogBox (hInstance, szAppName, NULL, DlgProc))
        
    {
        
                  MessageBox (  NULL, TEXT ("This program requires Windows NT!"),
        
                                                                        szAppName, MB_ICONERROR) ;
        
    }
        
           return 0 ;
        
}
        

VOID FillBuffer (PBYTE pBuffer, int iFreq)
        
{
        
           static double         fAngle ;
        
           int                                  i ;
        

           for (i = 0 ; i < OUT_BUFFER_SIZE ; i++)
        
           {
        
                  pBuffer [i] = (BYTE) (127 + 127 * sin (fAngle)) ;
        
                  fAngle += 2 * PI * iFreq / SAMPLE_RATE ;
        
                  if (   fAngle > 2 * PI)
        
                                         fAngle -= 2 * PI ;
        
    }
        
}
        

BOOL CALLBACK DlgProc (    HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
        
{
        
           static BOOL                                         bShutOff, bClosing ;
        
           static HWAVEOUT                             hWaveOut ;
        
           static HWND                                         hwndScroll ;
        
           static int                                          iFreq = FREQ_INIT ;
        
           static PBYTE                                        pBuffer1, pBuffer2 ;
        
           static PWAVEHDR                             pWaveHdr1, pWaveHdr2 ;

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -