第22章 声音与音乐(1)

22.1 Windows和多媒体

22.1.1 Windows中支持多媒体功能的API

(1)底层接口:如波形音频输入、输出函数waveIn和waveOut前缀开头

               MIDI输出设备midiOut函数

(2)高层接口:

  ①以mci为前缀的7个函数。mci本身有两种,一种是向MCI发送消息。一种是向MCI发送文本字符串。

  ②MessageBeep和PlaySound等函数。

22.1.2 TESTMCI程序

 效果图

/*---------------------------------------------------------
   TESTMCI.C —— MCI Command String Tester
                 (c)Charles Petzold,1998
---------------------------------------------------------*/

#include <Windows.h>
#include "resource.h"

#pragma comment(lib,"WINMM.lib")  //须用到WINMM.DLL的导入表

#define ID_TIMER 1
BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);

TCHAR szAppName[] = TEXT("TestMci");

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;
}

BOOL CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HWND hwndEdit;
    int iCharBeg, iCharEnd,iLineBeg,iLineEnd,iLine,iLength,iChar;
    TCHAR szCommand[1024], szReturn[1024], szError[1024],szBuffer[24];
    MCIERROR error;
    RECT rect;

    switch (message)
    {
    case WM_INITDIALOG:
        //将窗口置于屏幕中心
        GetWindowRect(hwnd, &rect);
        SetWindowPos(hwnd, NULL,
            (GetSystemMetrics(SM_CXSCREEN) - rect.right + rect.left) / 2,
            (GetSystemMetrics(SM_CYSCREEN) - rect.bottom + rect.top) / 2,
            0, 0, SWP_NOZORDER | SWP_NOSIZE);

        hwndEdit = GetDlgItem(hwnd, IDC_MAIN_EDIT);
        SetFocus(hwndEdit);
        return FALSE;  //手动设置焦点,应返回FALSE。否则会设在第1个拥有WS_TABSTOP控件上

    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDOK:
            //计算文本框中被选中的开始和结束的行数
            SendMessage(hwndEdit, EM_GETSEL, (WPARAM)&iCharBeg, (LPARAM)&iCharEnd);//开始位置和结束位置

            iLineBeg = SendMessage(hwndEdit, EM_LINEFROMCHAR, iCharBeg, 0);
            iLineEnd = SendMessage(hwndEdit, EM_LINEFROMCHAR, iCharEnd, 0);
            
            //遍历所有选中的行(如果没有选中的,则默认会读取最后一行文本)
            for (iLine = iLineBeg; iLine<=iLineEnd; iLine++)
            {
                //每次循环获取第iLine行文本,并在最末尾加上结束,如果是空行,则跳过。

                //缓冲区第1个字为缓冲区字符个数(含),EM_GETLINE消息要求的
                *(WORD*)szCommand = sizeof(szCommand) / sizeof(TCHAR); 
                iLength = SendMessage(hwndEdit, EM_GETLINE, iLine, (LPARAM)szCommand);
                szCommand[iLength] = '';

                if (iLength == 0) continue; //跳过空行

                //发送MCI命令
                /*
                MCIERROR mciSendString(// 可用mciGetErrorString获得错误的文本描述            
                                LPCTSTR lpszCommand, // 指向以null结尾的命令字符串:”命令 设备[ 参数]”
                                LPTSTR lpszReturnString,// 指向接收返回信息的缓冲区,为NULL时不返回信息
                                UINT cchReturn, // 上述缓冲区的大小
                                HANDLE hwndCallback);//在命令串中含notify时,它指定一个回调窗口的句柄,一般为NULL

                */
                error = mciSendString(szCommand, szReturn, sizeof(szReturn) / sizeof(TCHAR), hwnd);

                //显示返回的消息文本
                SetDlgItemText(hwnd, IDC_RETURN_STRING, szReturn);

                //显示错误信息
                mciGetErrorString(error, szError, sizeof(szError) / sizeof(TCHAR));
                SetDlgItemText(hwnd, IDC_ERROR_STRING, szError);
            }

            //将插入符(Caret)放在选定行中最后一行的后面
            iChar = SendMessage(hwndEdit, EM_LINEINDEX, iLineEnd, 0); //EM_LINEINDEX获取第iLineEnd行
                                                                      //第1个字符的位置索引

            iChar += SendMessage(hwndEdit, EM_LINELENGTH, iCharEnd, 0); //获取指定位置的字符(iCharEnd)所在行的字符个数
                                                                        //iChar指定该行最后一个字符的后面。
            SendMessage(hwndEdit, EM_SETSEL, iChar, iChar); //选择该行最后一个字符

            //插入回车换行符
            SendMessage(hwndEdit, EM_REPLACESEL, FALSE, (LPARAM)TEXT("
"));

            SetFocus(hwndEdit);
            return TRUE;

        case IDCANCEL:
            EndDialog(hwnd, 0);
            return TRUE;
            
        case IDC_MAIN_EDIT: //编辑框消息
            if (HIWORD(wParam) == EN_ERRSPACE) //请求的空间无法得到满足时
            {
                MessageBox(hwnd, TEXT("Error control out of space."),
                    szAppName, MB_OK | MB_ICONINFORMATION);
                return TRUE;
            }
            break;
        }
        break;

        /*
         lParam = (LONG) lDevID;//
         wParam = (WPARAM) wFlags:
         1、MCI_NOTIFY_ABORTED      2、MCI_NOTIFY_FAILURE
         3、MCI_NOTIFY_SUCCESSFUL   4、MCI_NOTIFY_SUPERSEDED
        */
    case MM_MCINOTIFY:
        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_MESSAGE), TRUE);
        wsprintf(szBuffer, TEXT("Device ID = %i"), lParam);
        SetDlgItemText(hwnd, IDC_NOTIFY_ID, szBuffer);
        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_ID), TRUE);

        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_SUCCESSFUL), wParam & MCI_NOTIFY_SUCCESSFUL);
        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_SUPERSEDED), wParam & MCI_NOTIFY_SUPERSEDED);
        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_ABORTED), wParam & MCI_NOTIFY_ABORTED);
        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_FAILURE), wParam & MCI_NOTIFY_FAILURE);

        SetTimer(hwnd, ID_TIMER, 5000, NULL);
        return TRUE;

    case WM_TIMER:
        KillTimer(hwnd, ID_TIMER);
        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_MESSAGE), FALSE);
        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_ID), FALSE);
        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_SUCCESSFUL), FALSE);
        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_SUPERSEDED), FALSE);
        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_ABORTED), FALSE);
        EnableWindow(GetDlgItem(hwnd, IDC_NOTIFY_FAILURE), FALSE);
        return TRUE;

    case WM_SYSCOMMAND:
        switch (LOWORD(wParam))
        {
        case SC_CLOSE:
            EndDialog(hwnd, 0);
            return TRUE;
        }
        break;
    }
    return FALSE;
}

