# 음성압축

# 핵심 강의

동영상 준비 중

# 강의 개요

화상채팅의 중요 모듈 중 하나인 음성압축 모듈에 대한 사용법을 알아봅니다. 이 강의에서는 음성압축의 구체적인 원리를 다루지 않고 이미 만들어진 라이브러리를 사용하는 방법에 집중합니다.

# 강의 전 준비 사항

# 이 강의에서 다룰 내용

  • AudioInput: 음성 캡쳐 방법
  • AudioOutput: 음성 출력 방법
  • AudioEncoder: 음성 압축 방법
  • AudioDecoder: 음성 압축 해제 방법
  • AudioZip: AudioInput + AudioEncoder
  • AudioUnZip: AudioOutput + AudioDecoder

# AudioInput

오디오 입력 장치로부터 오디오를 캡쳐하는 클래스입니다. "소리 설정"에서 기본 입력 장치로 선택되어 있는 장치로 부터 오디오를 가져오게 됩니다.

# Class 구조

class AudioInput {
public:
	/** AudioInput 생성자
	@param channels 캡쳐할 오디오의 채널 수. 1: 모노, 2: 스테레오
	@param sampe_rate 캡쳐할 오디오의 sampling rate. 초당 캡쳐할 샘플링(오디오의 데이터) 개수
	@param fpb 한 번에 처리할 프레임의 갯수
	*/
	AudioInput(int channels, int sampe_rate, int fpb)

	/** 오디오 장치를 오픈
	@return 에러 코드가 리턴된다. 정상처리되면 0이 리턴된다.
	*/
	int open() 

	/** 오디오 장치를 닫는다. 오디오 캡쳐가 중단된다. */
	void close() 

	/** 오디오가 캡쳐되는 중인지 알려준다.
	@return true: 오디오 캡쳐 중, false: 오디오 캡쳐가 중단됨
	*/
	bool isActive()

	/** OnError 이벤트 핸들러를 지정한다.
	@param event 에러가 났을 때 실행될 이벤트 핸들러
	*/
	void setOnError(IntegerEvent event) { OnError_ = event; }

