상세 컨텐츠

본문 제목

엘리시아 이브의 심심강좌 사운드

프로그래밍 관련/사운드

by AlrepondTech 2011. 1. 6. 14:55

본문

반응형

 

 

 

=================================

=================================

=================================

 

 

 

 

 

 

 

나우누리 게임제작 포럼

                                            엘리시아 이브의 심심강좌

                                 Direct X : Windows Game Programing
====================================================================
Episode 1 ... 게임 엔진 제작
====================================================================

    Section 28 소리 연주

에고^^; 원래는 윈도우 프로그래밍을 하려고 했었는데요.
아직 화면 캡춰 프로그램을 못구해서 ( 그림을 넣으려고^^;; ) 늦어지내요
( HTML 파일로 만들라고 했거든요 )

흠, 먼저 소리 출력에 대해서 쓸께요^^

이번에 만들 함수 및 클래스들은 다음과 같습니다.

int InitDSound(HWND hWnd,int Samples=22050,int Bits=16);
void CloseDSound();

class CWAVE
{
 LPDIRECTSOUNDBUFFER lpDSB;

 char*  szName;
 LPVOID lpvLockAddress;
 DWORD  dwLockSize;


 int Lock();
 void Unlock();

 public:
  CWAVE();
  ~CWAVE();
  int Load(char* pszName);
  int Create
            (int Size,
            WAVEFORMATEX *pWaveFormEx,DWORD dwFlag=DSBCAPS_STATIC);
  void Clear();
  void SetFormat(int Samples,int Bits);
  void SetPos(DWORD Pos);
  void Play();
  void SetBuffer(LPDIRECTSOUNDBUFFER lpDSBCopy);
  LPDIRECTSOUNDBUFFER DuplicateBuffer();
};

그럼 이제 만들어 보기로 하죠^^


    1. DirectSound 만들기

음.. 곧 모양이 바뀌겠지만요^^; 다이렉트 사운드를 만드는 것을 보겠습니
다. 음.. 기본적인 모양은 DDraw 랑... 거의 흡사합니다.

먼저 다이렉트사운드 전역 변수를 만들어 주어야 합니다^^
저번에 다이렉트 드로우를 만든 것 처럼요.

LPDIRECTSOUND lpDS=NULL;

다이렉트 드로우때도... 1차버퍼, 프라이머리 서피스라는게 있었죠? 소리
쪽에서도 1차 버퍼, 프라이머리 버퍼라는 것이 있습니다^^

LPDIRECTSOUNDBUFFER lpDSB;

여기에... 소리들을 넣어주면^^ 출력이 되는 것이지요.
간단하죠?
소리 섞는 일같은건... DSound 가 알아서 해주거든요


자, 그럼 실제 소스 부분으로 들어가겠습니다

if(FAILED(DirectSoundCreate(NULL,&lpDS,NULL))) return 0;

Create 함수 처음에 나오는 거 아시죠^^?
먼저 DSound를 만들어 줍니다

if(FAILED(lpDS->SetCooperativeLevel(hWnd,DSSCL_PRIORITY))) return 0;
다음은...협력 레벨을 정해 줍니다. DDSCL_NORMAL 이나 DSSCL_PRIORITY 중
에서 골라주세요^^
( 소스들을 보니까 다들 이걸 썼더만유^^; 그래서... 저도 그냥 이걸...)

이제 구체적으로 1차 버퍼를 만들어 봅시다

DDSURFACEDESC 와 작성 방식이 비슷합니다.

 DSBUFFERDESC DSBufDesc;
변수 선언하공^^

 memset(&DSBufDesc,0,sizeof(DSBUFFERDESC));
메모리 깨끗이 청소하고

 DSBufDesc.dwSize=sizeof(DSBUFFERDESC);
크기 정해주고요

 DSBufDesc.dwFlags=DSBSCL_PRIMARYBUFFER;