//resource.h

//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 TestMci.rc 使用
//
#define IDC_MAIN_EDIT                   1001
#define IDC_NOTIFY_ID                   1002
#define IDC_NOTIFY_MESSAGE              1003
#define IDC_NOTIFY_SUCCESSFUL           1004
#define IDC_NOTIFY_ABORTED              1005
#define IDC_NOTIFY_FAILURE              1006
#define IDC_NOTIFY_SUPERSEDED           1007
#define IDC_ERROR_STRING                1008
#define IDC_RETURN_STRING               1009

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        102
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1006
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif

//TestMci.rc

// Microsoft Visual C++ generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// 中文(简体,中国) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED

#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE 
BEGIN
    "resource.h"
END

2 TEXTINCLUDE 
BEGIN
    "#include ""winres.h""
"
    ""
END

3 TEXTINCLUDE 
BEGIN
    "
"
    ""
END

#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

TESTMCI DIALOGEX 0, 0, 279, 290
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "MCI Tester"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    DEFPUSHBUTTON   "OK",IDOK,61,266,50,14
    PUSHBUTTON      "Close",IDCANCEL,146,266,50,14
    EDITTEXT        IDC_MAIN_EDIT,16,15,248,101,ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL
    LTEXT           "Return String:",IDC_STATIC,19,118,46,8
    EDITTEXT        IDC_ERROR_STRING,144,130,120,67,ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_READONLY | WS_VSCROLL | WS_GROUP | NOT WS_TABSTOP
    LTEXT           "Error String:",IDC_STATIC,145,119,40,8
    EDITTEXT        IDC_RETURN_STRING,18,129,120,66,ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_READONLY | WS_VSCROLL | WS_GROUP | NOT WS_TABSTOP
    GROUPBOX        "MM_MCINOTIFY message",IDC_STATIC,18,199,247,62
    LTEXT           "",IDC_NOTIFY_ID,30,214,228,8
    LTEXT           "MCI_NOTIFY_SUCCESSFUL",IDC_NOTIFY_SUCCESSFUL,34,230,88,8,WS_DISABLED
    LTEXT           "MCI_NOTIFY_ABORTED",IDC_NOTIFY_ABORTED,163,230,78,8,WS_DISABLED
    LTEXT           "MCI_NOTIFY_FAILURE",IDC_NOTIFY_FAILURE,163,244,74,8,WS_DISABLED
    LTEXT           "MCI_NOTIFY_SUPERSEDED",IDC_NOTIFY_SUPERSEDED,34,244,89,8,WS_DISABLED
