티스토리 뷰

요 근래에 바쁜일이 많았습니다. 물론 이 공부도 중요한데 다른 일들을 미룰 수가 없더군요.
일단 이번 포스팅에서는 XNA를 통해서 Voice Recorder를 만드는 프로젝트를 해보고자 합니다. 물론 이 내용은 Rob Miles 교수님이 쓰신 Using Kinect for Windows with XNA 의 내용입니다.

우선 들어가기에 앞서서 키넥트에서의 음성처리를 언급해보고자 합니다.앞에서도 이야기 했었지만 키넥트에는 4개의 마이크 어레이가 내장되어서 이로써 소리가 나는 방향과 강도를 정확하게 측정할 수 있습니다. 그리고 키넥트를 연결하면 기본적으로 다음과 같이 인식됩니다.


여기서도 마이크 배열로 존재한다는게 보이네요. 참고로 마이크이기 때문에 우리가 말하는 것도 다 인지가 됩니다.
키넥트는 이렇게 받아온 오디오 정보를 16000개의 샘플로 나눠서 처리합니다. 그리고 각 샘플링된 정보는 16비트로 구성되는데 여기에는 각자가 다른 강도를 나타내지요. 사실 이정도는 일반적인 고음질의 오디오정보는 아니지만 그래도 발음까지는 인지할 수 있는 정도가 됩니다.

자 이제 본격적으로 들어가봅니다. VoiceRec이라는 이름으로 프로젝트를 생성합니다.
물론 Reference를 삽입하고 using 으로 네임스페이스를 처리하는 것은 다들 아시겠지요?

참 이번에는 Thread라는 개념을 사용할 겁니다. 이 Thread라는 개념이 왜 필요한가 하니 이게 동기화냐 비동기화냐를 구분해야 문제도 발생합니다. 조금 복잡하니까 우리가 해야되는 건 그냥 비동기화라고만 알아두시면 될거 같습니다. 그냥 이 녹음 기능을 사용하면 다른 응용 기능을 사용할건지 말건지에 대한 고민 정도로 여겨주시면 될거 같습니다.
아무튼 Thread를 사용하기 위해서는 다음과 같이 지시자를 사용해줘야 합니다.


(그런데 구현해놓고 보니 Threading에 관한 내용은 없네요. 공부한 후에 업로드하겠습니다.)

이제 뭔가 메세지를 표현해야 합니다. WPF와는 다르게 XNA에서 폰트를 표현하고자 할때는 spritefont라는 것을 삽입해야 합니다. 이 역시 content에서 삽입이 되어야 하겠구요.


이를 이용해서 화면상에 Message를 뿌릴 겁니다.

전반적인 프로그램의 운용에 대해서 고민을 해봐야 합니다.
우리가 이 프로그램을 실행시키면 어떤 과정이 있을까요? 우선 아무동작도 안하는 과정이 있을 것이고, 녹음을 하는 과정, 그리고 녹음한 것을 다시 듣는 과정 등으로 나눌 수 있겠지요. XNA에서는 enum이라는 변수를 사용해서 각 State에 대한 구분을 정의합니다. 그래서 다음과 같이 class의 바깥에 state를 정의하는 과정이 필요합니다.


그다음은 변수를 정의해줘야 합니다. 다음 사항을 정의합니다.


XNA에서만 볼수 있는 변수중 하나인 GamePadState와 KeyboardState는 각각 입력 장치로부터 받은 정보를 관리하는  변수입니다. old와 new로 나눠놓은 이유는 경우의 수를 나눴을 때 각각의 조건을 삽입하기 위해서 이전값과 비교하는 것이 필요합니다. 이 부분은 이렇게 XNA로 키에 따른 이벤트가 주어질 때 많이 사용하는 테크닉입니다. 뒷부분에서 이에 대한 조건을 언급하겠습니다. 

변수에 대한 정의가 이뤄졌으니 이제 각가의 상태를 정의해줘야 합니다. 이부분은 Update에서 이뤄집니다.


이전에 enum을 통해서 나눠졌던 state가 여기서 쓰입니다. 물론 입력할때의 key 상태와 그 값을 업데이트하기전에 저장해서 과거값으로 놔두는게 키포인트입니다. 그래서 이값을 비교한후 동작을 지정하게 되는 것이죠. 물론 화면에 나와있는 것처럼 각각 상태에 따른 메서드는 새로 만들어줘야 합니다.

먼저 UpdateIdle에 대한 메서드를 작성해보겠습니다.
여러분이 생각하시기엔 그냥 가만히 있는 Idle상태에선 어떤 동작이 이뤄져야 할까요? 두가지 경우를 고려해야 합니다. 이때 idle 상태에서 다시 들으려고 PlayBack 버튼을 눌렀을때, 또는 idle상태에서 마음에 안들어서 다시 녹음할때의 과정을 말입니다. 이걸 굳이 상황으로 나누자면 A키를 눌렀을때가 PlayBack이 되고 B키를 눌렀을 때 Recording 이 되는 것을 구현해야 합니다. 코드로는 다음과 같이 작성합니다.


