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






3.44/5 (8 votes)
An article to show how to play a Wave file with DirectSound and display its spectrum in real time.
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.
- Calculate values of left and right, then use FFT to calculate it.
- Use the Win32 GDI API to draw the spectrum.
- The position that I get is not exactly a circle buffer.
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("%1.4f\n"), 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?