过去PC机上播放声音和音乐比登天还难!然而,随着DirectSound和DirectMusic的出现,这一切变得相当容易了。本文根据《Windows游戏编程大师技巧》一书学习了DirectSound基本原理,从《Doubango开源项目》中习得了开发技巧。(后文中的源码,为博主从开源库中剥离出来的)。呼,本文讲了很多原理,下一篇文章《DirectSound录制声音入门指南(1)》就很简单粗暴了。


目录:

  • 初始化DirectSound
  • 理解协作等级
  • 主声音缓冲区与辅助声音缓冲区
    • 创建声音辅助缓冲区
    • 把数据写入辅助声音缓冲区
  • 声音渲染
    • 控制音量
    • 调整频率
  • 完整DEMO

DirectSound内部格式是22kHz、8位立体声。但自然界中的大多数声音都是单声道,除非你在不同位置用两个麦克风进行录音,或者你有正真的立体数据,否则对DirectSound传入立体声数据只是种浪费。

建议设置:16位、单声道、22kHz;

有两个组件是我们所关心的:

  • 使用DirectSound时加载的动态链接库DLL;
  • 编译时的库文件dsound.lib和头文件dsound.h;

在DirectX8.0中有一个新的DirectSound接口,IDirectSound8。微软公司跳过了一系列版本,直接从版本1过渡到版本8。但是,IDirectSound8没有为我们带来任何新东西。我们正在用的标准DirectSound接口实际从DirectX3.0就存在了。本段摘自《Windows游戏编程大师技巧》一书。

DirectSound主要接口:

  • IUnknown ——所有COM对象的基对象。
  • IDirectSound——DirectSound的主COM对象。它代表音频本身。如果你的计算机中有多块声卡,那么每块声卡都需要一个DirectSound对象。
  • IDirectSoundBuffer——代表混音硬件和实际的声音。有两种类型的DirectSound缓冲,主缓冲和辅助缓冲。
    • 主缓冲——只有一个主缓冲提供正在播放的声音,通过硬件或软件把声音混合起来。
    • 辅助缓冲——提供存储,用来回放的声音。
  • IDirectSoundCapture——实时捕获声音。
  • IDirectSoundNotify——这个接口被用于反馈声音给DirectSound。在一个具有复杂的声音系统中药使用它。

初始化DirectSound

主DirectSound对象代表一块声卡。如果你有多块声卡,就必须枚举、检测并未它们分配GUID(唯一标识)。一般直接使用默认声卡,简单创建一个DirectSound主对象就够了。

/* 创建播放设备 */
if ((hr = DirectSoundCreate(NULL, &ds->device, NULL) != DS_OK)){return -3;
}

理解协作等级

这步是必须的,否则无法发出声音。你可以较为直接的方法进行控制,但微软建议你不要硬来。

DirectSound被设定为许多等级。主要有两类:

  • 可控制主缓冲;
  • 不可控制主缓冲。

记住,主缓冲代表实际的混音硬件或软件,它始终在混合声音并将其发送给扬声器。如果你直接操作主缓冲,微软希望你知道你在做什么,因为这可能带来灾难性的…

#define DSSCL_NORMAL                0x00000001
#define DSSCL_PRIORITY              0x00000002
#define DSSCL_EXCLUSIVE             0x00000003
#define DSSCL_WRITEPRIMARY          0x00000004

几大协作等级:

  • 普通等级(DSSCL_NORMAL)——这是协作性最好的。当程序获得焦点即可发出声音,同时其他应用程序也可发出声音,不独占。不可控制主缓冲。
  • 优先等级(DSSCL_PRIORITY)——可以访问所有硬件,可以改变主混音器的设定,可以要求声卡完成更高等级的内存操作(如压缩)。这个设定只有在你必须改变主缓冲数据格式时才是必要得——比如想播放16位采样值时,可以这么做。
  • 排他等级(DSSCL_EXCLUSIVE)——同优先等级一样,但只有你的应用程序在前台才能发出声音。
  • 写优先等级(DSSCL_WRITEPRIMARY)——这是最高优先级别,你可以完全控制。并且想听到声音就必须得自己控制主缓冲。如果你在写自己的混音程序或者声音引擎,就只能使用该模式——我想只有John Miles才会使用这个等级。