이거 1차 표면을 만든다고 이야기를 해주는 것이지요

 DSBufDesc.dwBufferBytes=0;
 DSBufDesc.lpwfxFormat=NULL;
1차버퍼의 크기와 형식은 그냥 0으로 해주어야 한다는 군요

 lpDS->CreateSoundBuffer(&DSBufDesc,&lpDSB,NULL);
마지막으로 1차 버퍼 생성~~

하하^^ 너무나도 간단합니다...

라고 하기엔 이르죠?

여기에 출력 형식을 정해주어야 하거든요^^;;

출력 형식은 버퍼 생성에 성공했을 경우 사용합니다.
( 모.. 실패하면, 그냥 기본 설정쓰라는 이야기래요. 에러 처리 할 필요가
없대요~~ )

소리 데이터를 만들기 위해서는 3가지 정도의 정보값이 필요합니다;;

    채널 ->
        1개면 모노, 2개면 스테레오죠?
        요즘에 모노쓰는 겜은 없으니... 그냥 여기서 무조건 스테레오로
        하겠습니다.

    초당샘플링 ->
        소리 연주 프로그램들을 보면... 음질을 결정하는 수치가 있습니
        다. 11025hz, 22050hz, 44100hz 이런 수치들이 쪼만하게 나와있죠?

        역시 클수록 음질은 좋아지고^^; 소리 파일 크기도 커집니다.

        에... 연주할 내용이 임의면 어떻하냐고요^^;
        모... DSound가 알아서 처리해준다네요. 역시 걱정할 필요는 없습
        니다. 다만, 수치가 똑같으면... 별로 처리가 필요없으니... 부하
        가 적게 걸리겠죠?

        변수명 Samples;

    초당비트수 ->
        흣^^; 음... 소리는 아날로그죠? 그런데... 컴터는 디지털만 알아
        듣는답니다. 그래서 디지털화 할때... 단계를 구분해서, 아날로그
        신호를 일정단위로 끊거든요.몇단계나 끊을 것인지 결정하는 것입
        니다. 8비트면 256단계, 16비트면 65536 단계^^

        역시 클수록 음질이 좋겠죠? 더불어 파일크기도 팍팍~!!

        변수명 Bits;

 WAVEFORMATEX WaveFormEx;
 memset(&WaveFormEx,0,sizeof(WAVEFORMATEX));
 WaveFormEx.wFormatTag=WAVE_FORMAT_PCM;
 WaveFormEx.nChannels=2;
 WaveFormEx.nSamplesPerSec=Samples;
 WaveFormEx.wBitsPerSample=Bits;
 WaveFormEx.nBlockAlign=Bits/8*2;
 WaveFormEx.nAvgBytesPerSec=Bits*WaveFormEx.nBlockAlign;
 lpDSB->SetFormat(&WaveFormEx);

핫^^ 내용은 그냥 예제에서 그대로 옮긴 것이니... 세부적인 내용은
묻지마세요

자자, 그냥 넘어갑시다. 소리만 출력되면 되는 것이지요. 모. 핫핫
( 사실 제가 음악쪽에 약해서리요^^;; )

이렇게 DSound 설정 및 1 차 버퍼 만들기는 끝이 납니다요.

왠지 어정쩡하죠?


    2. DSound 없에기

흠.. 이정도는 충분히 짐작하실듯...

버퍼랑, DSound랑 모두 Release해주시면 됩니다

    lpDSB->Release();
    lpDS->Release();


    3. 좀더 멋진 방법으로...

자^^ 이제 클래스로 만들어 보기로 합시다

먼저, 버퍼를 만드는 함수부터 보기로 할까요?