	/** OnData 이벤트 핸들러를 지정한다.
	@param event 오디오가 캡쳐되었을 때 실행될 이벤트 핸들러
	*/
	void setOnData(const DataEvent &value) { on_data_ = value; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 사용방법

#include <ryulib/AudioIO.hpp>

int main(void) {
	Audio::init();

	AudioInput audio_input(2, 48000, 3840);
	audio_input.setOnData([](const void* obj, const void* data, int size) {
		printf("buffer_size: %d \n", size);
	});
	audio_input.open();

	while (audio_input.isActive()) {
		Pa_Sleep(1000);
	}

	audio_input.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 1: AudioInput 클래스가 정의된 AudioIO.hpp 헤더를 가져옵니다.
  • 4: AudioIO를 사용하기 전에는 반드시 Audio::init() 함수를 실행하여 초기화가 되어야 합니다.
  • 6: 2채널(스테레오), 48000Hz(샘플 레이트)로 오디오를 캡쳐할 객체를 생성합니다.
  • 7-9: 오디오가 캡쳐됐을 때 실행할 핸들러를 작성합니다.
  • 8: 오디오가 캡쳐되면 오디오 데이터의 크기를 콘솔창에 출력합니다.
  • 10: 오디오 캡쳐를 시작합니다.
  • 12-14: 오디오 장치를 사용할 수 있을 때까지 계속 기다립니다. 반복하는 동안 CPU 사용률을 낮추기 위해서 Pa_Sleep(1000)를 반복하고 있습니다.
  • 16: 오디오 캡쳐를 중단합니다.

# 실행결과

  • 이미지의 앞 부분은 Audio::init() 함수에 의한 초기화과정이 표시된 것 입니다.
  • "buffer_size: 30720"가 반복되는 것을 보실 수가 있습니다. 오디오 입력 창치로부터 가져온 데이터의 크기 입니다.

# AudioEncoder

OUPS 코덱을 이용해서 오디오를 압축해주는 클래스입니다.

# Class 구조

class AudioEncoder {
public:
	/** AudioEncoder 생성자
	@param channels 오디오의 채널 수. 1: 모노, 2: 스테레오
	@param sampe_rate 오디오의 sampling rate. 초당 캡쳐할 샘플링(오디오의 데이터) 개수
	*/
	AudioEncoder(int channels, int sampe_rate)

	/** 압축하려는 오디오 데이터를 큐에 추가합니다. 오디오 압축은 내부 스레드에서 비동기로 처리됩니다.
	@param data 오디오 데이터의 주소
	@param size 오디오 데이터의 크기
	*/
	void add(const void* data, int size) 

	/** OnError 이벤트 핸들러를 지정한다.
	@param event 에러가 났을 때 실행될 이벤트 핸들러
	*/
	void setOnError(IntegerEvent event)

	/** OnEncode 이벤트 핸들러를 지정한다.
	@param event 오디오가 압축되었을 때 실행될 이벤트 핸들러
	*/
	void setOnEncode(DataEvent event)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 사용방법

#include <ryulib/AudioIO.hpp>
#include <ryulib/AudioEncoder.hpp>

int main(void) {
	Audio::init();

	AudioEncoder encoder(2, 48000);
	encoder.setOnEncode([&](const void* obj, const void* data, int size) {
		printf("Encoded size: %d \n", size);
	});

	AudioInput audio_input(2, 48000, 3840);
	audio_input.setOnData([&](const void* obj, const void* buffer, int buffer_size) {
		printf("original size: %d \n", buffer_size);
		encoder.add(buffer, buffer_size);
	});
	audio_input.open();

	while (audio_input.isActive()) {
		Pa_Sleep(1000);
	}

	audio_input.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • 1: AudioInput 클래스가 정의된 AudioIO.hpp 헤더를 가져옵니다.
  • 2: AudioEncoder 클래스가 정의된 AudioEncoder.hpp 헤더를 가져옵니다.
  • 5: AudioIO를 사용하기 전에는 반드시 Audio::init() 함수를 실행하여 초기화가 되어야 합니다.
  • 7: 2채널(스테레오), 48000Hz(샘플 레이트) 오디오를 압축할 객체를 생성합니다.
  • 8-10: 오디오 압축이 완료됐을 때 실행할 핸들러를 작성합니다.
  • 9: 압축된 오디오 데이터의 크기를 콘솔 화면에 표시합니다.
  • 15: 캡쳐된 오디오 데이터를 encoder 객체에게 압축해달라고 의뢰합니다.

나머지 코드는 AudioInput 사용방법과 같기 때문에 생략하였습니다.

# 실행결과

  • "original size: 30720"에 이어서 "Encoded size: 760"과 같이 압축된 데이터의 크기가 콘솔 화면에 나타납니다. 압축된 데이터의 크기는 조금씩 다른 것을 볼 수가 있는데요, 캡쳐된 오디오가 압축하기에 유리한 형태이면 압축된 크기도 작아지는 것을 알 수 있습니다.

# AudioZip과 AudioUnZip

오디오 압축과 해제를 좀 더 쉽게 사용할 수 있도록 오디오 입출력, 인코딩 그리고 디코딩을 묶어서 단순하게 포장한 클래스입니다.

# AudioZip class 구조

class AudioZip {
public:
	/** AudioZip 생성자
	@param channels 오디오의 채널 수. 1: 모노, 2: 스테레오
	@param sampe_rate 오디오의 sampling rate. 초당 캡쳐할 샘플링(오디오의 데이터) 개수
	*/
	AudioZip(int channels, int sampe_rate)

	/** 오디오 캡쳐 및 압축을 시작한다. */
	void start() 

	/** 오디오 캡쳐 및 압축을 중단한다. */
	void stop() 

	/** OnError 이벤트 핸들러를 지정한다.
	@param event 에러가 났을 때 실행될 이벤트 핸들러
	*/
	void setOnError(IntegerEvent event)

	/** OnEncode 이벤트 핸들러를 지정한다.
	@param event 오디오가 압축되었을 때 실행될 이벤트 핸들러
	*/
	void setOnEncode(DataEvent event)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# AudioUnZip class 구조

class AudioUnZip {
public:
	/** AudioUnZip 생성자
	@param channels 오디오의 채널 수. 1: 모노, 2: 스테레오
	@param sampe_rate 오디오의 sampling rate. 초당 캡쳐할 샘플링(오디오의 데이터) 개수
	*/
	AudioUnZip(int channels, int sampe_rate)

	/** 압축된 데이터를 압축해제하고 재생을 한다.
	@param data 압축된 오디오 데이터의 주소
	@param size 압축된 오디오 데이터의 크기
	*/
	void play(const void* data, int size)

	/** OnError 이벤트 핸들러를 지정한다.
	@param event 에러가 났을 때 실행될 이벤트 핸들러
	*/
	void setOnError(IntegerEvent event)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 사용방법

#include <ryulib/AudioZip.hpp>
#include <ryulib/AudioUnZip.hpp>

int main(void) {
	Audio::init();

	AudioUnZip unzip(1, 48000);
	unzip.setOnError([&](const void* obj, int error_code) {
		printf("AudioUnZip - error: %d", error_code);
	});

	AudioZip zip(1, 48000);
	zip.setOnError([&](const void* obj, int error_code) {
		printf("AudioZip - error: %d", error_code);
	});
	zip.setOnEncode([&](const void* obj, const void* data, int size) {
		unzip.play(data, size);
	});

	zip.start();

	while (true) {
		Pa_Sleep(1000);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  • 7: 오디오를 압축해제하고 재생할 객체를 만듭니다.
  • 8-9: 오디오 압축해제 및 재생 과정에서 오류가 나면 처리할 이벤트 핸들러를 작성합니다.
  • 12: 오디오를 캡쳐하고 압축할 객체를 만듭니다.
  • 13-15: 오디오 캡쳐 및 압축 과정에서 오류가 나면 처리할 이벤트 핸들러를 작성합니다.
  • 16-18: 오디오 압축이 완료될 때 실행할 이벤트 핸들러를 작성합니다.
  • 17: 압축된 데이터를 바로 unzip 객체에 전달하여 압축해제 및 재생을 의뢰합니다.
  • 20: 오디오 캡쳐 및 압축을 시작합니다.

위의 예제가 그다지 길지는 않지만 이벤트로 엮여 있기 때문에 초보분들에게는 코드의 흐름을 머릿속에 담기에 어려울 수도 있는데요, 아래 다이어그램으로 표현해보면 코드의 흐름이 좀 더 쉽게 이해할 수 있습니다.

  • AudioZip.start() 메소드 실행이 되면 OnEncode 이벤트가 발생합니다.
  • AudioZip.OnEncode 이벤트가 발생하면 압축된 데이터를 AudioUnZip.play() 메소드에 전달하여 재생을 의뢰합니다.
  • 각 과정에서 에러가 발견되면 OnError 이벤트가 발생합니다. 상황에 맞는 에러처리를 추가해주시면 됩니다.

# 테스트 방법

위의 코드를 실행하면 마이크(또는 오디오 입력 장치)에서 오디오를 캡쳐하여 압축한 뒤 바로 AudioUnZip 객체에게 전달하여 압축을 해제하고 재생하게 됩니다. 따라서, 여러분들이 마이크에 소리를 입력하면 바로 스피커로 출력되는 것을 확인할 수 있습니다.

이때 압축 시간과 해제 시간 등에 의해서 소리가 조금 간격을 두고 메아리처럼 들리게 됩니다. 스피커의 소리가 너무 커서 마이크에 너무 많이 유입되면 소리가 점점 커지면서 하울링이 발생하니 조심하시기 바랍니다.

에코 캔슬

화상회의에서 해드셋을 사용하지 않고 스피커를 사용하게 되면, 상대방의 목소리가 내 마이크로 유입되어 상대방에게 전달됩니다. 이 때문에 상대방은 자신의 목소리가 다시 자신에게 들리는 현상이 발생합니다. 이것이 반복되면 소리가 점점 커져서 소음이 심하게 발생되는데요, 이것을 제거해주는 기술을 에코 캔슬이라고 합니다.

에코 캔슬에 대해서는 이 프로젝트의 마지막 부분에서 다뤄볼 예정입니다.

참고 링크