END


/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//

#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO
BEGIN
    "TESTMCI", DIALOG
    BEGIN
        LEFTMARGIN, 7
        RIGHTMARGIN, 272
        TOPMARGIN, 7
        BOTTOMMARGIN, 283
    END
END
#endif    // APSTUDIO_INVOKED

#endif    // 中文(简体,中国) resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED

22.2 波形音频

22.2.1 声音与波形

(1)正弦波

   振幅:指声音强度

   频率:指音调(人类耳朵从低音调到高音调的区间范围为20Hz~20 000Hz)

(2)人类对频率的感知是和频率的对数成正比的——频率翻倍定义为八度

(3)傅里叶级数和正弦波谐波:任何周期性的波形,都可以分解为多个正弦波,它们的频率关系是整数倍数。基波的频率代表正弦波的频率,基波也被称为一次谐波,还有二次、三次等谐波。

22.2.2 采样率和采样的大小

(1)采样率:指每秒对波形采集的样本数量,必须是声音最高频率的2倍。这被称为“奈奎斯将频率”。如人耳能听到高达20kHz的声音,为了获取人类可以听到的整个音频范围,采样率为40kHz(是声音最高频率的2倍),但考虑到经过低通滤波后有衰减效应,所以提高10%左右,即44kHz。一般取44.1kHz。

(2)采样大小:单位是位(bit)——用于表示声音的强度,决定了最小的声音和最大声音之间的差别。

以分贝来表示——dB = 20*log(A1/A2),其中A1和A2是两个声音的振幅。

  ①采样大小为1位,即A1/A2 =1,则动态范围dB=0,因为只有一个振幅。

  ②采样大小为8位,即A1/A2 =256,动态范围dB= 20*log(256),即48分贝。其范围相当于一个安静的房间与一台运行着的电动机之间的区别。

  ③采样大小为16位时,产生的动态范围为dB=20*log(65536),即96分贝,这个范围非常接近听力最低阈值和产生痛感阈值之间的区别。注意,立体声是双声道的,每个声道采样大小为16位

(3)计算未压缩音频所需占用的空间:声音长度*采样率*采样大小/8 *声道数 (单位:字节)

22.2.3 用软件生成正弦波——WaveOutXXX波形输出设备函数的使用

22.2.3.1 程序结构

(1)本程序生成20Hz~5000Hz的正弦波

(2)在脉冲编码时,采样率是一个常数,本例采用11025Hz,设要生成的正弦波的频率为X,则正弦波的每个周期内采集的样本数量为11025/x。由于每个周期的样本数量可能是小数,这样每个周期末尾都不连续。所以不能用此方法来生成正弦波。

(3)正确的方法:——相位角。第1个样本为0度,递增值为正弦波频率*2π/采样率。如用11025Hz的采样率生成1000Hz的正弦波,则每个周期大约11个样本,用这11个样本的正弦值来表示每个样本的位数(振幅)。