int CWAVE::Create(int Size,WAVEFORMATEX *pWFE,DWORD dwFlag)
{
 Clear();

 DSBUFFERDESC DSBufDesc;
 memset(&DSBufDesc,0,sizeof(DSBUFFERDESC));
 DSBufDesc.dwSize=sizeof(DSBUFFERDESC);
 DSBufDesc.dwFlags=dwFlag;
 DSBufDesc.dwBufferBytes=Size;
 DSBufDesc.lpwfxFormat=pWFE;

 if(FAILED(lpDS->CreateSoundBuffer(&DSBufDesc,&lpDSB,NULL)))
            return 0;

 return 1;
}

에... 버퍼 만들때의 구성요소에는 3가지가 있습니다.
버퍼의 크기, 소리의 형식, 그리고... 어떤 버퍼인가 하는 것이지요

크기나 형식은 아실테고요. 버퍼의 종류에 대해서 간단히 설명해 드릴께요

    프라이머리 버퍼 - 아까 만들어 본거죠? *

    스태틱 버퍼 - 짧은 소리용 버퍼입니다 *

    스트림 버퍼 - 긴소리용 버퍼입니다

(*는 이번 강좌에서 다루는 내용만 체크한 것입니다^^ )

흠... 기본적인 내용은 위에서 다루었고, 나머지는 다 대입식이라 설명할
것이 없을듯 싶내요^^

다음에 보실것은 소리 형식을 만드는 함수입니다.

void CWAVE::SetFormat(int Samples,int Bits)
{
 WAVEFORMATEX WaveFormEx;
 memset(&WaveFormEx,0,sizeof(WAVEFORMATEX));
 WaveFormEx.wFormatTag=WAVE_FORMAT_PCM;
 WaveFormEx.nChannels=2;
 WaveFormEx.nSamplesPerSec=Samples;
 WaveFormEx.wBitsPerSample=Bits;
 WaveFormEx.nBlockAlign=Bits/8*2;
 WaveFormEx.nAvgBytesPerSec=Bits*WaveFormEx.nBlockAlign;
 lpDSB->SetFormat(&WaveFormEx);
}

흠;; 역시 위에서 다루었던 내용이죠?
호호. 그냥 넘어갑니다.

자, 버퍼를 청소하는 것도 미리 만들어 둡시다

void CWAVE::Clear()
{
 if(lpDSB)
 {
  lpDSB->Release();
  lpDSB=NULL;
 }
}

CWAVE::CWAVE()
{
 lpDSB=NULL;
}

CWAVE::~CWAVE()
{
 Clear();
}

역시 위에서 이미 설명했습니다요^^;

자, 요런 함수들을 가지고... 다이렉트 사운드 만들기를 깨끗이 다시 만들
어 봅시다

LPDIRECTSOUND   lpDS=NULL;      // 다이렉트 사운드
CWAVE*          CWavePrimary;   // 프라이머리 버퍼

int InitDSound(HWND hWnd,int Samples,int Bits)
{

 // 다이렉트 사운드 만들기
 if(FAILED(DirectSoundCreate(NULL,&lpDS,NULL))) return 0;

 // 협력 레벨 정하기
 if(FAILED(lpDS->SetCooperativeLevel(hWnd,DSSCL_PRIORITY)))
            return 0;

 // 프라이머리 버퍼 만들기
 CWavePrimary=new CWAVE;
 if(CWavePrimary->Create(0,NULL,DSBCAPS_PRIMARYBUFFER))
  // 소리 형식 설정
  CWavePrimary->SetFormat(Samples,Bits);
  
 return 1;
}

void CloseDSound()
{
 if(CWavePrimary) delete CWavePrimary;
 lpDS->Release();
}

뜨아... 이번 강좌는 정말 설명해 드릴께 별로 없군요^^
역시 소스 내용은 위에서 다루었습니다


    4. 소리내기

자, 이제 핵심 작업인 소리내기 작업을 해보도록 합시다.
모.. 웨이브 Stream 연주도 있습니다만, 다들 시디음원쓰니 -.-; 필요가 없
겠죠? 그냥 짧은 소리 내는 거만 다루겠습니다.

