这篇文章主要给大家介绍下如何用DirectSound打造一个简易播放器,因为篇幅有限且代码逻辑较为复杂,我们只介绍下核心技术内容。该播放器主要包括以下功能:

  • 播放、暂停
  • 播放进度提示。

1. DirectSound播放概念简介

1.1 播放相关概念

首先要介绍下DirectSound的设计理念:

在DirectSound中,你需要播放的音频一般需要(也可以直接放入主缓冲区,但是操作上比较困难而且对其他DirectSound程序不太友好)放入一个被称为次缓冲区(Secondary Buffer)的地址区域中,该缓冲区由开发者人为创建操控。由于DirectSound支持多个音频同时播放,所以我们可以创建多个缓冲区并同时播放。在播放时,放入次缓冲区的音频先会被送入一个叫做主缓冲区(Primary Buffer)的地方进行混音,然后在送入硬件声卡中进行播放。在Windows driver model,即WDM模式下,DirectSound实际上不能直接操作声卡硬件,所有的混音操作不是送给主缓冲区而是被送往内核混音器(Kernel Mixer)进行混音,然后由内核混音器送往硬件。在WDM模式下,内核混音器替代了主缓冲区的功能位置。

1.2 缓冲区相关概念

DirectSound的缓冲区类别大体可以分为两种:1) 静态缓冲区,2) 流缓冲区。静态缓冲区就是一段较短的音频全部填充到一个缓冲区中,然后从头到尾播放;流缓冲区可以描述为音频流,实际上这种流也是通过单个有长度的缓冲区来抽象模拟的。在流缓冲区模式下,单个缓冲区会被重复填充和播放,也就是说当DirectSound播放到缓冲区的最后一个尾部时,它会回到缓冲区的头部继续开始播放。因此,在播放较长的音频文件时需要开发者手动循环填充缓冲区。

DirectSound中还有游标(cursor)的概念,游标分两种:1) 播放游标(play cusror),2) 写入游标(write cursor)。顾名思义,播放游标指向当前播放的地址,写入游标指向当前可以写入的开始地址,写入游标总是在播放游标前面,且两者之间的数据块已经被DirectSound预定,不能被写入。其中,播放指针可以通过函数来更改,而写入指针由DirectSound自己控制,开发者不能操作它。一旦次缓冲区设定好音频格式,在播放中这两个游标会一直保持固定的间距:如果没记错,采样率44100Hz、2声道、8比特的音频数据,两者的位置间隔660字节,也就是1/70秒的数据。

为了在适当的时候填充下一块要播放的数据,DirectSound提供了notify的功能:当播放到某一个缓冲区位置的时候,他会提醒你。该notify功能的实现通过Windows的事件对象(Event Object)实现,也就是说你需要等待这个事件被唤醒,在GUI程序中,这通常意味着你需要另起一个线程。

2. 播放器实现

2.1 创建缓冲区

通过调用IDirectSound8::CreateSoundBuffer(...)函数,我们创建一个能够容纳seconds秒的次缓冲区。参数DSBUFFERDESC中需要指定DSBCAPS_CTRLPOSITIONNOTIFY、DSBCAPS_GETCURRENTPOSITION2,前者允许我们设置notify,后者保证我们在调用IDirectSoundBuffer8::GetCurrentPosition(...)时播放游标的位置比较准确。

