Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

Play Wave file with DirectSound and display its spectrum in real time

3.44/5 (8 votes)
15 Sep 2008CPOL2 min read 1   8.4K  
An article to show how to play a Wave file with DirectSound and display its spectrum in real time.

DSound_Spectrum.gif

Introduction

Do you want to play a Wave file with DirectSound and display its spectrum in real time? This article uses the DirectSound Win32 GDI API to achieve this. This article includes two parts: one is playing the Wave file, which is based on the article: Using DirectSound to Play Audio Stream Data; the other is how to display the sound spectrum in real time? This is our main work.

Contents

Maintain our circle buffer when playing

In this section, I add some code to maintain a circle buffer with red color displayed, which is based on the article: Using DirectSound to Play Audio Stream Data.

class CMyDirectSound  
{
public:

    CMyDirectSound();
    virtual ~CMyDirectSound();

    void SetFormat(WAVEFORMATEX WFE);
    void SetCallback(LPGETAUDIOSAMPLES_PROGRESS Function_Callback, 
                     LPVOID lpData);
    void Play();
    void Pause();
    void Stop();
    DWORD GetSamplesPlayed(DWORD* pCurPlayPos);
    void TimerCallback();
    LPDIRECTSOUNDBUFFER GetSoundBuffer() { return m_lpDSB; }
    WAVEFORMATEX GetWaveFormateEx() { return m_WFE; } // jacky_zz[2008-09-04]
    LPDIRECTSOUNDBUFFER GetDirectSoundBuffer() { return m_lpDSB; } // jacky_zz[2008-09-04]
    LPBYTE GetSampleDataBuffer() { return m_lpSampleDataBuffer; } // jacky_zz[2008-09-04]

private:

    //<DirectSound>
    WAVEFORMATEX m_WFE;
    LPDIRECTSOUND m_lpDS;
    LPDIRECTSOUNDBUFFER m_lpDSB;
    HANDLE m_pHEvent[2];
    //</DirectSound>

    //<Audio Buffer>
    LPBYTE m_lpAudioBuf;
    LPGETAUDIOSAMPLES_PROGRESS m_lpGETAUDIOSAMPLES;
    LPVOID m_lpData;
    //</Audio Buffer>

    //<SampleData> jacky_zz[2008-09-04]
    LPBYTE m_lpSampleDataBuffer;
    //</SampleData> jacky_zz[2008-09-04]

    //<Playing>
    MMRESULT m_timerID;
    DWORD m_dwCircles1;
    DWORD m_dwCircles2;
    int m_iDB;    
    //</Playing>

    //<Error Information>
    CString m_strLastError;
    //</Error Information>

    DWORD m_dwThreadID;
    HANDLE m_hThread;
};

The member m_lpSampleDataBuffer is two seconds long, the same as the second buffer, m_lpDSB, in the class CMyDirectSound. The class CMyDirectSound reads the Wave file sound data and fills it into the second buffer, and I add some code here to copy sound data into m_lpSampleDataBuffer.

void CMyDirectSound::Play()
{
    ...

    //Copy Audio Buffer to DirectSoundBuffer
    if (NULL == lpvAudio2)
    {
        memcpy(lpvAudio1, m_lpAudioBuf, dwRetBytes);

        //jacky_zz
        memcpy(m_lpSampleDataBuffer, m_lpAudioBuf, dwRetBytes);
        //jacky_zz
    }
    else
    {
        memcpy(lpvAudio1, m_lpAudioBuf, dwBytesAudio1);
        memcpy(lpvAudio2, m_lpAudioBuf + dwBytesAudio1, dwBytesAudio2);

        //jacky_zz
        memcpy(m_lpSampleDataBuffer, m_lpAudioBuf, 
               dwBytesAudio1 + dwBytesAudio2);
        //jacky_zz
    }

    ...
}
void CMyDirectSound::TimerCallback()
{
    ...

    //If near the end of the audio data
    if (dwRetSamples < m_WFE.nSamplesPerSec)
    {
        DWORD dwRetBytes = dwRetSamples*m_WFE.nBlockAlign;
        memset(m_lpAudioBuf+dwRetBytes, 0, 
               m_WFE.nAvgBytesPerSec - dwRetBytes);

        //jacky_zz
        memset(m_lpSampleDataBuffer+dwRetBytes, 0, 
               m_WFE.nAvgBytesPerSec*2 - dwRetBytes);
        //jacky_zz
    }

    //Copy AudioBuffer to DirectSoundBuffer
    if (NULL == lpvAudio2)
    {
        memcpy(lpvAudio1, m_lpAudioBuf, dwBytesAudio1);

        //jacky_zz
        memcpy(m_lpSampleDataBuffer+dwOffset, 
               m_lpAudioBuf, dwBytesAudio1);
        //jacky_zz
    }
    else
    {
        memcpy(lpvAudio1, m_lpAudioBuf, dwBytesAudio1);
        memcpy(lpvAudio2, m_lpAudioBuf + dwBytesAudio1, dwBytesAudio2);

        //jacky_zz
        memcpy(m_lpSampleDataBuffer+dwOffset, m_lpAudioBuf, 
               dwBytesAudio1+dwBytesAudio2);
        //jacky_zz
    }

    ...
}

Calculate values of left and right, then use FFT to calculate it

In this section, I will describe how to calculate the values of left and right, then use FFT to calculate it. I want to say some things before doing it. I referenced a few articles like FFT of waveIn audio signals, Simple Audio Out Oscilloscope and Spectrum Analyzer, and Multimedia PeakMeter control. All of them cast the sound data (in bytes) to a short array with reinterpret_cast or (short*). But this method does not work right in my program, so I used a Java MP3 player which can display the spectrum.