먼저 소리를 내기위해서는 웨이브 파일을 읽어드려야 합니다.
소스 먼저 볼까요?

int CWAVE::Load(char* pszName)
{
 HMMIO   hMmIO;
 MMCKINFO  MmCkInfo;
 MMCKINFO  MmCkInfoParent;
 WAVEFORMATEX* pWaveFormEx;

 if(WaveOpenFile(pszName,&hMmIO,&pWaveFormEx,&MmCkInfoParent)!=0)
            return 0;
 if(WaveStartDataRead(&hMmIO,&MmCkInfo,&MmCkInfoParent)!=0)
            return 0;
 if(!Create(MmCkInfo.cksize,pWaveFormEx))
 {
  WaveCloseReadFile(&hMmIO,&pWaveFormEx);
  return 0;
 }
 if(!Lock())
 {
  WaveCloseReadFile(&hMmIO,&pWaveFormEx);
  Clear();
  return 0;
 }
 UINT uiSize;
 if
      (
            WaveReadFile
            (hMmIO,dwLockSize,(BYTE*)lpvLockAddress,&MmCkInfo,&uiSize)
      )
 {  
  WaveCloseReadFile(&hMmIO,&pWaveFormEx);
  Clear();
  return 0;
 }
 Unlock();
 WaveCloseReadFile(&hMmIO,&pWaveFormEx);
 return 1;
}

흠... 이상한 함수들이 많군요^^ 호호

 HMMIO   hMmIO; // 파일 핸들
 MMCKINFO  MmCkInfo; // 청크 정보
 MMCKINFO  MmCkInfoParent; // 부모 청크 정보

변수이름들이 묘하군요^^; 이런게... DSound에 있을리가 없겠죠?
내.  mmsystem.h 에 있는 것들입니다.
mmsystem.h 은 윈도우 멀티미디어 관련 내용들이 있는 헤더입니다.
( 쓰실때는 mmsystem.lib 이 아니라 WinMm.Lib 을 연결하셔야 합니다 )

if(WaveOpenFile(pszName,&hMmIO,&pWaveFormEx,&MmCkInfoParent)!=0)
    return 0;

웨이브 파일을 여는 함수입니다^^;
여기서... 웨이브 형식정보를 얻어냅니다

흠.. 이걸 만들어야 하나.. 걱정하시는듯..;
에.. 그러실 필요가 없습니다. 예제중에 있거든요^^
Wave.C 하고 Wave.H 라는^^;;
이걸 그냥 복사하셔서... 프로젝트에 연결해주세요
( Wave- 요런 파일은 모두 Wave.C 파일입니다 )

if(WaveStartDataRead(&hMmIO,&MmCkInfo,&MmCkInfoParent)!=0)
    return 0;

웨이브 파일 읽을 준비를 하는 것입니다.

if(!Create(MmCkInfo.cksize,pWaveFormEx))
{
 WaveCloseReadFile(&hMmIO,&pWaveFormEx);
 return 0;
}

웨이브파일에서 얻은 정보를 토대로 버퍼를 만듭니다
만약 실패한다면... 웨이브 파일을 닫습니다.

에... 저번 DDraw를 했지만... 메모리 제어를 하려면...꼭 Lock/Unlock처리
를 해주셔야 합니다. 다음 순서가^^ 데이터를 읽는 부분이기 땜시로...
Lock과정을 먼저 다루어 보겠습니다

( 핫^^; 배에서 갑자기 꼬로록 소리가... 잠시 식사 좀 하겠습니다 )
( 으흠^^ 오징어 국은 정말 맛있군요. 아침을 안 먹어서인지...더 맛있어
요. 홍홍홍. ^^; 요즘 매일 새벽 5시 자서 1시에 일어나니... 아침을 못 먹
거든요. 속쓰려라^^; - 아, 대학 입학할때까지는 계속 이런 생활을^^ 룰룰
루~~ )