void WavPlayer::createBufferOfSeconds(unsigned seconds)
{
DSBUFFERDESC bufferDescription;
bufferDescription.dwSize = sizeof(bufferDescription);
bufferDescription.dwFlags = DSBCAPS_CTRLPOSITIONNOTIFY |
DSBCAPS_GLOBALFOCUS |
DSBCAPS_GETCURRENTPOSITION2 |
DSBCAPS_LOCDEFER ;
bufferDescription.dwBufferBytes = m_secondaryBufferSize
= m_wavFile.getWaveFormat().nAvgBytesPerSec * seconds;
bufferDescription.dwReserved = 0;
bufferDescription.lpwfxFormat = &m_wavFile.getWaveFormat();
bufferDescription.guid3DAlgorithm = GUID_NULL; IDirectSoundBuffer* soundBuffer;
if (m_directSound8->CreateSoundBuffer(&bufferDescription, &soundBuffer, NULL) != DS_OK) {
throw std::exception("create secondary buffer failed:CreateSoundBuffer");
} if (soundBuffer->QueryInterface(IID_IDirectSoundBuffer8, (LPVOID*)&m_soundBufferInterface)
!= S_OK) {
throw std::exception("IDirectSoundBuffer8 interface not supported!");
}
}

2.2 预填充缓冲区

本人尝试过直接在缓冲区头部设置notify,使数据的填充比较自然。大多数情况下这样没有问题,但是在电脑cpu负载较高时会造成音频毛刺,效果不尽如人意。因此我选择预填充数据,防止这类情况出现。

void WavPlayer::fillDataIntoBuffer()
{
Q_ASSERT(m_bufferSliceCount > 1); // fill half buffer to signal the notify event to do next data filling
LPVOID firstAudioAddress;
LPVOID secondAudioAddress;
DWORD firstAudioBytes;
DWORD secondAudioBytes;
HRESULT result = m_soundBufferInterface->Lock(0,
m_secondaryBufferSize / m_bufferSliceCount,
&firstAudioAddress, &firstAudioBytes,
&secondAudioAddress, &secondAudioBytes,
0);
if (result == DSERR_BUFFERLOST) {
result = m_soundBufferInterface->Restore();
}
if (result != DS_OK) {
throw std::exception("Cannot lock entire secondary buffer(restore tryed)");
} Q_ASSERT(firstAudioBytes == m_secondaryBufferSize / m_bufferSliceCount &&
secondAudioAddress == nullptr &&
secondAudioBytes == 0);
m_nextDataToPlay = static_cast<char*>(m_wavFile.getAudioData());
CopyMemory(firstAudioAddress, m_nextDataToPlay, firstAudioBytes);
if (m_soundBufferInterface->Unlock(firstAudioAddress, firstAudioBytes,
secondAudioAddress, secondAudioBytes)
!= DS_OK) {
throw std::exception("Unlick failed when fill data into secondary buffer");
} m_nextDataToPlay += firstAudioBytes;
}

2.3 设置缓冲区notify

为了在运行时循环填充数据,我们先要设置notify,这里的notify比较复杂,包含了3种类别:

  • 数据填充notify。
  • 音频播放终止notify。
  • 退出notify。(为了优雅的退出填充线程,我们选择在退出播放时唤醒线程)

其中,第二种notify可能会也可能不会与第一种notify重合,在不重合情况下我们才新分配一个notify:

m_additionalNotifyIndex = 0;
if (m_additionalEndNotify)
for (unsigned i = 1; i < m_bufferSliceCount; ++i)
if (bufferEndOffset < (m_secondaryBufferSize / m_bufferSliceCount * i)) {
m_additionalNotifyIndex = i;
break;
} // add a stop notify count at the end of entire notifies to make the data filling
// thread exit gracefully
++m_notifyCount;
m_notifyHandles = static_cast<HANDLE*>(malloc(sizeof(HANDLE)* (m_notifyCount)));
if (m_notifyHandles == nullptr)
throw std::exception("malloc error");
m_notifyOffsets = static_cast<DWORD*>(malloc(sizeof(DWORD)* (m_notifyCount)));
if (m_notifyHandles == nullptr)
throw std::exception("malloc error"); for (unsigned i = 0; i < m_notifyCount; ++i) {
m_notifyHandles[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
if (m_notifyHandles[i] == NULL)
throw std::exception("CreateEvent error"); if (m_additionalEndNotify && i == m_additionalNotifyIndex) {
// set buffer end notify
m_notifyOffsets[i] = bufferEndOffset;
m_endNotifyHandle = m_notifyHandles[i];
}
else if (i == m_notifyCount - 1) {
// do nothing
} else {
// NOTE: the entire buffer size must can be devided by this `notifyCount`,
// or it will lost some bytes when filling data into the buffer. since the end
// notify is inside the notify count, we need to calculate the buffer slice index.
unsigned bufferSliceIndex = getBufferIndexFromNotifyIndex(i);
m_notifyOffsets[i] = m_secondaryBufferSize / m_bufferSliceCount * bufferSliceIndex; if (!m_additionalEndNotify && m_notifyOffsets[i] == bufferEndOffset)
m_endNotifyHandle = m_notifyHandles[i];
}
}
// skip the exit notify which we toggle explicitly
setNotifyEvent(m_notifyHandles, m_notifyOffsets, m_notifyCount - 1);

2.4 创建数据填充线程、播放进度更新

该线程内包含多种类别的notify:

  1. 播放终止notify,则发出终止信号、退出线程。
  2. 数据填充notify,则填充数据、更新播放进度。
  3. 非终止非数据填充notify(发生在数据填充完成但播放未结束时),continue。

该线程一直等待这几个notify,并对不同情况进行不同的处理:

DWORD WINAPI WavPlayer::dataFillingThread(LPVOID param)
{
WavPlayer* wavPlayer = reinterpret_cast<WavPlayer*>(param); while (!wavPlayer->m_quitDataFillingThread) {
try {
DWORD notifyIndex = WaitForMultipleObjects(wavPlayer->m_notifyCount, wavPlayer->m_notifyHandles, FALSE, INFINITE);
if (!(notifyIndex >= WAIT_OBJECT_0 &&
notifyIndex <= WAIT_OBJECT_0 + wavPlayer->m_notifyCount - 1)) throw std::exception("WaitForSingleObject error"); if (notifyIndex == wavPlayer->m_notifyCount - 1)
break; // each notify represents one second(or approximately one second) except the exit notify
if (!(wavPlayer->m_additionalNotifyIndex == notifyIndex && wavPlayer->m_endNotifyLoopCount > 0)) {
++wavPlayer->m_currentPlayingTime;
wavPlayer->sendProgressUpdatedSignal();
} // if return false, the audio ends
if (tryToFillNextBuffer(wavPlayer, notifyIndex) == false) {
wavPlayer->stop(); ++wavPlayer->m_currentPlayingTime;
wavPlayer->sendProgressUpdatedSignal(); wavPlayer->sendAudioEndsSignal();
// not break the loop, we need to update the audio progress although data filling ends
}
}
catch (std::exception& exception) {
OutputDebugStringA("exception in data filling thread:");
OutputDebugStringA(exception.what());
}
}
return 0;
}

3. 运行结果

完整代码见链接

DirectSound---简易Wav播放器的更多相关文章

  1. 简易音乐播放器主界面设计 - .NET CORE(C#) WPF开发

    微信公众号:Dotnet9,网站:Dotnet9,问题或建议:请网站留言, 如果对您有所帮助:欢迎赞赏. 简易音乐播放器主界面设计 - .NET CORE(C#) WPF开发 阅读导航 本文背景 代码 ...

  2. ios开发学习- 简易音乐播放器2 (基于iPhone4s屏幕尺寸)-- 歌词解析--plist文件应用--imageNamed图片加载耗内存

    声明:(部分图片来自网络,如果侵犯了您的权益请联系我,会尽快删除!) 又是音乐播放器,不过这次和上次不一样了,准确说这次更像播放器了,初学者不建议看这个,可以先看前面一个音乐播放器(1),当然 我没加 ...

  3. iso 开发学习--简易音乐播放器(基于iPhone4s屏幕尺寸)

    三个按钮  一个进度条 贴图(软件中部分图片,来自网络,如果侵犯了您的权益,请联系我,会立刻撤下) 核心代码 // // ViewController.m // 08-10-MusicPlayer / ...

  4. Qt 简易图片播放器

    一.前言 使用 Qt 制作了一个简单的图片播放器,点击 "浏览按钮" 浏览图片所在目录,目录中的所有图片缩小图标和名称会显示在左侧的图片列表中,点击列表中的图片项,可以在右侧区域的 ...

  5. Opencv实现简易播放器

    实现了在MFC中显示图片,再要显示一个视频就是轻而易举的事了,本篇介绍使用Opencv制作一个简易的播放器,实现打开文件.暂停.继续播放.再次播放和总\当前帧数显示功能. 首先还是先看一下界面效果: ...

  6. IOS开发之简单音频播放器

    今天第一次接触IOS开发的UI部分,之前学OC的时候一直在模拟的使用Target-Action回调模式,今天算是真正的用了一次.为了熟悉一下基本控件的使用方法,和UI部分的回调,下面开发了一个特别简易 ...

  7. 一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇

    一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...

  8. java音频播放器

    java音频播放器备份,支持wav,mp3 都是摘抄于网络,wav播放,mp3播放 播放wav版本 包: 不需要其他jar包 代码: package com; import javax.sound.s ...

  9. 吴裕雄--天生自然python学习笔记:python 用pygame模块制作一个音效播放器

    用 Sound 对象制作一个音效播放器. 应用程序总览 程序在执行后默认会把 WAV 音频文件加载到清单中,单击“播放”按钮可开始 播放,同时显示 “正在播放 xxx 音效”的信息 . 播放过程中,可 ...

随机推荐

  1. win处navicat直接导出的sql脚本导入Linux mysql报错问题

    最近几天在把win上的项目的数据库转移到Ubuntu,于是第一件事就是从win处的navicat直接导出sql脚本,然后进入Ubuntu导入的时候会报错误,跳过错误继续执行导致数据库表的缺失. 跨平台 ...

  2. 最常用Python开源框架有哪些?

    Python开源框架有很多,像Django.Flask.webpy等等,但哪些是最常用到的呢?我们收集了一些Python使用者的宝贵意见,把他们认为最常用的Python开源框架简单的介绍给大家. 一. ...

  3. sudo :apt-get:command not found

    在centos下用yum install xxx yum和apt-get的区别 一般来说著名的linux系统基本上分两大类:  1.RedHat系列:Redhat.Centos.Fedora等  2. ...

  4. sqlite效率探测

    在编译后当然就是使用sqlite,贸贸然去测试了一下创建数据库,插入数据,仅几条数据,发现,真不错的数据库,后来把数据量提高到10000的 时候,发现,怎么这么慢,后来自己都受不了,网上一查,原来是如 ...

  5. JSP页面中的pageEncoding和contentType的区别

    <%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> pag ...

  6. SQLServer分页查询模板

    SELECT TOP 10 * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY id) AS RowNumber,* FROM ERPTelFile ) A WHE ...

  7. Centos7-两台Centos机器间复制文件

    我又两台Centos机器,一台192.168.1.1:另一台192.168.1.2 现在在将192.168.1.1上的一个文件复制到192.168.1.2.登陆到192.168.1.1然后运行命令 命 ...

  8. JAVAEE——BOS物流项目01:学习计划、搭建环境、主页设计(jQuery EasyUI)

    1 学习计划 1.项目概述 项目背景介绍 2.搭建项目开发环境 数据库环境 maven项目搭建 svn环境搭建 3.主页设计(jQuery EasyUI) layout页面布局 accordion折叠 ...

  9. java常用类--系统相关

    java提供了System类和Runtime类来与程序的运行平台进行交互. System类 System类代表java程序的运行平台,程序不能创建这个类的对象,System类提供了一些类field和方 ...

  10. Log4net使用详细说明

    1.概述 log4net是.Net下一个非常优秀的开源日志记录组件.log4net记录日志的功能非常强大.它可以将日志分不同的等级,以不同的格式,输出到不同的媒介.本文主要是介绍如何在Visual S ...