LPBYTE lpAudioBuffer = playmod->pMyDS->GetSampleDataBuffer();
if(lpAudioBuffer == NULL)
    return;

float left, right;
for(int i=0;i<FFT_SAMPLE_SIZE;i++) {
    if(dwCurPlayPos > dw2SecondByteSize)
        dwCurPlayPos -= dw2SecondByteSize;

    left = (float)((lpAudioBuffer[dwCurPlayPos+1] << 8) + 
                    lpAudioBuffer[dwCurPlayPos+0])/32767;
    right = (float)((lpAudioBuffer[dwCurPlayPos+3] << 8) + 
                     lpAudioBuffer[dwCurPlayPos+2])/32767;
    floatSamples[i] = (left+right)/2;
    dwCurPlayPos+=4;
}

FFT* fft = (FFT*)playmod->fft;
float* lpFloatFFTData = fft->calculate(floatSamples, FFT_SAMPLE_SIZE);
memcpy(floatMag, lpFloatFFTData, FFT_SAMPLE_SIZE/2);
SendMessage(playmod->hWndMain, WM_PAINT+913, (WPARAM)0, (LPARAM)13);

Use Win32 GDI API to draw the spectrum

void DrawSpectrum(HWND hwnd, float* fftData)
{
    if(fftData == NULL)
        return;    

    HDC hdc = GetWindowDC(hwnd);
    SetBkMode(hdc, TRANSPARENT);

    HPEN hpen, hpenOld;
    HBRUSH hbrush, hbrushOld;
    HBRUSH hbrush1, hbrushOld1;
    RECT rect;

    rect.left = 4;
    rect.top = 23;
    rect.right = rect.left+SPECTRUM_WIDTH;
    rect.bottom = rect.top+SPECTRUM_HEIGHT;

    // Create a green pen.
    hpen = CreatePen(PS_SOLID, 1, RGB(0, 255, 0));
    // Create a red brush.
    hbrush = CreateSolidBrush(RGB(0, 0, 0));
    hbrush1 = CreateSolidBrush(RGB(125, 125, 125));

    // Select the new pen and brush, and then draw.
    hpenOld = (HPEN)SelectObject(hdc, hpen);
    hbrushOld = (HBRUSH)SelectObject(hdc, hbrush);
    hbrushOld1 = (HBRUSH)SelectObject(hdc, hbrush1);
    Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);

    int maxFreq = FFT_SIZE / 2;
    int height = 0;
    int maxHeight = SPECTRUM_HEIGHT;

    float c = 0;
    float floatFrrh = 1.0;
    float floatDecay = (float)SPECTRUM_DECAY;
    float floatSadFrr = (floatFrrh*floatDecay);
    float floatBandWidth = ((float)SPECTRUM_WIDTH/(float)SPECTRUM_BANDS);
    float floatMultiplier = 2.0;

    //CString xx;
    RECT r;
    for(int a=0, band=0; band < SPECTRUM_BANDS; 
            a+=(int)floatMultiplier, band++)
    {
        float wFs = 0;

        // -- Average out nearest bands.
        for (int b = 0; b < floatMultiplier; b++) {
            wFs += fftData[a + b];
        }

        // -- Log filter.
        wFs = (wFs * (float) log((float)(band + 2)));
        //xx.Format(_T(&quot;%1.4f\n&quot;), wFs);
        //OutputDebugString(xx);
        if (wFs > 1.0f) {
            wFs = 1.0f;
        }

        // -- Compute SA decay...
        if (wFs >= (floatOldMag[a] - floatSadFrr)) {
            floatOldMag[a] = wFs;
        } else {
            floatOldMag[a] -= floatSadFrr;
            if (floatOldMag[a] < 0) {
                floatOldMag[a] = 0;
            }
            wFs = floatOldMag[a];
        }

        r.left = rect.left + (int)c + 1;
        r.right = r.left + (int)(floatBandWidth-1);
        r.top = SPECTRUM_HEIGHT - (int)(wFs*SPECTRUM_HEIGHT);
        if(r.top < rect.top)
            r.top = rect.top + 2;

        r.top += 22;
        r.bottom = rect.bottom-2;        

        FillRect(hdc, &r, hbrushOld1);

        int height = HEIGHT(r);
        if(height > intPeaks[band])
        {
            intPeaks[band] = height;
            intPeaksDelay[band] = SPECTRUM_DELAY;
        }
        else
        {
            intPeaksDelay[band]--;
            if (intPeaksDelay[band] < 0) {
                intPeaks[band]--;
            }

            if (intPeaks[band] < 0) {
                intPeaks[band] = 0;
            }
        }

        r.top -= intPeaks[band];
        if(r.top < rect.top)
            r.top = rect.top + 2;

        r.top += 22;
        if(r.top >= rect.bottom)
            r.top = rect.bottom - 2;

        r.bottom = r.top + 1;
        FillRect(hdc, &r, hbrushOld1);

        c += floatBandWidth;
    }

    // Do not forget to clean up.
    SelectObject(hdc, hpenOld);
    DeleteObject(hpen);
    SelectObject(hdc, hbrushOld);
    DeleteObject(hbrush);
    SelectObject(hdc, hbrushOld1);
    DeleteObject(hbrush1);
    ReleaseDC(hwnd, hdc);

    Sleep(20);
}

The position that I get is not exactly a circle buffer

This program is not mature. So, some problems still exist, some of which I am not able to get the right way to resolve. I hope people at CodeProject can help me. One of the issues is that the position that I get is not exactly a circle buffer. I have tried methods like using a thread or a timer. Do you think you could help me resolve it?

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)