쫍쩝. 먹으면서^^ 계속 강좌를 쓰겠습니다

int CWAVE::Lock()
{
 if(!lpDSB) return 0;
 if
      (
        FAILED
        (
            lpDSB->Lock
            (
                0, // 락 시작 오프셋
                0, // 무시
                &lpvLockAddress, // 락 메모리 주소
                &dwLockSize, // 락 크기
                NULL, // 순환 시작주소
                NULL, // 순환 끝주소
                DSBLOCK_ENTIREBUFFER //;; 그냥 쓰길래.. 썼음
            )
        )
      ) return 0;
 return 1;
}


에...Lock을 하기위해서는 두가지의 변수가 필요합니다.
락할 메모리 주소와 락한 메모리 크기이죠

 LPVOID lpvLockAddress;
 DWORD  dwLockSize;

멤버 변수로 선언한 것들중에 있죠?

다시 아까 그곳으로 넘어갑시다

if(!Lock())
{
 WaveCloseReadFile(&hMmIO,&pWaveFormEx);
 Clear();
 return 0;
}

에.. 역시 락을 실패하면... 웨이브 닫고요, 버퍼를 제거해줍니다.

이제 파일을 읽어야 겠죠?

UINT uiSize;
if
(
    WaveReadFile
    (hMmIO,dwLockSize,(BYTE*)lpvLockAddress,&MmCkInfo,&uiSize)
)
{  
 WaveCloseReadFile(&hMmIO,&pWaveFormEx);
 Clear();
 return 0;
}

흠^^; 모.. 그냥 소스 그대로 이해해 주시길.. uiSize의 용도는 아직 찾질
못했습니다. 죄송^^

자, 메모리 처리가 끝났으니까.. 이제 언락과정을 해야 합니다.

역시^^; 언락은 간단합니다.

void CWAVE::Unlock()
{
 if(!lpDSB) return;
 lpDSB->Unlock(lpvLockAddress,dwLockSize,NULL,0);
}

홋홋. 쉽죠? 아, 첫줄은... 버퍼가 설정안되었는데... 처리할 경우를 대비
해서 써넣은 것입니다. 버퍼가 없으면... 그냥 리턴하죠.


자, 이제 중요한 소리 연주입니다.

void CWAVE::Play()
{
 if(!lpDSB) return;
 SetPos(0);
 lpDSB->Play(0,0,0);
}

역시 버퍼가 없을 경우 에러처리를 하고요.
에... SetPos또 처음 보는 거죠?

void CWAVE::SetPos(DWORD Pos)
{
 if(!lpDSB) return;
 lpDSB->SetCurrentPosition(Pos);
}

출력 버퍼의 위치를 기록하는 것입니다. 그냥 소리 출력이니...언제나 처음
으로 설정했습니다.

다음줄이^^ 바로 소리 연주 짜잔~~

간단하죠?

아^^ 맛있어. 밥을 다먹었내요. 잠시 설겆이하러~~ ^^

착. 흠... 국그릇하고 숟가락, 컵밖에 없어서 금방 끝나는군요.

에... 다시...
간혹... 버퍼를 놓치는 경우도 있습니다^^;
다이렉트 드로우 경우도... Alt+Tab을 누르면 버퍼를 잃어 버리곤하죠
그럴때는...

    if(FAILED(lpDDS->IsLost())) lpDDS->Restore();
    else 출력...

을 하죠^^;

에... 소리에서도 비슷합니다. Play 함수에서 버퍼를 잃어버리는 경우,
DSERR_BUFFERLOST 라는 에러를 리턴하게 됩니다. 이럴경우, Restore를 해준
다음.. 웨이브 파일을 다시 읽어 드리시면 됩니다.

void CWAVE::Play()
{
 if(!lpDSB) return;
 SetPos(0);
 if(lpDSB->Play(0,0,0)==DSERR_BUFFERLOST)
  if(SUCCEEDED(lpDSB->Restore()))
   if(Load(szName)) lpDSB->Play(0,0,0);
}