22.2.3.2程序用到函数或结构体

(1)WAVEFORMATEX结构体

字段

含义

WORD  wFormatTag

指定格式类型; 默认 WAVE_FORMAT_PCM = 1

WORD  nChannels

指出波形数据的通道数; 单声道为 1, 立体声为 2

DWORD nSamplesPerSec

指定样本采样率(每秒的样本数)

DWORD nAvgBytesPerSec

指定数据传输的平均速率(每秒的字节数)

PCM编码:其值等于nSamplesPerSec*nBlockAlign

WORD  nBlockAlign

指定块对齐, 块对齐是数据的最小单位。

PCM编码:其值等于nChannels*wBitsPerSample/8(单位字节),即每个样本占用的字节总数,16 位立体声 PCM 的块对齐是 4 字节(每个样本2字节, 2个通道)

WORD  wBitsPerSample

采样大小(字节),等于8位或16位

WORD  cbSize

附加信息大小; PCM 格式没这个字段,可设为0。(PCM:脉冲编码调制)

(2)waveOutOpen函数介绍

参数

含义

LPHWAVEOUT phwo

指向接收波形音频输出装置的句柄。如果fdwFlags设定为WAVE_FORMAT_QUERY时,该参数可为NULL

UINT uDeviceID

①将要被打开的波形音频输出装置的ID,该值是个索引值,范围从0至系统中波形输出设备的数量减1.

②波形输出设备数量可用WaveOutGetNumDevs查询

如果该值设为WAVE_MAPPER(-1),则会自动选择,一般会选择控制面板的多媒体程序的【音频】选项卡中【首选设备】中指定的设备,如果无法满足需求,会自动选择另一个设备。

LPWAVEFORMATEX pwfx

指向一个WAVEFORMATEX结构体的指针

DWORD dwCallback

指明接收波形输出消息的窗口句柄或回调函数。注意:回调函数是在中断时间内访问的, 必须在 DLL 中。要访问的数据都必须是在固定的数据段中。

DWORD dwCallbackInstance

如果dwCallBack指明了使用回调函数,则该参数可以指定要传递给回调函数额外的数据。

DWORD fdwFlags

CALLBACK_WINDOW、CALLBACK_FUNCTION:指定第4个参数的类型。

CALLBACK_EVENT:指定dwCallback为事件句柄

CALLBACK_THREAD:指定dwCallback为线程ID

WAVE_ALLOWSYNC:当是同步设备时必须指定

WAVE_FORMAT_QUERY:检查设备是否可以打开,而不实际去打开它。

【返回值】

  ①MMSYSERR_NOERROR     = 0;  {成功时}

  ②MMSYSERR_BADDEVICEID = 2;  {设备ID超界}

  ③MMSYSERR_ALLOCATED   = 4;  {指定的资源已被分配}

  ④MMSYSERR_NODRIVER    = 6;  {没有安装驱动程序}

  ⑤MMSYSERR_NOMEM       = 7;  {不能分配或锁定内存}

  ⑥WAVERR_BADFORMAT     = 32; {设备不支持请求的波形格式}

说明:

  ①函数调用后立即返回

  ②如果选择窗口句柄为接收波形输出消息,则会收到WM_WOM_OPEN消息,wParam为波形输出设备句柄,lParam为保留值(无用)

(3)WaveOutPrepareHeader函数及WAVEHDR结构体

  ①WaveOutPrepareHeader函数:准备波形数据,可避免WAVEHDR结构体和缓冲区从内存被交换到磁盘去。

  ②WAVEHDR结构体

字段

含义

LPSTR  lpData;

存储声音数据的缓冲区地址,要在堆里的,不能是函数的局部变量,MM_WOM_DONE消息的处理函数中还要用到。

DWORD  dwBufferLength

上述缓冲区的大小

DWORD  dwBytesRecorded

当设备用于录音时,标志已经录入的数据长度

DWORD  dwUser

给应用程序用的

DWORD  dwFlags;

波形数据的缓冲区的属性

