码迷,mamicode.com
首页 > 其他好文 > 详细

音频处理(二) 音频输出

时间:2017-09-16 13:37:17      阅读:207      评论:0      收藏:0      [点我收藏+]

标签:const   com   unp   erro   开发者   log   bre   专用   device   

Windows下的音频输出常用的3中方法:

1. PlaySound:使用最简单直接,但是不够灵活,功能也非常单一,无法混音;

2. WaveOut:早期的Windows系统中广泛应用的音频输出程序接口,功能比PlaySound较完善(WaveIn用于音频输入);

3. DirectSound:现在Windows中主流的应用于音频输入输出的API,支持混音、独立音量控制、硬件加速、硬件仿真等强大的功能;

 

PlaySound

   PlaySound的使用非常简单,下面是一个示例 (vs2013 Project):

#include "stdafx.h"
#include <Windows.h>
#include <mmsystem.h>

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

const char fPath1[] = "C:/Windows/Media/Ring09.wav\0\0";

// 同步播放
void PlaySync()
{
    printf("Sync Start...\n");
    PlaySoundA((char*)fPath1, NULL, SND_FILENAME|SND_SYNC);
    printf("Sync Complete!\n");
}

// 异步播放
void PlayAsync()
{
    printf("Async Start...\n");
    PlaySoundA((char*)fPath1, NULL, SND_FILENAME|SND_ASYNC);
    printf("Async Complete!\n");
}

int main(int argc, char* argv[])
{
    PlaySync();
    //PlayAsync();

    Sleep(3000);
    return 0;
}

    使用PlaySound方法之前须添加mmsystem.h和Windows.h两个头文件,并将winmm.lib链接库添加到工程,这里是用#pragma添加的,上面使用了同步和异步两种方式播放一段wav音频;

    它们的区别是,同步方式下,PlaySound方法调用时会阻塞程序,直到这段音频播放结束,再返回往下继续执行;而异步方式下,调用PlaySound方法会立即返回,音频播放的同时,程序依然正常往下执行,这种情况下,上例如果没有Sleep方法休眠主线程,那么程序会直接结束,导致听不到完整的音乐。

    另外,PlaySound无法同时播放两个音频,当重复调用PlaySound方法时,已经在播放的音频会中断,转而播放新的那段音频;又或者第二次调用PlaySound会失败返回FALSE,而已经播放的音频不受影响,这一切要看PlaySound的第三个参数的设定;比如上例中,如果重复调用,已经在播放的音频会中断,转而播放新的音频;而如果加上SND_NOSTOP,那么重复调用播放同一段音频时,之前的音频不会中断,而再次调用的PlaySound会失败返回FALSE;

    SND_FILENAME表示采用文件名的方式加载音频资源,除此之外还有引用内存中已有音频资源的方式 (详见官方文档);

WaveOut

     使用WaveOut进行音频输出大致分为几个步骤:创建缓冲区 — 读取音频文件 — 复制数据到缓冲区 — 播放,用一张图来说明音频播放的大致流程:

                               技术分享

    首先是创建缓存区,这里要用到官方定义的一个结构 WAVEHDR,这个结构是专用来进行音频数据缓存块和设备之间的桥梁,这里通过它可以提交音频数据给设备;

它的结构定义如下:

/* wave data block header */
typedef struct wavehdr_tag {
    LPSTR       lpData;                 /* 指向数据区起始地址 */
    DWORD       dwBufferLength;         /* 数据缓存大小(Byte) */
    DWORD       dwBytesRecorded;        /* used for input only */
    DWORD_PTR   dwUser;                 /* 开发者自由使用 */
    DWORD       dwFlags;                /* 标志位 */
    DWORD       dwLoops;                /* 循环计数器 */
    struct wavehdr_tag FAR *lpNext;     /* reserved for driver */
    DWORD_PTR   reserved;               /* reserved for driver */
} WAVEHDR, *PWAVEHDR, NEAR *NPWAVEHDR, FAR *LPWAVEHDR;

    一般我们会创建3个以上的缓存块,循环地把数据写入、并按顺序提交给设备,那么设备就会按提交的顺序不断地播放已提交的那些缓存块音频数据,如上图;CPU控制硬盘读入数据,并复制到缓存区,然后按顺序提交一个个缓存块,播放完了的缓存块,可以继续读入再提交,循环往复;

之所以选择3个以上缓存块,是为了避免播放间隙时间的产生,微小的间隙在音频播放中,观感是难以忍受的;

创建代码:

#define BlockSize   1024*10    // 数据块缓存大小(这个案例中必须是 BufferSize 的整数倍)
#define BlockCount  12         // 数据块个数(不限,建议3个以上)

WAVEHDR* Blocks = NULL;

//
// 创建缓存区
// WAVEHDR* CreateBlocks() { unsigned char* buffer; DWORD totalBufferSize = (BlockSize + sizeof(WAVEHDR))*BlockCount; // 申请的内存空间 = 块结构内存 + 缓存空间 if ((buffer = (UCHAR*)malloc(totalBufferSize)) == NULL) { printf("Memory Malloc Error!\n"); return NULL; } memset(buffer, 0, totalBufferSize); Blocks = (WAVEHDR*)buffer; buffer += sizeof(WAVEHDR) * BlockCount; for (int i = 0; i < BlockCount; i++) { Blocks[i].dwBufferLength = BlockSize; Blocks[i].lpData = (char*)buffer; buffer += BlockSize; } return Blocks; }

缓存块创建完成,然后可以读入wav音频数据了,首先读入wav头结构,分析音频信息,头结构的分析直接引用上一篇中ReadHeader()方法;