에... szName처리과정을^^; 위 소스에서는 뺐습니다.
모... 간단히 적으라면요.

CWAVE::CWAVE()
{
 lpDSB=NULL;
 szName=NULL;
}

void CWAVE::Clear()
{
 if(lpDSB)
 {
  lpDSB->Release();
  lpDSB=NULL;
 }
 if(szName) delete szName;
}

int CWAVE::Load(char* pszName)
{
    ... 끝에쯤해서...
    szName=new char[strlen(pszName)+1];
    strcpy(szName,pszName);
    return 1;
}

이런 내용을 포함시켜 주시면 됩니다.


    5. 똑같은 버퍼 만들기

음. 게임을 하다보면... 똑같은 소리가 나는 경우가 있습니다.
총알 버튼 누르고 있음 ( 전 버튼 누르는 속도가 느려서^^; 그냥 누르고 있
습니다. ) 따다다계속 소리가 나죠?

그냥 출력시마다, Play함수를 실행시키면... 연주되던게 끊기고... 다시 첨
부터 플레이가 됩니다. 이러면 원하는 결과가 나오지 않겠죠? 그래서... 제
어에 제약은 따르지만, 소리만 낼수 있는 버퍼를 만들 수 있습니다.다시 버
퍼를 읽는 과정이 없이 이전꺼에서.. 복사를 하는 것이지요^^

LPDIRECTSOUNDBUFFER CWAVE::DuplicateBuffer()
{
 LPDIRECTSOUNDBUFFER lpDSBCopy;
 lpDS->DuplicateSoundBuffer(lpDSB,&lpDSBCopy);
 return lpDSBCopy;
}

짜잔^^ 이렇게 하시면... 현재 클래스속의 버퍼를 복사해 올 수 있습니다.
그럼 지정하는 방법은?

void CWAVE::SetBuffer(LPDIRECTSOUNDBUFFER lpDSBCopy)
{
    Clear();
    lpDSB=lpDSBCopy;
}

요 함수를 이용해 지정을 해줍니다.


    6. 예제 보기

자, 그럼 예제 뿌리고 마칩니다

#include <windows.h>
#include <Eter.H>
#include <DSound.H>
#include <DD.H>
#include <mmsystem.h>
#include "Wave.H"

/*

    함수 및 클래스 만드는 부분은 생략...

*/

CWAVE *CWave;
CWAVE *CWave2;

int Start()
{
 InitDDraw(ghWnd);
 InitDSound(ghWnd);
 CFont=new CFONT;
 CWave=new CWAVE;
 CWave2=new CWAVE;
  if(CWave->Load("Demo.Wav"))
  {
                  // 버퍼 복제
   CWave2->SetBuffer(CWave->DuplicateBuffer());
   CWave->Play();
   CWave2->Play();
  }
 
 return 1;
}

void End()
{
 delete CWave;
 delete CWave2;
 CloseDSound();
 CloseDDraw();
 delete CFont;
}

int Main()
{
 return 0;
}

int Idle()
{
 return 1;
}

LRESULT wmKeyDown(MSGPARAM)
{
 PostQuitMessage(0);
 return 0;
}

int Window(LPSTR szArgs)
{
 MsgProc[WM_KEYDOWN]=wmKeyDown;
 return InitWindow("DSound",LoadIcon(NULL,IDI_APPLICATION),"MENU");
}


=====================================================================
e-mail myevan@nownuri.net & nowngm3@nownuri.net

나우누리 게임 제작 포럼 NGM
    http://blue.nownuri.net/ngm/

게임 제작 팀 이터니티
    http://mypage.channeli.net/blaze79/

 

 

 

=================================

=================================

=================================

 

 

 

 

반응형


관련글 더보기

댓글 영역