小结:

描述
DSSCL_NORMAL 设定普通等级
DSSCL_PRIORITY 设定优先等级,允许设置主缓冲数据格式
DSSCL_EXCLUSIVE 同上,但是它是独占的
DSSCL_WRITEPRIMARY 神一样的级别,完全控制主缓冲
/* 设置协调级别 */
if ((hWnd = GetForegroundWindow()) || (hWnd = GetDesktopWindow()) || (hWnd = GetConsoleWindow())){if ((hr = IDirectSound_SetCooperativeLevel(ds->device, hWnd, DSSCL_PRIORITY)) != DS_OK){return -4;}
}

主声音缓冲区与辅助声音缓冲区

主缓冲区就是来源于声卡本身的DirectSound对象。只要你没有将协作等级设置为DSSCL_WRITEPRIMARY,DirectSound就会为你管好主缓冲区。另外,如果你把协作等级设置为最低DSSCL_NORMAL,DirectSound就会为你创建一个主缓冲区而不需要你自己创建。

辅助缓冲区代表你想播放的声音,它可以任意大小,根据你的计算机内存,请量力而行。然而,声卡的SRAM却只能存储一定的数据。

有两种类型的辅助缓冲:静态、流态。
流式声音缓冲稍有不同。DirectSound使用了一个环形缓冲区,以环形数据形式存储声音,

  • 通过播放游标不断的读取数据;
  • 通过写游标不断的写入数据。

为了提高性能,声音缓冲区访问函数可能返回一个被分成两部分的内存地址。比如,

环形缓冲区大小为10,
目前存放至了位置8,
又来了一个大小为5的语音包,
那么,将会跨越,造成两个返回地址。

创建声音辅助缓冲区

dsbd.dwSize = sizeof(DSBUFFERDESC);
dsbd.dwFlags = DSBCAPS_PRIMARYBUFFER;
dsbd.dwBufferBytes = 0;
dsbd.lpwfxFormat = NULL;
//...
if ((hr = IDirectSound_CreateSoundBuffer(ds->device, &dsbd, &ds->primaryBuffer, NULL)) != DS_OK){return -6;
}

dwFlags 包含着声音缓冲区创建的标志,

#define DSBCAPS_PRIMARYBUFFER       0x00000001 //表明缓冲的是主缓冲的声音。只当想创建主缓冲的时候,才设定该标志
#define DSBCAPS_STATIC              0x00000002 //表明缓冲用于静态声音数据,大多数情况下,你将在硬件内存中创建这些缓冲
#define DSBCAPS_LOCHARDWARE         0x00000004 //如果内存够的话,使用硬件混频和内存建立声音缓冲
#define DSBCAPS_LOCSOFTWARE         0x00000008 //强制缓冲存储在软件内存中,并且使用软件混音
#define DSBCAPS_CTRL3D              0x00000010
#define DSBCAPS_CTRLFREQUENCY       0x00000020 //缓冲用于频率控制功能
#define DSBCAPS_CTRLPAN             0x00000040 //缓冲拥有省道平衡控制功能
#define DSBCAPS_CTRLVOLUME          0x00000080 //缓冲拥有音量控制功能
#define DSBCAPS_CTRLPOSITIONNOTIFY  0x00000100
#define DSBCAPS_CTRLFX              0x00000200
#define DSBCAPS_STICKYFOCUS         0x00004000
#define DSBCAPS_GLOBALFOCUS         0x00008000
#define DSBCAPS_GETCURRENTPOSITION2 0x00010000
#define DSBCAPS_MUTE3DATMAXDISTANCE 0x00020000
#define DSBCAPS_LOCDEFER            0x00040000
#define DSBCAPS_TRUEPLAYPOSITION    0x00080000