技术分享
//
// 读取Wav文件头,并验证文件格式
// 成功返回文件句柄,并重定位文件指针到数据区
// 失败返回NULL
//
HANDLE ReadHeader(char* path)
{
    HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

    if (hFile == INVALID_HANDLE_VALUE)
    {
        printf("Unable to Open File!");
        return NULL;
    }
    char buffer[512];
    DWORD readByte;
    if (ReadFile(hFile, buffer, sizeof(buffer), &readByte, NULL))
    {
        Riff_Header header;
        Fmt_Block fmt;
        Data_Block data;
        memcpy(&header, buffer, sizeof(Riff_Header));
        if (strncmp(header.szRiffId, "RIFF", 4) != 0) { CloseHandle(hFile); return NULL; }

        memcpy(&fmt, buffer + sizeof(Riff_Header), sizeof(Fmt_Block));
        if (strncmp(fmt.szFmtId, "fmt ", 4) != 0) { CloseHandle(hFile); return NULL; }

        memcpy(&data, buffer + sizeof(Riff_Header)+fmt.dwFmtSize+8, sizeof(Data_Block));
        if (strncmp(data.szDataId, "data", 4) != 0) { CloseHandle(hFile); return NULL; }

        memcpy(&wfx, &fmt.wFormatTag, sizeof(WAVEFORMATEX) - 2);
        wfx.cbSize = 0;

        // 重定位文件指针,到数据起始位置
        int headSize = sizeof(Riff_Header) + fmt.dwFmtSize + 8 + sizeof(Data_Block);
        headSize = (headSize / 8 + (headSize % 8 ? 1 : 0)) * 8;
        SetFilePointer(hFile, headSize, 0, FILE_BEGIN);

        return hFile;
    }
    return NULL;
}
ReadHeader

 头文件分析完毕,可以得到波形文件信息wfx,现在可以打开设备,并读入音频数据到缓存块,最后提交,这样就可以播放音频了;

#define BufferSize    1024     // 读取文件的缓存大小

const char testWave[] = "C:/Windows/Media/Ring02.wav";

WAVEFORMATEX wfx;
CRITICAL_SECTION wcSection;static volatile int freeCount;      // 可用的缓存块数量,初始为BlockCount
static int curIndex;                // 当前要读入数据的缓存块的Index

int PlayWave(HANDLE hFile)
{
    unsigned char buffer[BufferSize];
    if (Blocks == NULL || hFile == NULL) return 1;

    freeCount = BlockCount;
    curIndex = 0;
    InitializeCriticalSection(&wcSection);

    HWAVEOUT hwo;
// 打开设备
if (waveOutOpen(&hwo, WAVE_MAPPER, &wfx, (DWORD_PTR)WaveOutProc, 0, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) { printf("Unable to Open Mapper Device!"); return 2; } WAVEHDR* current = &Blocks[curIndex]; printf("Play Wave Start...\n"); while (1) { DWORD readByte; if (!ReadFile(hFile, buffer, BufferSize, &readByte, NULL)) break; if (readByte == 0) break; if (readByte < BufferSize) memset(buffer + readByte, 0, BufferSize - readByte); memcpy(current->lpData + current->dwUser, buffer, BufferSize); current->dwUser += BufferSize; if (current->dwUser < BlockSize) continue; // 必须先填满当前缓存块 waveOutPrepareHeader(hwo, current, sizeof(WAVEHDR)); // 准备数据块 waveOutWrite(hwo, current, sizeof(WAVEHDR)); // 把数据块提交给设备 (播放),立即返回 EnterCriticalSection(&wcSection); freeCount--; LeaveCriticalSection(&wcSection); while (freeCount == 0) Sleep(10); // 当所有的数据都准备好,且没有释放时,等待 curIndex = (curIndex + 1) % BlockCount; current = &Blocks[curIndex]; current->dwUser = 0; } while (freeCount < BlockCount) Sleep(100); // 等待所有数据块播放完 printf("Finish Play Wave!\n"); DeleteCriticalSection(&wcSection); waveOutClose(hwo); return 0; }
//
// 设备回调方法,三种情况下调用:
// 当设备开启、关闭、播放一个缓存块完成时
//
void CALLBACK WaveOutProc(HWAVEOUT hwo, UINT msg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
    if (msg != WOM_DONE) return;  // 过滤设备开启、关闭消息

    WAVEHDR* pwh = (WAVEHDR*)dwParam1;
    waveOutUnprepareHeader(hwo, pwh, sizeof(WAVEHDR));  // 释放播放完的块

    EnterCriticalSection(&wcSection);
    freeCount++;
    LeaveCriticalSection(&wcSection);
}

说明:

1. 上面的打开设备方法中,参数WaveOutProc是一个回调方法,就是设备的反馈消息,包括设备的打开、关闭、缓存块播放完成消息;

2. wcSection是一个用于数据同步的信息,通过wcSection中的访问计数,可以在多个线程同时访问EnterCriticalSection和LeaveCriticalSection之间的数据时,避免数据并发冲突;

    如上例中,通过wcSection计数可以记录freeCount的访问个数,避免多线程(WaveOutProc方法)同时修改freeCount导致的错误;

那么,上面就是数据块的创建、读入数据、提交数据的过程,下面是调用它们:

#include "stdafx.h"
#include <Windows.h>
#include <mmsystem.h>
#include "WavStruct.h"

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

int
main(int argc, char* argv[]) { HANDLE hFile = ReadHeader((char*)testWave); if (hFile == NULL) return 0; CreateBlocks(); PlayWave(hFile); free(Blocks); CloseHandle(hFile); return 0; }

 

音频处理(二) 音频输出

标签:const   com   unp   erro   开发者   log   bre   专用   device   

原文地址:http://www.cnblogs.com/mwwf-blogs/p/7511183.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!