65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.03/5 (19投票s)

2008年12月1日

CPOL

3分钟阅读

viewsIcon

531199

downloadIcon

6546

这是一篇介绍如何使用 DirectSound 播放波形文件并实时显示其频谱的文章。

引言

本文是我文章的改进版:使用 DirectSound 播放波形文件并实时显示其频谱。在本文中,我引用了Sun 的 JDK 源代码YoYoPlayerNewACKJ DSP 等。并且,一些代码也来源于这些项目。

本文包含四个部分

  1. 多线程
  2. 快速傅里叶变换 (FFT)
  3. Direct Sound 封装器 (来源于 JDK)
  4. 使用 Win32 GDI API 进行频谱绘制

本系列的最新版本可在以下链接找到:使用 DirectSound 播放音频文件并实时显示其频谱 - 第 3 部分

多线程

在本文中,我使用了两个线程,一个用于管理声音播放,另一个用于管理用于样本分析的音频数据。CThread 类是所有子类的父类。另一件重要的事情是,我将这两个线程声明为 CBasicPlayer 的友元类;通过此声明,我可以在线程中使用 CBasicPlayerprivateprotected 成员。首先,让我们看看声音播放线程。在此线程中,我调用 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.javaKJDigitalSignalProcessingAudioDataConsumer.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
© . All rights reserved.