注意,
你赋予一个声音的能力越多,那么在听到声音前处理的道数就越多,处理时间越长。

把数据写入辅助声音缓冲区

辅助缓冲区实际上是环形的,这比直接写线性缓冲区困难一些。DirectSound给我们做了很多工作,你只要锁定表面内存,接着就可以写入数据了…

如果把dwFlags设定为DSBLOCK_FROMWRITECURSOR ,那么缓冲将从当前写入游标被锁住;如果设定为DSBLOCK_ENTIREBUFFER,缓冲区将被完全锁住。

// lock
if (hr = IDirectSoundBuffer_Lock(ds->secondaryBuffer,dwWriteCursor/* Ignored because of DSBLOCK_FROMWRITECURSOR */,(DWORD)ds->bytes_per_notif_size,&lpvAudio1, &dwBytesAudio1,&lpvAudio2, &dwBytesAudio2,DSBLOCK_FROMWRITECURSOR) != DS_OK){printf("IDirectSoundBuffer_Lock error\n");continue;
}#if OPEN_READ_PCM_FROM_FILE
if ((out_size = fread(ds->bytes_per_notif_ptr, 1, ds->bytes_per_notif_size, ds->fp)) != ds->bytes_per_notif_size){//ds->started = false;//停止播放printf("player finish.\n");stopPlayer(ds);
}
#endif
if (out_size < ds->bytes_per_notif_size) {// fill with silencememset(&ds->bytes_per_notif_ptr[out_size], 0, (ds->bytes_per_notif_size - out_size));
}
if ((dwBytesAudio1 + dwBytesAudio2) == ds->bytes_per_notif_size) {memcpy(lpvAudio1, ds->bytes_per_notif_ptr, dwBytesAudio1);if (lpvAudio2 && dwBytesAudio2) {memcpy(lpvAudio2, &ds->bytes_per_notif_ptr[dwBytesAudio1], dwBytesAudio2);}
}
else {//DEBUG_ERROR("Not expected: %d+%d#%d", dwBytesAudio1, dwBytesAudio2, dsound->bytes_per_notif_size);
}// unlock
if ((hr = IDirectSoundBuffer_Unlock(ds->secondaryBuffer, lpvAudio1, dwBytesAudio1, lpvAudio2, dwBytesAudio2)) != DS_OK) {}

DirectSound工作方式有些注意点,
锁定它,但不是返回一个指针,而是两个!因此,你必须先把一部分数据写入到第一个指针指向的内存,其余的写入第二个指针指向的内存区域。

再将上文讲到的知识重复一遍,

假如你有一个1000字节的缓冲区(逻辑上环形),锁定缓冲区返回的两个指针可能为ptr1= 100、ptr2 = 0。假设你要写入950字节数据,DirectSound从ptr1 = 100开始写入900字节,(由于逻辑上环形缓冲区),这时候又从ptr2开始写入剩下的数据。

声音渲染

一切的准备好了,播放声音…

DSBPLAY_LOOPING标志可以循环播放声音,如果打算只播放一次,把dwFlags设置为0即可。

/* 开始播放缓冲区
将次缓冲区的声音数据送到混声器中,与其他声音进行混合,最后输到主缓冲区自动播放.*/
if ((hr = IDirectSoundBuffer_Play(ds->secondaryBuffer, 0, 0, DSBPLAY_LOOPING)) != DS_OK){return -6;
}

再美妙的声音也终将有停止的时刻,

if ((hr = IDirectSoundBuffer_Stop(ds->secondaryBuffer)) != DS_OK){}

控制音量

SetVolume()与你期待的工作方式可能不一样,如果传入0,折相当于DSBVOLUME_MAX,声音将被无衰减播放——即音量最大;如果设定为DSBVOLUME_MIN或-10000,那么衰减将达到最大:-100dB,这时听不到任何声音,连蚂蚁都听不到。