일부러 키패드를 집어넣을 필요는 없지만 사실 이런 개발을 하는 이유는 어느 체제에서든 다양하게 쓸 수 있는 프로그램을 구현하는 것이 목적입니다. Miles 교수님도 이런 부분을 고려하고 프로그래밍을 했던 것 같습니다. 이제는 StartPlayback과 StartRecording을 구현해야 되겠네요.

참 그전에 우리가 짚고 가지 않았던 부분이 있네요. 일단 Kinect를 켜야 하겠지요. 앞에서 했던 것처럼 LoadContent부분에서 이뤄집니다.


일단 Nui를 시작해야지 키넥트가 정상적으로 작동할 겁니다. 그리고 혹여나 AudioStream에 관해서도 여기서 활성화시켜야 되는 건 아닌가 라고 생각하실 수도 있겠지만 생각해보면 Audio 기능을 사용할 때는 오직 A키를 눌렀을 때, 즉 녹음이 실행될 때입니다. 그러니 그 부분을 활성화 시키는 부분은 바로 StartRecording()에 들어가야 하겠지요.


본격적인 Startrecording과 StartPlayBack 구현입니다. 일단은 녹음 기능을 구현하기 위해서는 버퍼가 필요합니다. 그 버퍼의 크기를 변수 선언하는 곳에 끼워 넣습니다.


지금은 크기가 100000바이트인 버퍼를 구축한 겁니다. 대략 100kb 정도 되니까 음성으로 녹음하면 짧게만 녹음할 수 있겠네요. 물론 이 크기를 늘려서 더 많이 녹음할 수도 있습니다.


Recording을 시작하는 것은 매우 간단합니다. 그냥 켜주면 되니까요. 참고로 다른 Depth나 Color는 Enable이었지만 음원은 시작과 끝이 있기 때문에 엄연히 Start을 씁니다.
이렇게 함으로써 AudioSource가 계속 저장되는 겁니다. 어디선가는 그 AudioSource를 멈춰야 하겠지요. 그 것에 대한 내용을 UpdateRecording에서 해줘야 합니다.


Read메서드는 총 3개의 변수를 받습니다. 그중 첫번째가 앞에서 변수지정한 바이트형 배열, 그리고 오프셋, 크기 이렇게 입니다. 사실 우리는 녹음 분량이 많지 않기 때문에 오프셋은 지정할 필요가 없지요. 크기야 그 버퍼의 크기를 입력해주면 되겠고요. 이런 과정이 마무리되면 상태가 다시 idle이 되어야 합니다.

그런데 여기서 또 고민해봐야 할 점이 있습니다. 과연 처음의 idle 상태와 이렇게 녹음이 끝난 상태의 idle 상태가 같은 idle 상태일까요? 눈치채신 분도 있겠지만 서로가 다른 idle 상태인 겁니다. 실상 처음의 idle 상태는 녹음이 끝난 상태라면 다시 돌아올 필요는 없습니다. 처음만 그렇게 보여주면 되니까요. 그후로는 PlayBack할거냐 Recording을 할거냐만 물어보면 됩니다. 

이 상태에서 PlayBack을 구현해야 하는데 필요한 변수가 바로 DynamicSoundEffectInstance 입니다. 음원에 대한 환경 및 재생에 관련한 모든 효과를  이 변수가 속성으로 제공합니다. 그래서 이부분에 대한 정의가 필요합니다.


맨 앞에서 이야기 했었지만 키넥트 자체에선 16000개의 샘플링 레이트를 가집니다. 이를 지정해준겁니다. 

이걸 StartPlayback에서 써먹어야 합니다.


아까 임의로 만들어놓은 버퍼의 정보를 playback의 버퍼로 옮긴 후에 재생하는 과정이 필요합니다.

또 UpdatePlayback에서도 이 playback이 필요합니다. 여기서 좀 전에 언급한 초기 playback이냐 녹음후의 playback이냐를 찾게 됩니다. 여기서는 그냥 buffer의 재생이 끝났을 때 무조건 message를 띄우게 했네요.


당연히 재생이 끝나면 상태는 idle로 넘어가야 할것입니다. 여기까지가 동작에 대한 구현이었고 이제 message를 입력하면 되겠지요. 참고로 XNA에서는 한글로 쳐도 제대로 입력이 안됩니다. 그 이유는 font 자체가 이미지 형식으로 그려지기 때문에 일반 개발처럼 한글을 쓴다고 해서 출력이 되지 않습니다. 물론 입력하는 방법도 있지만 조금 손을 봐야 하기 때문에 이 부분은 넘어가도록 하겠습니다.
처음 message는 B키만 누르면 녹음이 되는 겁니다. 그걸 String 변수로 집어넣습니다.



마지막으로 Draw() 부분에서 DrawString을 통해 폰트 이미지를 그리면 됩니다.


결과물을 한번 보겠습니다.


처음에는 조금 짤리는 감이 있지만 녹음기능이 구현되어 있는 것을 확인할 수 있었습니다. 참고로 동기화냐 비동기화냐의 문제는 잠깐 보여드리겠습니다.


지금 상태를 보면 아시겠지만 아무리 드래그를 할려고 해도 안 먹힙니다. 이러면 다른 응용프로그램을 사용하는데 있어서 문제점을 야기합니다. 이때문에 Thread 기법을 사용해야 합니다.

이 부분은 공부를 더해보고 업데이트 하겠습니다.

댓글