使用 DirectSound 播放 Wave 文件并实时显示其频谱 - 第二部分






4.03/5 (19投票s)
这是一篇介绍如何使用 DirectSound 播放波形文件并实时显示其频谱的文章。

引言
本文是我文章的改进版:使用 DirectSound 播放波形文件并实时显示其频谱。在本文中,我引用了Sun 的 JDK 源代码、YoYoPlayer、NewAC、KJ DSP 等。并且,一些代码也来源于这些项目。
本文包含四个部分
- 多线程
- 快速傅里叶变换 (FFT)
- Direct Sound 封装器 (来源于 JDK)
- 使用 Win32 GDI API 进行频谱绘制
本系列的最新版本可在以下链接找到:使用 DirectSound 播放音频文件并实时显示其频谱 - 第 3 部分。
多线程
在本文中,我使用了两个线程,一个用于管理声音播放,另一个用于管理用于样本分析的音频数据。CThread
类是所有子类的父类。另一件重要的事情是,我将这两个线程声明为 CBasicPlayer
的友元类;通过此声明,我可以在线程中使用 CBasicPlayer
的 private
或 protected
成员。首先,让我们看看声音播放线程。在此线程中,我调用 DAUDIO_GetDirectAudioDeviceCount
来获取所有可用的输出设备并将它们缓存在内存中,然后调用 DAUDIO_Open
打开第一个(默认)输出设备进行输出,调用 DAUDIO_Start
开始输出。请看下面的代码
void CPlayThread::Execute()
{
if(m_Player == NULL)
return;
if(m_Stop == TRUE)
return;
const DWORD buffersize = 16000;
SetFilePointer(m_Player->GetFileHandle(), 44, NULL, FILE_BEGIN);
INT32 count = DAUDIO_GetDirectAudioDeviceCount();
// wait time = 1/4 of buffer time
DWORD waitTime = (DWORD)((m_Player->m_BufferSize*1000.0F)/
(m_Player->m_SampleRate*m_Player->m_FrameSize));
waitTime = (DWORD)(waitTime / 4);
if(waitTime<10) waitTime = 1;
if(waitTime>1000) waitTime = 1000;
m_Player->m_info = (DS_Info*)DAUDIO_Open(0, 0 , TRUE, DAUDIO_PCM,
m_Player->m_SampleRate, m_Player->m_BitPerSample,
m_Player->m_FrameSize, m_Player->m_Channels, TRUE,
FALSE, m_Player->m_BufferSize);
m_Player->m_bytePosition = 0;
if(DAUDIO_Start((void*)m_Player->m_info, TRUE))
{
m_Player->m_SpectrumAnalyserThread->Resume();
printf("start play ...\n");
char buffer[buffersize];
while(!m_Stop)
{
DWORD dwRead;
if(ReadFile(m_Player->GetFileHandle(), (void*)buffer,
buffersize, &dwRead, NULL) == FALSE)
break;
if(dwRead <= 0)
break;
DWORD len = dwRead;
DWORD offset = 0;
DWORD written = 0;
/*
* in this loop, the data may not be written to device one time,
* maybe more than one time. So, we need this loop to process it.
*/
while(TRUE)
{
m_cs->Enter();
int thisWritten = DAUDIO_Write((void*)m_Player->m_info,
buffer+offset, len);
if(thisWritten < 0) break;
m_Player->m_bytePosition += thisWritten;
m_cs->Leave();
len -= thisWritten;
written += thisWritten;
if(len > 0)
{
offset += thisWritten;
m_cs->Enter();
Sleep(waitTime);
m_cs->Leave();
}
else break;
}
//copy audio data to audio buffer
//for audio data synchronize
DWORD pLength = dwRead;
jbyte* pAudioDataBuffer = pSpectrum->GetAudioDataBuffer();
if(pAudioDataBuffer != NULL)
{
int wOverrun = 0;
int iPosition = pSpectrum->GetPosition();
DWORD dwAudioDataBufferLength = pSpectrum->GetAudioDataBufferLength();
if (iPosition + pLength > (int)(dwAudioDataBufferLength - 1)) {
wOverrun = (iPosition + pLength) - dwAudioDataBufferLength;
pLength = dwAudioDataBufferLength - iPosition;
}
memcpy(pAudioDataBuffer + iPosition, buffer, pLength);
if (wOverrun > 0) {
memcpy(pAudioDataBuffer, buffer + pLength, wOverrun);
pSpectrum->SetPosition(wOverrun);
} else {
pSpectrum->SetPosition(iPosition + pLength);
}
}
}
m_Player->m_SpectrumAnalyserThread->Stop();
DAUDIO_Stop((void*)m_Player->m_info, TRUE);
DAUDIO_Close((void*)m_Player->m_info, TRUE);
m_Player->m_bytePosition = 0;
printf("stop play.\n");
}
m_Player->m_info = NULL;
}
在声音播放循环中,首先我们将音频数据读入缓冲区,然后将其写入输出设备,接着将音频数据复制到一个数据缓冲区以进行音频数据同步。现在,让我们看看样本数据分析线程。这部分源代码来自YoYoPlayer,来自 AudioChart.java 和 KJDigitalSignalProcessingAudioDataConsumer.java。所做的唯一工作是将 Java 代码转换为 C++ 代码。最重要的一点是 CSystem
类,它以纳秒为单位获取系统时间,用于精确的位置计算。它的源代码来自Sun 的 JDK 源代码,并且我已对其进行了修改以适应此应用程序。CSystem
类定义如下
typedef __int64 jlong;
typedef unsigned int juint;
typedef unsigned __int64 julong;
typedef long jint;
#define CONST64(x) (x ## LL)
#define NANOS_PER_SEC CONST64(1000000000)
#define NANOS_PER_MILLISEC 1000000
jlong as_long(LARGE_INTEGER x);
void set_high(jlong* value, jint high);
void set_low(jlong* value, jint low);
class CSystem
{
private:
static jlong frequency;
static int ready;
static void init()
{
LARGE_INTEGER liFrequency = {0};
QueryPerformanceFrequency(&liFrequency);
frequency = as_long(liFrequency);
ready = 1;
}
public:
static jlong nanoTime()
{
if(ready != 1)
init();
LARGE_INTEGER liCounter = {0};
QueryPerformanceCounter(&liCounter);
double current = as_long(liCounter);
double freq = frequency;
return (jlong)((current / freq) * NANOS_PER_SEC);
}
};
您只需调用 CSystem::nanoTime()
即可获取当前的系统时间(以纳秒为单位)。它很准确!
快速傅里叶变换 – FFT
FFT 在数字信号处理 (DSP) 中扮演着重要角色。CFastFourierTransform
类实现了它;它的源代码来自 KJFFT.java。这里所做的唯一工作是代码转换。而且,它易于使用。有关 FFT 算法的详细理论,请在此处查看。
Direct Sound 封装器
整个源代码来自Sun 的 JDK 源代码。早期,我发现了一个开源代码项目 – YoYoPlayer,它可以播放 MP3、Wav、OGG 等声音文件。当我深入研究其源代码时,我发现代码调用原生代码 (Win32 代码) 来播放声音。而且,我在 JDK 源代码中找到了原生代码。封装器包含一些函数来维护 Direct Sound 的声音播放,因此此应用程序需要 DirectX 6 或更高版本。
使用 Win32 GDI API 绘制频谱
源代码部分来自YoYoPlayer。请看下面的源代码
第一步是使用 FFT 计算样本数据,然后对 FFT 结果做一些处理。
void CSpectrumAnalyser::Process(float pFrameRateRatioHint)
{
if(IsIconic(m_Player->m_hWnd) == TRUE)
return;
for (int a = 0; a < m_SampleSize; a++) {
m_Left[a] = (m_Left[a] + m_Right[a]) / 2.0f;
}
float c = 0;
float pFrrh = pFrameRateRatioHint;
float* wFFT = m_FFT->Calculate(m_Left, m_SampleSize);
float wSadfrr = m_saDecay * pFrrh;
float wBw = ((float) m_width / (float) m_saBands);
RECT rect;
rect.left = 0;
rect.top = 0;
rect.right = rect.left + m_winwidth;
rect.bottom = rect.top + m_winheight;
FillRect(m_hdcMem, &rect, m_hbrush);
for (int a = 0, bd = 0; bd < m_saBands; a += (INT)m_saMultiplier, bd++) {
float wFs = 0;
for (int b = 0; b < (INT)m_saMultiplier; b++) {
wFs += wFFT[a + b];
}
wFs = (wFs * (float) log(bd + 2.0F));
if(wFs <= 0.01F)
wFs *= 10.0F;
else
wFs *= PI; //enlarge PI times, if do not,
//the bar display abnormally, why??
if (wFs > 1.0f) {
wFs = 1.0f;
}
if (wFs >= (m_oldFFT[a] - wSadfrr)) {
m_oldFFT[a] = wFs;
} else {
m_oldFFT[a] -= wSadfrr;
if (m_oldFFT[a] < 0) {
m_oldFFT[a] = 0;
}
wFs = m_oldFFT[a];
}
drawSpectrumAnalyserBar(&rect, (int) c, m_height,
(int) wBw - 1, (int) (wFs * m_height), bd);
c += wBw;
}
BitBlt(m_hdcScreen, 2, 41, m_winwidth, m_winheight, m_hdcMem, 0, 0, SRCCOPY);
}
下一步是绘制频谱条。在此部分,我在实例 CSpectrumAnalyser
时在内存位图上绘制渐变,然后调用 BitBlt
将内存位图复制到另一个内存位图。
void CSpectrumAnalyser::drawSpectrumAnalyserBar(RECT* pRect, int pX,
int pY, int pWidth, int pHeight, int band)
{
/* draw gradient bar */
BitBlt(m_hdcMem, pX, pY-pHeight, pWidth, pHeight,
m_hdcMem1, pX, pY-pHeight, SRCCOPY);
if (m_peaksEnabled == TRUE) {
if (pHeight > m_peaks[band]) {
m_peaks[band] = pHeight;
m_peaksDelay[band] = m_peakDelay;
} else {
m_peaksDelay[band]--;
if (m_peaksDelay[band] < 0) {
m_peaks[band]--;
}
if (m_peaks[band] < 0) {
m_peaks[band] = 0;
}
}
RECT rect = {0};
rect.left = pX;
rect.right = rect.left + pWidth;
rect.top = pY - m_peaks[band];
rect.bottom = rect.top + 1;
FillRect(m_hdcMem, &rect, m_hbrush1);
}
}
当您想要加快或减慢频谱显示速度时,请更改 BasicPlayer.h 文件中定义的 DEFAULT_FPS
。并且,要增加或减少频谱条的数量,请更改 BasicPlayer.h 中定义的 DEFAULT_SPECTRUM_ANALYSER_BAND_COUNT
。许多参数等待您去发现。
现有问题
本文代码中的一个现有问题是某些频率未被处理。文章多媒体 PeakMeter 控件展示了如何使用预定义频率来处理您想要的频率。
修改
- 添加了
CSpectrumAnalyser
类,将CBasicPlayer
中的一些代码分离出来 - 将音频缓冲区的*数据类型*从 BYTE (无符号
char
) 更改为 signed char (jbyte
),频谱似乎恢复正常 - 添加了渐变条来显示频谱
历史
- 2008-12-01:第一个版本在The Code Project发布
- 2008-12-03:修复了一些 bug