/* 设置音量 [-10000,0]*/
if (IDirectSoundBuffer_SetVolume(_secondaryBuffer, 0/*_convert_volume(0)*/) != DS_OK){printf("setVolume error\n");
}

最好封装一个宏,这样就将-10000~0映射为以dB为声音单位(0-100dB),

#define DSVOLUME_TO_DB(volume) ((DWORD)-30*100-volume)

调整频率

这是最酷的地方,可以让声音变得慢且邪恶,或变得欢快且萝莉。可以听起来忽然像考拉,忽而像树懒…

一切尽在 IDirectSoundBuffer_SetFrequency中。

完整DEMO

摘录了一部分,跃跃欲试的请下拉,找到git传送门。

Config.h

#ifndef _WIN32_CONFIG_H
#define _WIN32_CONFIG_H#define MEDIA_BITS_PER_SAMPLE_DEFAULT  16
#define MEDIA_CHANNELS_DEFAULT         1
#define MEDIA_RATE_DEFAULT             8000
#define MEDIA_PTIME_DEFAULT            60#endif

DsoundPlayer.h

#ifndef WIN32_AUDIO_CONTROL_DSPLAYER_H
#define WIN32_AUDIO_CONTROL_DSPLAYER_H#include <dsound.h>
#include <stdint.h>
#include <stdio.h>
#include "Config.h"#pragma comment (lib,"dsound.lib")
#pragma comment (lib,"dxguid.lib")#if !defined(PLAYER_NOTIF_POS_COUNT)
#   define PLAYER_NOTIF_POS_COUNT   20
#endif /* PLAYER_NOTIF_POS_COUNT *//*开关,是否从文件读取数据*/
#define OPEN_READ_PCM_FROM_FILE 1typedef struct PLAYER{PLAYER(){device = NULL;primaryBuffer = NULL;secondaryBuffer = NULL;started = false;bytes_per_notif_ptr = NULL;
#if OPEN_READ_PCM_FROM_FILEfp = NULL;
#endif} LPDIRECTSOUND device; LPDIRECTSOUNDBUFFER primaryBuffer; LPDIRECTSOUNDBUFFER secondaryBuffer; HANDLE notifEvents[PLAYER_NOTIF_POS_COUNT];bool started;size_t bytes_per_notif_size;uint8_t* bytes_per_notif_ptr; HANDLE tid[2];
#if OPEN_READ_PCM_FROM_FILEFILE* fp;
#endif
} Player;/*播放准备*/
int prepare(Player* ds);
/*开始播放*/
int startPlayer(Player* ds);
/*挂起播放*/
int suspendPlayer(Player* ds);
/*唤醒播放*/
int resumePlayer(Player* ds);
/*停止播放*/
int stopPlayer(Player* ds);
/*释放内存资源*/
int unprepare(Player* ds);DWORD WINAPI playerThreadImpl(LPVOID params);
#if OPEN_READ_PCM_FROM_FILE
int openFile(Player* ds);
int closeFile(Player* ds);
#endif#endif

全部项目工程尽在《DirectSound 播放声音入门指南(0)》,DEMO

