ALSA란?
위키피디아의 설명에 따르면 ALSA(Advanced Linux Sound Architecture)는 리눅스 커널의 구성요소중 하나로 사운드 카드용 장치 드라이버를 위한 API를 제공하는 소프트웨어 프레임워크이다. 이해한바에 따르면 각기 다른 사운드 장치에 대해서 통합된 인터페이스를 제공해주는 역할을 하는 것이 ALSA라고 생각하면 된다.
이 글을 쓰게 된 이유
Ubunutu에서 사운드를 재생할 때는 pulseaudio를 사용한다. pulseaudio는 server client구조로 이루어져 있기 때문에 각 app에서 client를 만들고 이를 통해 음원 data를 pulseaudio server로 전달하는 과정이 Ubuntu에서 사운드 재생을 할 때 사용하는 코드이다. pulseaudio server는 client로부터 음원 데이터를 취합하여 이를 mixing한 결과를 장치로 재생해 주는데, 그러면 실제로 취합된 data를 어떻게 스피커로 전달하는지? 라는 의문이 생겼다.
이 과정은 장치 마다 다른데 bluetooth 장치인 경우에는 pulseaudio가 bluetooth 관련 인터페이스를 제공하는 bluez라이브러리를 통해서 재생하고, 일반적인 유선 장치의 경우에는 Linux의 ALSA를 사용하여 재생하게 된다.
즉 위의 의문인 음원 data가 어떻게 스피커로 전달되는지, 정확히 얘기하면 “유선 스피커”로 어떻게 전달되는지를 알고 싶다면, ALSA가 어떻게 동작하는지를 파악해야한다. 이번 post에서는 ALSA를 이용하여 간단한 음원 재생 코드를 분석해보고, 각 재생코드의 함수를 하나하나 뜯어가면서 실제 음원이 어떻게 재생되는 지를 알아보고자 한다.
해당 코드는 이 링크 에서 참고 했다.
Audio application의 구조
아래 코드는 psuedo-code인데 실제 audio data가 process되는 과정을 간단하게 표현하고 있다.
open_the_device();
set_the_parameters_of_the_device();
while (!done) {
/* one or both of these */
receive_audio_data_from_the_device();
deliver_audio_data_to_the_device();
}
close the device
크게 네가지의 부분으로 나뉘는데 첫 두줄이 초기화 과정이며 while문은 device에 데이터를 보내거나 받는 과정이고 마지막은 자원을 정리하는 과정이다.
- 장치 선택
- open_the_device : 즉 어떤 장치가 사운드를 내보낼지를 정하는 과정이다. 보통 ALSA에서는 sound_card 몇번의 몇번 장치 라는 형식으로 표현되는 듯 하다.
- 장치 설정
- set_the_parameters_of_the_device : 장치의 스펙을 결정한다. 장치의 channel을 몇개를 사용할 것인지, 각 음원 데이터 표현을 어떻게 할 것인지, 초당 몇개의 frame을 보낼 것인지에 대한 정보를 여기서 결정한다.
- 음원 전송 및 수신
- receive_audio_data_from_the_device : 장치에 들어온 음원 데이터를 받는다
- deliver_audio_data_to_the_device : 장치로 내가 재생할 음원 데이터를 보낸다
- 정리
- close the device
A Minimal Playback Program
마지막으로 ALSA를 이용해서 audio data를 재생하는 코드를 리뷰해보자.
코드 전문은 아래와 같다.
#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
main (int argc, char *argv[]) {
int i;
int err;
short buf[128];
snd_pcm_t *playback_handle;
snd_pcm_hw_params_t *hw_params;
if ((err = snd_pcm_open (&playback_handle, argv[1], SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
fprintf (stderr, "cannot open audio device %s (%s)\n",
argv[1],
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_malloc (&hw_params)) < 0) {
fprintf (stderr, "cannot allocate hardware parameter structure (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_any (playback_handle, hw_params)) < 0) {
fprintf (stderr, "cannot initialize hardware parameter structure (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_access (playback_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
fprintf (stderr, "cannot set access type (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_format (playback_handle, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) {
fprintf (stderr, "cannot set sample format (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_rate_near (playback_handle, hw_params, 44100, 0)) < 0) {
fprintf (stderr, "cannot set sample rate (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params_set_channels (playback_handle, hw_params, 2)) < 0) {
fprintf (stderr, "cannot set channel count (%s)\n",
snd_strerror (err));
exit (1);
}
if ((err = snd_pcm_hw_params (playback_handle, hw_params)) < 0) {
fprintf (stderr, "cannot set parameters (%s)\n",
snd_strerror (err));
exit (1);
}
snd_pcm_hw_params_free (hw_params);
if ((err = snd_pcm_prepare (playback_handle)) < 0) {
fprintf (stderr, "cannot prepare audio interface for use (%s)\n",
snd_strerror (err));
exit (1);
}
for (i = 0; i < 10; ++i) {
if ((err = snd_pcm_writei (playback_handle, buf, 128)) != 128) {
fprintf (stderr, "write to audio interface failed (%s)\n",
snd_strerror (err));
exit (1);
}
}
snd_pcm_close (playback_handle);
exit (0);
}
앞에서 얘기했던 바와 같이 코드를 크게 세부분으로 나눠서 다시 보자
- 장치 찾기
snd_pcm_open snd_pcm_hw_params_malloc snd_pcm_hw_params_any
snd_pcm_open을 통해서 playback_handle이라고 하는 장치 handle을 받는다. 이 handle을 통해서 sound_device에 대한 control이 가능하다.
snd_pcm_hw_params_malloc, snd_pcm_hw_params_any를 통해서 장치의 각종 설정값에 대한 구조체(hw_params)를 초기화한다.
- 장치 설정
snd_pcm_hw_params_set_access snd_pcm_hw_params_set_format snd_pcm_hw_params_set_rate_near snd_pcm_hw_params_set_channels snd_pcm_hw_params snd_pcm_hw_params_free
snd_pcm_hw_params_set 함수를 통해서 hw_params에 값들을 설정한다. 이때 sound_device에 접근해서 해당 설정값이 유효한지를 검증하고 불가능하면 함수가 실패한다.
snd_pcm_hw_params에서 hw_params에 설정된 값들을 실제 device에 적용한다.
이제 hw_params는 쓰이지 않으므로 snd_pcm_hw_params_free를 통해 정리한다.
- audio data를 장치로 전송
snd_pcm_prepare snd_pcm_writei
snd_pcm_prepare : pcm 준비
snd_pcm_writei : pcm data를 audio device에 전달한다.
- 정리
snd_pcm_close