본문 바로가기

Programming/DirectX

[DirectSound] 9. 스트리밍 재생 예제

반응형

스트리밍 재생은 크기가 큰 파일이나 네트워크에서 전송되는 데이터를 재생하는 것이 가능합니다.

스트리밍 재생을 좀 더 보면 다음과 같습니다.

동일한 크기의 5개의 구간으로 나눠진 버퍼입니다.

(1)~(5)까지의 각 구간은 동일한 크기입니다.

화살표가 표시된 지점이 재생이 완료됐을 때 통지를 받을 지점입니다.

스트리밍 재생을 다시 설명하면 다음과 같습니다.

1. (1)~(5)까지의 구간에 PCM데이터를 입력

2. 재생을 시작

3. (1) 구간의 재생이 끝나고 통지가 전달되면 새로운 데이터(1~5까지의 데이터를 입력했으면 6)를 입력

4. 3번과 동일하게 다음 통지 구간 이후에 계속 반복

(1)~(5)까지 데이터를 채우고 재생한 이후에 각 통지 지점에서 통지를 받습니다.

재생이 완료된 부분을 Lock()으로 잠그고 새로운 데이터를 입력하면 됩니다.

버퍼가 5까지 완료하면 다시 1로 돌아오기 때문에 무한히 데이터를 입력하면 계속 재생이 됩니다.

스트리밍 재생을 하는 예제는 다음과 같습니다.

먼저 프로젝트를 생성합니다.

MFC 프로젝트를 생성하고 Dialog based로 변경한 이후에 Finish를 눌러서 프로젝트를 생성합니다.

먼저 다음과 같이 필요한 헤더와 라이브러리를 추가합니다.

#include <memory>
#include <mmsystem.h>
#include <dsound.h>

#pragma comment(lib, "Winmm.lib")
#pragma comment(lib, "dsound.lib")

DirectSound와 Multimedia 계열 API를 사용하기 위해서 추가합니다.

전체적인 Dlg.h 파일의 소스 코드는 다음과 같습니다.

#include <memory>
#include <MMSystem.h>
#include <dsound.h>

#pragma comment(lib, "Winmm.lib")
#pragma comment(lib, "dsound.lib")

// CDSoundLoopDlg dialog
class CDSoundLoopDlg : public CDialogEx
{
	struct ArrayDeleter
	{
		template <class T>
		void operator()(T* p)
		{
			delete[] p;
		}
	};

// Construction
public:
	CDSoundLoopDlg(CWnd* pParent = NULL);	// standard constructor

// Dialog Data
	enum { IDD = IDD_DSOUNDLOOP_DIALOG };

protected:
	virtual void DoDataExchange(CDataExchange* pDX);	// DDX/DDV support

	enum {Num_of_buffers = 4};
	BOOL m_bPlaying;
	std::shared_ptr<char> m_pData;
	DWORD m_nDataLen;
	DWORD m_nDataPos;

	CComPtr<IDirectSound8> m_lpDSound;
	CComPtr<IDirectSoundBuffer8> m_lpDSoundBuffer8;

	HANDLE m_hEvents[Num_of_buffers];
	MMRESULT m_timerID;
	WAVEFORMATEX m_wfx;

	int LoadWaveFile(LPTSTR szFileName);
	static void CALLBACK TimerProcess(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2);

// Implementation
protected:
	HICON m_hIcon;

	// Generated message map functions
	virtual BOOL OnInitDialog();
	afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
	afx_msg void OnPaint();
	afx_msg HCURSOR OnQueryDragIcon();
	DECLARE_MESSAGE_MAP()
public:
	afx_msg void OnBnClickedBtnLoad();
	afx_msg void OnBnClickedBtnPlay();
};

버튼 두 개를 추가하고 각각 Load와 Play를 담당하도록 했습니다.

그리고 ArrayDeleter를 추가해서 shared_ptr이 배열을 관리할 수 있도록 추가했습니다.

소스 코드를 간결하게 작성하기 위해서 예외처리 등은 추가로 필요할 수 있습니다.

추가된 함수는 LoadWaveFile()과 TimerProcess()입니다.

LoadWaveFile()은 이름 그대로 wav 파일을 열어서 m_pData, m_nDataLen, m_wfx에 각각 값을 추가합니다.

재생을 위한 부분은 다음과 같습니다.