WHDR_BEGINLOOP:指明该缓冲区是每次循环的第一个缓冲区。

WHDR_ENDLOOP:指明该缓冲区是每次循环的最后一个缓冲区。

WHDR_DONE:指明缓冲区数据播放完毕后,被设备驱动程序设为该值。

DWORD  dwLoops

循环播放的次数

struct wavehdr_tag * lpNext

保留值

DWORD  reserved

保留值

(4)waveOutWrite函数——向指定的波输形输出设备传入数据,真正开始播放声音

  播放完后发送一条:MM_WOM_DONE消息,wParam为波形输出设备的句柄,lParam为指向WAVEHDR结构的指针。

(5)waveOutReset函数——停止声音播放,然后将当前位置置0,所有等待播放的数据被标志为Done,并发送MM_WOM_DONE消息

  ①waveOutReset不是立即返回的函数,要等待驻留在波形输出设备中的数据处理完才返回。

  ②在调用到返回之间的这段时间内,禁止任何线程对处理reset中的hWaveOut调用,否则会造成死锁。

(6)waveOutClose函数——发送MM_WOM_CLOSE消息

(7)waveOutUnprepareHeader——清除由 waveOutPrepareHeader 完成的准备。

//SineWave.c

#include <Windows.h>
#include <math.h>
#include "resource.h"

#pragma comment(lib,"WINMM.lib")

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

TCHAR szAppName[] = TEXT("SineWave");

BOOL CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); 

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR szCmdLine, int nCmdShow)
{
    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;

    //用相位角来生成正弦波声音数据,声音强度(振幅)从0-254
    for  (i= 0; i<OUT_BUFFER_SIZE; i++)
    {
        pBuffer[i] = (BYTE)(127+127*sin(fAngle));

        //相位增量为:正弦波频率*2π/采样率
        fAngle += 2 * PI * iFreq / SAMPLE_RATE;

        if (fAngle>2 * PI)
            fAngle -= 2 * PI; //注意这里不能重置为0,因为要让声音连续。
    }
}