DirectSound入门指南(0)播放声音相关推荐

  1. DirectSound入门指南(1)录制声音

    在上一篇文章<DirectSound播放声音入门指南(0)>基础上对声音的录制进行研究,形成了本文.同时,本文也主要参考了Doubango开源项目(该项目现在已经难以维护了,因为其功能太强 ...

  2. DirectSound入门指南(1)录音实战

    在上一篇文章<DirectSound播放声音入门指南(0)>基础上对声音的录制进行研究,形成了本文.同时,本文也主要参考了Doubango开源项目(该项目现在已经难以维护了,因为其功能太强 ...

  3. CTF入门指南(0基础)

    ctf入门指南 如何入门?如何组队? capture the flag 夺旗比赛 类型: Web 密码学 pwn 程序的逻辑分析,漏洞利用windows.linux.小型机等 misc 杂项,隐写,数 ...

  4. web开发快餐式入门指南 0. 写在前面

    Web框架层出不穷,然而它们又大同小异.绝大多数框架都按照MVC架构风格所设计,所以他们提供的组件和功能都十分类似.很多教程在讲解如何使用某一框架开发Web应用时,专注于它在这个框架的实现细节,而忽视 ...

  5. gridcontrol值为0时设置为空_XASSET 4.0入门指南

    XASSET 5.1已经发布 XASSET 5.1为Unity项目提供了可以快速投入到生产环境中使用的具有更智能和灵活的资源分包.热更新机制和稳健高效的资源加载和内存管理的资源管理方案.它不仅可以服务 ...

  6. TensorFlow 2.0 快速入门指南 | iBooker·ApacheCN

    原文:TensorFlow 2.0 Quick Start Guide 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 不要担心自己的形象,只关心如何实现目标.--<原则>,生活 ...

  7. Asp.net MVC3.0 入门指南 6 审视编辑方法和视图

    审视编辑方法和视图 在这一节中,您将审视movie控制器生成的响应方法和视图.然后您将添加 一个自定义搜索页面. 运行程序并通过在URL追加/Moives浏览movie控制器.把鼠标悬停在Edit 链 ...

  8. python入门指南-python3.6.0入门指南(官方版).pdf

    您所在位置:网站首页 > 海量文档 &nbsp>&nbsp计算机&nbsp>&nbspPython python3.6.0入门指南(官方版).pdf7 ...

  9. 【51单片机快速入门指南】4.2: SSD1306 OLED屏(0.96寸、1.3寸)的I2C控制详解

    目录 硬知识 SSD1306简介 I2C 接口 从机地址位(SA0) I2C 总线写数据 命令解码器 晶振电路和显示时间发生器 复位 图形显示数据RAM (GDDRAM) 命令表 基本命令表 部分指令 ...

最新文章

  1. 中国IT运维O2O市场产值规模预测及发展策略建议报告2022年
  2. 最新 | Python 官方中文文档正式发布!
  3. 2019.7.13刷题统计
  4. go sync.WaitGroup源码分析
  5. OC-成员变量的作用域
  6. .net pdf转图片_在客户端实现PDF转图片
  7. 21个Docker 命令
  8. 简单的图书馆系统 LibrarySystem(OC模拟)
  9. 正态性检验(Normality test)
  10. COPYPASTE: AN AUGMENTATION METHOD FOR SPEECH EMOTION RECOGNITION -论文阅读
  11. SpringBoot整合Mail
  12. 一个像素的旅行,卷积网络可视化项目火了:点点鼠标就能看懂的扫盲神器
  13. 高速电路设计实践学习笔记(一)电阻 电容 电感 磁珠
  14. 把手机摄像头或智能电视摄像头数据推送到另一台手机或智能电视上的方法
  15. 伺服使能信号的作用与注意事项
  16. XILINX DDR4 SDRAM(MIG)笔记2(基于VU9P FPGA)
  17. 阿里巴巴内测全网社交产品来往
  18. 白话数字签名(3)—Web程序中的数字签名
  19. mac上配macOS10.12.6的系统盘问题
  20. DAX基础3:简单介绍Power BI中的New Quick Measures

热门文章

  1. python绘制渐变图_Python利用imshow制作自定义渐变填充柱状图(colorbar)
  2. CMD数据库备份与恢复拒绝访问及MySQL语法错误
  3. (一百七十七) WiFi如何分辨出不同加密方式的AP?(续)
  4. 2006年儿童节的blog
  5. 了解一台多媒体计算机的基本配置,一台典型的多媒体计算机的硬件不一定会包括...
  6. flowable工作流节点总是自动跳过
  7. docker安装sqlserver2017(待完善)
  8. 卢伟冰:Redmi Note 9热销,Note10或将上市
  9. Windows临时端口不够用导致TCP连接失败的问题
  10. NB-IoT模块(BC系列—BC95)详解