void CDSoundLoopDlg::OnBnClickedBtnPlay()
{
	if (FALSE == m_bPlaying)
	{
		if (nullptr == m_pData || 0 == m_nDataLen)
			return ;

		DSBUFFERDESC bufdesc;
		ZeroMemory(&bufdesc, sizeof(DSBUFFERDESC));
		bufdesc.dwSize = sizeof(DSBUFFERDESC);

		bufdesc.dwFlags = DSBCAPS_STATIC | DSBCAPS_GLOBALFOCUS | DSBCAPS_CTRLPOSITIONNOTIFY;
		bufdesc.dwBufferBytes = m_wfx.nAvgBytesPerSec / 2 * Num_of_buffers;
		bufdesc.lpwfxFormat = &m_wfx;

		CComPtr<IDirectSoundBuffer> pBuffer;
		HRESULT hr = m_lpDSound->CreateSoundBuffer(&bufdesc, &pBuffer, NULL);
		if (FAILED(hr))
		{
			return ;
		}

		m_lpDSoundBuffer8.Release();
		GUID IID_IDSoundBuffer = {0x6825a449, 0x7524, 0x4d82, 0x92, 0x0f, 0x50, 0xe3, 0x6a, 0xb3, 0xab, 0x1e};
		pBuffer->QueryInterface(IID_IDSoundBuffer, (LPVOID*)&m_lpDSoundBuffer8);

		void *write1 = 0, *write2 = 0;
		unsigned long length1 = 0, length2 = 0;
		hr = m_lpDSoundBuffer8->Lock(0, bufdesc.dwBufferBytes, &write1, &length1, &write2, &length2, 0);
		if (FAILED(hr))
		{
			return ;
		}

		if (0 < length1)
		{
			memset(write1, 0x00, length1);
			memcpy_s(write1, length1, m_pData.get(), length1 >= m_nDataLen ? m_nDataLen : length1);
			m_nDataPos = length1 >= m_nDataLen ? m_nDataLen : length1;
		}

		hr = m_lpDSoundBuffer8->Unlock(write1, length1, write2, length2);

		GUID IID_IDSoundNotify = {0xb0210783, 0x89cd, 0x11d0, 0xaf, 0x8, 0x0, 0xa0, 0xc9, 0x25, 0xcd, 0x16};

		// Make Notification Point.
		CComPtr<IDirectSoundNotify8> lpDsNotify;
		DSBPOSITIONNOTIFY PositionNotify[Num_of_buffers];

		hr = m_lpDSoundBuffer8->QueryInterface(IID_IDSoundNotify, (LPVOID*)&lpDsNotify);
		if (SUCCEEDED(hr))
		{ 
			for (int i = 0 ; i < Num_of_buffers ; i++)
			{
				PositionNotify[i].dwOffset = m_wfx.nAvgBytesPerSec / 2 * (i + 1) - 1;
				PositionNotify[i].hEventNotify = m_hEvents[i];
			}
			hr = lpDsNotify->SetNotificationPositions(Num_of_buffers, PositionNotify);

			m_timerID = timeSetEvent(50, 50, TimerProcess, (DWORD)this, TIME_PERIODIC | TIME_CALLBACK_FUNCTION);

			m_bPlaying = TRUE;
			GetDlgItem(IDC_BTN_PLAY)->SetWindowText(_T("Stop"));
			m_nDataPos = 0;

			m_lpDSoundBuffer8->SetCurrentPosition(0);
			m_lpDSoundBuffer8->Play(0, 0, DSBPLAY_LOOPING);
		}
	}
	else
	{
		timeKillEvent(m_timerID);

		m_lpDSoundBuffer8->Stop();

		m_bPlaying = FALSE;
		GetDlgItem(IDC_BTN_PLAY)->SetWindowText(_T("Play"));
	}
}

통지를 받기 위해서 DSBCAPS_CTRLPOSITIONNOTIFY를 사용해서 버퍼를 생성하는 것을 확인할 수 있습니다.

Make Notification Point. 부분 이후에 실제 통지를 지정하는 부분이 시작됩니다.

파일 크기가 아닌 0.5초 * Num_of_Buffer(4) = 2초로 할당됩니다.

주의할 점은 통지 지점 - 1을 해야 실제 통지를 하고 싶은 위치에서 통지가 가능해집니다.

이제 0.5초 재생이 진행된 이후에 통지가 전달됩니다.

통지는 Event로 Signal 상태로 전환되기 때문에 WaitForSingleObject() 등의 API 함수로 확인할 수 있습니다.

timeSetEvent()를 호출해서 타이머 함수가 작동하도록 했습니다.

이제 타이머 함수에서는 주기적으로 확인을 하며 Signal 상태로 Event가 전환되었는지 확인하게 됩니다.

타이머 함수는 아래와 같이 구현되어 있습니다.

void CALLBACK CDSoundLoopDlg::TimerProcess(UINT uTimerID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2)
{
	CDSoundLoopDlg* pDDS = (CDSoundLoopDlg*)dwUser;
	HRESULT hr;

	void *write1 = 0, *write2 = 0;
	unsigned long length1, length2;

	DWORD dwResult = WaitForMultipleObjects(Num_of_buffers, pDDS->m_hEvents, FALSE, 0);
	if ((dwResult - WAIT_OBJECT_0) < Num_of_buffers)
	{
		TRACE(_T("[LOG] Wait : %d\n"), dwResult);
		DWORD nIndex = dwResult - WAIT_OBJECT_0;
		hr = pDDS->m_lpDSoundBuffer8->Lock((pDDS->m_wfx.nAvgBytesPerSec / 2) * nIndex, pDDS->m_wfx.nAvgBytesPerSec / 2, &write1, &length1, &write2, &length2, 0);

		if (0 < length1)
		{
			int nSize = (length1 > pDDS->m_nDataLen - pDDS->m_nDataPos) ? pDDS->m_nDataLen - pDDS->m_nDataPos : length1;
			memset(write1, 0x00, length1);
			memcpy_s(write1, nSize, pDDS->m_pData.get() + pDDS->m_nDataPos, nSize);
			pDDS->m_nDataPos += nSize;
			if (pDDS->m_nDataLen <= pDDS->m_nDataPos)
				pDDS->m_nDataPos = 0;
		}

		pDDS->m_lpDSoundBuffer8->Unlock(write1, length1, write2, length2);
	}
}

통지 구간이 4개이기 때문에 WaitForMultipleObjects() 사용해서 대기를 합니다.

실제 통지된 구간을 확인한 이후에 버퍼에 새로운 값을 채워넣는 작업을 진행합니다.

파일의 끝까지 도달했을 때 다시 처음으로 이동하게 해서 무한히 재생되도록 했습니다.

전체 소스코드와 테스트용 파일은 아래 링크를 통해서 다운로드가 가능합니다.

DSoundLoop.zip


APPLAUSE.WAV_

Wave파일의 확장자를 WAV로 변경해서 사용하면 됩니다.

스트리밍 재생은 큰 파일이나 네트워크를 통한 스트리밍 재생에 활용하면 됩니다.

멀티미디어 타이머를 통해서 통지를 받도록 설정했지만 쓰레드를 사용해도 구현이 가능합니다.

반응형