BOOL CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static BOOL bShutOff, bClosing;
    static HWND hwndScroll;
    static int iFreq = FREQ_INIT;
    static PBYTE  pBuffer1, pBuffer2; //用双缓冲播放声音
    static PWAVEHDR pWaveHdr1, pWaveHdr2;
    static WAVEFORMATEX waveformat;
    int iDummy;

    static HWAVEOUT hWaveOut;

    RECT  rect;
    switch (message)
    {
    case WM_INITDIALOG:

        //将窗口置于屏幕中央
        GetWindowRect(hwnd, &rect);
        SetWindowPos(hwnd, NULL, (GetSystemMetrics(SM_CXSCREEN) - rect.right + rect.left) / 2, 
                                 (GetSystemMetrics(SM_CYSCREEN) - rect.bottom + rect.top) / 2,
                                 rect.right - rect.left, rect.bottom - rect.top, SWP_SHOWWINDOW);

        hwndScroll = GetDlgItem(hwnd, IDC_SCROLL);
        SetScrollRange(hwndScroll, SB_CTL, FREQ_MIN, FREQ_MAX, FALSE);
        SetScrollPos(hwndScroll, SB_CTL, FREQ_INIT, TRUE);
        SetDlgItemInt(hwnd, IDC_TEXT, FREQ_INIT, FALSE);

        //SendMessage(hwnd, DM_SETDEFID, (WPARAM)IDC_SCROLL, 0); //测试用的,改变对话框回车键默认行为
        return TRUE;

    case WM_HSCROLL:
        switch (LOWORD(wParam))
        {
        case SB_LINELEFT:       iFreq -= 1; break;
        case SB_LINERIGHT:        iFreq += 1; break;
        case SB_PAGELEFT:       iFreq /= 2; break;//降低1个八度
        case SB_PAGERIGHT:      iFreq *= 2; break;//升高1个八度
        case SB_THUMBTRACK:     iFreq = HIWORD(wParam);break;
        case SB_TOP:       
            GetScrollRange(hwndScroll, SB_CTL, &iFreq, &iDummy); //将最小值放入iFreq,最大放入iDummy
            break;
        case SB_BOTTOM:
            GetScrollRange(hwndScroll, SB_CTL, &iDummy, &iFreq); //将最大值放入iFreq
            break;
        }

        iFreq = max(FREQ_MIN, min(FREQ_MAX, iFreq));

        SetScrollPos(hwndScroll, SB_CTL, iFreq, TRUE);
        SetDlgItemInt(hwnd, IDC_TEXT, iFreq, FALSE);
        
        return TRUE;

    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case IDC_ONOFF:

            //如果是要打开波形音频,则hWaveOut==NULL
            if (hWaveOut == NULL)
            {
                //为2个声音头结构和声音数据分配内存
                pWaveHdr1 = malloc(sizeof(WAVEHDR));
                pWaveHdr2 = malloc(sizeof(WAVEHDR));
                pBuffer1  = malloc(OUT_BUFFER_SIZE);
                pBuffer2  = malloc(OUT_BUFFER_SIZE);

                if (!pWaveHdr1 || !pWaveHdr2 || !pBuffer1 ||!pBuffer2)
                {
                    if (!pWaveHdr1) free(pWaveHdr1);
                    if (!pWaveHdr2) free(pWaveHdr2);
                    if (!pBuffer1)  free(pBuffer1);
                    if (!pBuffer2)  free(pBuffer2);

                    MessageBeep(MB_ICONEXCLAMATION);
                    MessageBox(hwnd, TEXT("Error allocating memory!"),
                                szAppName, MB_ICONEXCLAMATION | MB_OK);
                    return TRUE;
                }

                //关闭按钮按下标志
                bShutOff = FALSE;

                //打开波形输出设备
                waveformat.wFormatTag        = WAVE_FORMAT_PCM;
                waveformat.nChannels        = 1;//单声道
                waveformat.nSamplesPerSec   = SAMPLE_RATE;
                waveformat.nAvgBytesPerSec    = SAMPLE_RATE;//nSamplesPerSec*nBlockAlign
                waveformat.nBlockAlign        = 1; //每个样本所占的字节数,nChannels*wBitsPerSample/8
                waveformat.wBitsPerSample    = 8; //每个样本大小(位数)(单位:位)
                waveformat.cbSize            = 0;

                //自动选择波形输出设备,并打开。消息发送指定的窗口
                if (MMSYSERR_NOERROR != waveOutOpen(&hWaveOut , WAVE_MAPPER , &waveformat,
                                                    (DWORD)hwnd, 0, CALLBACK_WINDOW ))
                {
                    free(pWaveHdr1);
                    free(pWaveHdr2);
                    free(pBuffer1);
                    free(pBuffer2);

                    hWaveOut = NULL;
                    MessageBeep(MB_ICONEXCLAMATION);
                    MessageBox(hwnd, TEXT("Error opening waveform audio device!"),
                        szAppName, MB_ICONEXCLAMATION | MB_OK);
                    return TRUE;
                }

                //创建波形头结构并做好输出准备
                pWaveHdr1->lpData            = pBuffer1;       //指向声音数据
                pWaveHdr1->dwBufferLength    = OUT_BUFFER_SIZE;//声音数据的大小
                pWaveHdr1->dwBytesRecorded  = 0;
                pWaveHdr1->dwUser            = 0;
                pWaveHdr1->dwFlags            = 0;
                pWaveHdr1->dwLoops            = 1;
                pWaveHdr1->lpNext            = NULL;
                pWaveHdr1->reserved            = 0;

                //防止WAVEHDR和缓冲区从内存被交换到磁盘去
                waveOutPrepareHeader(hWaveOut, pWaveHdr1, sizeof(WAVEHDR));

                pWaveHdr2->lpData            = pBuffer2;       //指向声音数据
                pWaveHdr2->dwBufferLength    = OUT_BUFFER_SIZE;//声音数据的大小
                pWaveHdr2->dwBytesRecorded    = 0;
                pWaveHdr2->dwUser            = 0;
                pWaveHdr2->dwFlags            = 0;
                pWaveHdr2->dwLoops            = 1;
                pWaveHdr2->lpNext            = NULL;
                pWaveHdr2->reserved            = 0;

                //防止WAVEHDR和缓冲区从内存被交换到磁盘去
                waveOutPrepareHeader(hWaveOut, pWaveHdr2, sizeof(WAVEHDR));
            }
            else  //如果要关闭波形文件,则输出设备复位
            {
                bShutOff = TRUE;
                waveOutReset(hWaveOut);//停止播放
            }
            return TRUE;
        }
        break;

    case MM_WOM_OPEN: //调用waveOutOpen函数后,收到该消息
        SetDlgItemText(hwnd, IDC_ONOFF, TEXT("Turn Off"));

        //将两个缓冲区的数据写入波形输出设备中
        FillBuffer(pBuffer1, iFreq);
        waveOutWrite(hWaveOut, pWaveHdr1, sizeof(WAVEHDR)); //真正的播放开始

        FillBuffer(pBuffer2, iFreq);  //双缓冲技术,可以保证发出的声音是连续的。
        waveOutWrite(hWaveOut, pWaveHdr2, sizeof(WAVEHDR));
        return TRUE;

    case MM_WOM_DONE:  //当缓冲区数据播放完毕或waveOutReset时,收到该消息
        if (bShutOff)
        {
            waveOutClose(hWaveOut); //按下Turn Off按钮
            return TRUE;
        }

        //如果缓冲区的数据播放完毕,则
        //生成新的声音数据,并发送给波输形输出设备
        FillBuffer(((PWAVEHDR)lParam)->lpData, iFreq);
        waveOutWrite(hWaveOut, (PWAVEHDR)lParam, sizeof(WAVEHDR));
        return TRUE;

    case MM_WOM_CLOSE:
        waveOutUnprepareHeader(hWaveOut, pWaveHdr1, sizeof(WAVEHDR));
        waveOutUnprepareHeader(hWaveOut, pWaveHdr2, sizeof(WAVEHDR));

        if (pWaveHdr1) free(pWaveHdr1);
        if (pWaveHdr2) free(pWaveHdr2);
        if (pBuffer1)  free(pBuffer1);
        if (pBuffer2)  free(pBuffer2);

        hWaveOut = NULL;
        SetDlgItemText(hwnd, IDC_ONOFF, TEXT("Turn On"));

        if (bClosing)
            EndDialog(hwnd, 0);

        return TRUE;

    case WM_SYSCOMMAND:
        switch (wParam)
        {
        case SC_CLOSE:
            if (hWaveOut !=NULL)
            {
                bShutOff = TRUE;
                bClosing = TRUE;

                waveOutReset(hWaveOut);
            }
            else
            EndDialog(hwnd, 0);
            return TRUE;
        }
        break;
    }

    return FALSE;
}

//resource.h

//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 SineWave.rc 使用
//
#define IDC_SCROLL                      1001
#define IDC_TEXT                        1002
#define IDC_ONOFF                       1003

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        102
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1004
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif

//SineWave.rc

// Microsoft Visual C++ generated resource script.
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// 中文(简体,中国) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED

#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE 
BEGIN
    "resource.h"
END

2 TEXTINCLUDE 
BEGIN
    "#include ""winres.h""
"
    ""
END

3 TEXTINCLUDE 
BEGIN
    "
"
    ""
END

#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

SINEWAVE DIALOGEX 0, 0, 247, 62
STYLE DS_SETFONT | DS_FIXEDSYS | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Sine Wave Generator"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    DEFPUSHBUTTON   "Turn On",IDC_ONOFF,106,44,50,14
    SCROLLBAR       IDC_SCROLL,15,12,189,11,WS_TABSTOP
    RTEXT           "440",IDC_TEXT,202,13,25,8
    LTEXT           "Hz",IDC_STATIC,229,13,9,8,WS_TABSTOP
END


/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//

#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO
BEGIN
    "SINEWAVE", DIALOG
    BEGIN
    END
END
#endif    // APSTUDIO_INVOKED

#endif    // 中文(简体,中国) resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED
原文地址:https://www.cnblogs.com/5iedu/p/4715193.html