2010년 3월 29일 월요일

리눅스 사운드 프로그래밍

음악이 생겨라 (Let There Be Music)

By Shuveb Hussain 
한글번역 전정호 
이 글은 한글번역판입니다. 원문은 여기에서 볼 수 있습니다.

리눅스 멀티미디어에는 여러 활동이 있어 왔다. 많은 사람들이 리눅스를 멋진 멀티미디어 플래폼으로 만들기위해 노력해왔다. 현재도 많은 리눅스 멀티미디어 프로젝트들이 진행중이다. 여러 리눅스용 사운드 도구에 대한 소개가 필요하다면, 훌륭한 이전 Linux Gazette 기사를 참고하라.

나는 새로 작성할 프로그램에 음악을 포함하고 싶어서 웹을 검색했다. Ogg Vorbis나 MP3와 같은 압축 음악파일 형식을 변환할 수 있는 라이브러리가 많았다. 스피커로 음을 재생하는 것도 어려운 일이 아니였다. 이 글은 간단한 프로그램을 작성하여 Ogg Vorbis와 MP3 파일을 연주하는 방법을 설명한다. 이 글은 초보자를 염두에 두고 썼다. 그러니 리눅스 고수에게는 간단한 것을 너무 길게 설명하여 미안하다.

사운드카드 프로그래밍

사운드카드 프로그래밍을 살펴보기 전에 어떻게 컴퓨터가 음악 자료를 녹음, 저장, 재생하는지 알아보자.

샘플링(sampling)

음을 녹음할때 음질을 선택한다. 음질을 결정하는 중요한 요소중 하나가 샘플링 빈도(sampling rate)이다. 음은 시간에 따라 진동수가 바뀌는 파장일 뿐이다. 음을 디지털로 녹음하려면 일정 간격으로 음의 진동수를 기록한다. 이 간격을 샘플링 빈도라고 한다. 샘플링 빈도가 높을수록 음질이 좋아진다. 예를 들어, CD의 샘플링 빈도는 초당 44100번, 즉 44100 Hz이다. PCM(Pulse Code Modulation, 펄스 코드 변조)이라고 하는 이 작업은 ADC(Analogue to Digital Converter, 아날로그-디지털 변환기)가 한다.

비트와 채널(channel)

음을 샘플링할때, 샘플당 일정 수의 비트를 사용하고, 이런 채널을 여러개 사용한다. 샘플링하는 자료의 비트개수를 샘플링 정밀도(resolution)라고 한다. 가장 흔한 형식은 8 비트 양수 바이트 혹은 16 비트이다. 채널별로 자료를 분리한다. 예를 들어, CD 음악에는 왼쪽, 오른쪽 (스테레오) 두 채널이 있다. 8 비트 정밀도로 CD 음질을 녹음할때 초당 필요한 크기를 계산해보면:

		2 x 44100 = 88200 bytes

	 (채널 개수 x 샘플링 빈도)

이제 이론은 충분하니 실제 프로그래밍을 시작하자. 사운드카드를 프로그래밍하는 방법이 몇가지 있다. 초기 리눅스 드라이버가 만들어지던때 두 그룹이 있었다. Open Sound System (OSS)은 많은 장치드라이버를 작성한 그룹으로 심지어 하드웨어 사양을 공개하지 않거나 비밀유지동의(non-disclusure agreement)를 하지않은 개발자에게 사양을 공개하지않는 사운드카드 제조사의 이진모듈도 포함하였다. 하드웨어 제조사가 개인에게 사양을 알려주려고하지 않았기때문에 OSS는 4Front Technologies란 회사를 만들었다. 다른 그룹인 ALSA (Advanced Linux Sound Architecture)는 제조사가 사양을 공개한 사운드카드 드라이버만을 작성했다. ALSA는 2.6-test 커널에 포함되있다.

ALSA는 OSS 에뮬레이션계층을 제공하기때문에 OSS로 작성한 프로그램은 OSS와 ALSA 시스템에서 모두 동작한다. 간단하고 대다수의 시스템에 설치되있기때문에 이 글은 OSS 프로그래밍을 다룰 것이다.

유닉스/리눅스는 모든 장치드라이버마다 (보통 /dev 디렉토리에) 파일시스템 인터페이스가 있다. 이 장치파일을 open,read,write,lseek,close 같은 파일관련 시스템호출에 사용한다. 장치파일을 장치드라이버가 요청을 기다리는 파일시스템상의 훅(hook)이라고 보면 된다. 예를 들어, /dev/hda1 파일은 primary master IDE 하드디스크의 첫번째 파티션에 대한 인터페이스이다. open 시스템호출을 사용하여 파일을 열면 일반파일과 같이 파일핸들(file handle)을 얻을 수 있다. 파일핸들을 읽으면 실제로 파티션의 첫번째 섹터에 있는 자료를 읽게 된다! 또, 앞으로 lseek하면 파일포인터(file pointer)를 앞으로 이동한다. 섹터를 건너뛰으려면 섹터 크기만큼 앞으로 lseek한다. 위험하므로 하드디스크 장치파일을 수정하지는 마라. 파티션에 값을 쓰면 파티션이 망가질 수 있다. 하드웨어 장치는 보통 root만 직접 접근할 수 있다.

이제 코드로 넘어가자. 이 코드는 root가 아니여도 문제없이 실행되야 한다. 장치파일을 접근하는데 문제가 있다면 root로 su하고 chmod하여 접근권한을 푼다. 물론 리눅스에서 작동하는 사운드카드를 사용해야 한다. 직접 코드를 입력하지않고 여기에서 다운받을 수 있다. demo.pcm 파일도 같이 다운받아야 한다.

/*
	oss.c는 사운드카드에서 raw PCM 22KHz 샘플음을 낸다
	
	중요 - 프로그램을 실행하기전에 현재 디렉토리에
	demo.pcm 파일이 있는지 확인하라.
*/

#include < sys/types.h >
#include < sys/stat.h >
#include < sys/soundcard.h >
#include < sys/ioctl.h >
#include < unistd.h >
#include < fcntl.h >
#include < errno.h >
#include < stdlib.h >

#define SECONDS 5 //재생 시간 (초)

int main() 
{
	int fd;
	int handle = -1;
	int channels = 1;         // 0=모노 1=스테레오
	int format = AFMT_U8;
	int rate = 22000;
	unsigned char* data;
	
/* 사운드카드에 해당하는 파일에 쓰기위해(write) 파일을 연다(open). DSP = Digital Signal Processor */
	if ( (handle = open("/dev/dsp",O_WRONLY)) == -1 )
	{
		perror("open /dev/dsp");
		return -1;
	}
/* 재생하려는 음이 스테레오라고 사운드카드에 알린다. 0=모노 1=스테레오 */

	if ( ioctl(handle, SNDCTL_DSP_STEREO,&channels) == -1 )
	{
		perror("ioctl stereo");
		return errno;
	}

	/* 자료 형식을 사운드카드에게 알린다 */
	if ( ioctl(handle, SNDCTL_DSP_SETFMT,&format) == -1 )
	{
		perror("ioctl format");
		return errno;
	}
	
	/* DSP 재생율(playback rate), 즉 raw PCM 음의 샘플링 빈도를 지정한다. */
	if (ioctl(handle, SNDCTL_DSP_SPEED,&rate) == -1 )
	{
		perror("ioctl sample rate");
		return errno;
	}
   
	// 빈도 * 5 초 * 두 채널
	data = malloc(rate*SECONDS*(channels+1));
	
	if((fd=open("demo.pcm",O_RDONLY))==-1)
	{
		perror("open file");
		exit(-1);
	}

	/* demo 파일에 저장된 정보를 읽어서 할당한 메모리에 저장한다 */
	read(fd,data,rate*SECONDS*(channels+1));
	close(fd);
	
	/* 읽은 내용을 사운드카드에 쓴다(write)! 그러면 재생이 된다. */

	write(handle, data, rate*SECONDS*(channels+1));

	if (ioctl(handle, SNDCTL_DSP_SYNC) == -1)
	{
		perror("ioctl sync");
		return errno;
    	}

	free(data); //좋다. 마무리.
	close(handle);
	printf("
Done
");
	return 0;
}

이 프로그램은 raw PCM, 22 KHz, 스테레오 자료파일을 5초간 연주한다. 프로그램은 크게 세가지 작업을 한다:

a. sound 장치 열기(open)
b. 재생을 위한 파라미터 설정
c. 장치에 자료 쓰기(write)

open()/write()/close() 시스템호출은 일반파일과 동일하다. ioctl() (Input/Ouput ConTroL) 시스템호출을 사용하여 재생에 사용할 파라미터를 설정한다. ioctl() 시스템호출은 입출력 장치와 통신하거나 파라미터를 설정할때 사용한다. 또, 시스템호출의 개수를 줄이기위한 편법으로 사용되기도 한다. 그래서 흔히 프로그래머의 다용도칼이라고 부른다. ioctl() 시스템호출의 함수형은:

int ioctl(int fd, int command, ...)

첫번째 파라미터는 장치의 파일기술자(file descriptor)이고, 두번째 파라미터는 장치에 대한 요청/명령, 나머지 파라미터들은 두번째 파라미터에 따라 다르다 [명령 목록은 ioctl_list manpage를 참고]. 다음을 보자:

ioctl(handle, SNDCTL_DSP_SPEED,&rate)

이 ioctl() 호출은 DSP 재생율을 설정한다. 세번째 파라미터는 장치드라이버로 전달되는 실제 재생율이다.

예제 프로그램에는 세가지 ioctl() 호출이 있다. 첫번째는 재생에 사용할 채널개수를 설정하고, 두번째는 PCM 형식을, 세번째는 재생율을 지정한다. 이것으로 장치에 필요한 정보는 충분하고, 다음은 장치파일에 PCM 자료를 직접 쓰면(write) 재생이 된다. 마지막 ioctl()은 장치를 비우고(flush), 그 다음 PCM 자료를 저장하기위해 할당한 메모리를 해제한다. 이게 끝이다.

음악 저장 형식

방금 전에 5초짜리 22KHz 음악을 재생하기위해 raw PCM 자료 220 KB가 필요했다.

	
	22,000 x 5 x 2 = 220,000
	(샘플 x 초 x 채널)
	
	CD 음질로 1분을 재생하기위해 필요한 크기는:

	44,100 x 60 x 2 = 5292000 바이트, 약 5 MB!

음악CD는 raw PCM 형식으로 자료를 저장한다. 그러나 컴퓨터는 음악을 효율적으로 저장하고 (인터넷으로) 전송하기위해 음악을 압축하여 크기를 줄이고 재생시 다시 압축을 푼다. 어떤 음악이던 관계없이 샘플링률은 고정되있다. 아무 소리가 없는 몇초간을 샘플링했다고 생각해보자. 같은 시간의 시끄러운 락음악과 동일한 크기가 필요하다. raw 음악파일은 자료반복이 매우 적기때문에 일반적인 압축방법으로는 잘 압축되지 않는다. 음악은 다른 기술을 사용하여 압축한다. 가장 일반적인 방법은 사람이 귀로 들을 수 없는 부분을 제거하거나 음악 특성에 따라 압축한다. 음악을 좋아하는 사람들에게 가장 대중적인 형식은 분명 MP3다. 현재 MP3에는 특허문제가 걸려있기때문에, 오픈소스 공동체에서는 Ogg Vorbis 형식을 반긴다. 두 형식 모두 크기가 비슷하게 줄어들며 음질의 손실을 최소화한다.

사운드카드가 어떻게 raw 음악자료를 재생하는지 알았으니, Ogg Vorbis 파일을 재생하는 방법을 알아보자. Ogg Vorbis 파일을 재생하려면 먼저 복호화(decode), 즉 raw PCM 자료로 변환해야 한다. libvorbisfile이 이 작업을 한다. 이 라이브러리는 매우 낮은 수준을 다루지만 더 세밀한 조절이 가능한 libvorbis 라이브러리보다 높은 수준에서 변환을 한다. 이제 문제가 간단해진다: Ogg Vorbis 파일에서 읽은 자료를 라이브러리로 넘기고, 라이브러리가 변환한 raw PCM 자료를 사운드카드로 직접 보낸다. 소스는 여기에

#include < sys/types.h >
#include < sys/stat.h >
#include < sys/soundcard.h >
#include < sys/ioctl.h >
#include < unistd.h >
#include < fcntl.h >
#include < errno.h >
#include < stdlib.h >

#include "vorbis/codec.h"
#include "vorbisfile.h"

int setup_dsp(int fd,int rate, int channels);


char pcmout[4096]; // 변환한 PCM 자료를 저장할 4KB 버퍼
int dev_fd;

int main(int argc, char **argv)
{
	OggVorbis_File vf;
	int eof=0;
	int current_section;
	FILE *infile,*outfile;

	if(argc<2)
	{
		printf("supply file arguement
");
		exit(0);
	}

	if ( (dev_fd = open("/dev/dsp",O_WRONLY)) == -1 ) 
	{
		perror("open /dev/dsp");
		return -1;
	}

	infile=fopen(argv[1],"r");
	
	if(infile==NULL)
	{
		perror("fopen");
		exit(-1);
	}   

	if(ov_open(infile, &vf, NULL, 0) < 0) 
	{
		fprintf(stderr,"Input does not appear to be an Ogg bitstream.
");
		exit(1);
	}
	
	char **ptr=ov_comment(&vf,-1)->user_comments;
	vorbis_info *vi=ov_info(&vf,-1);
	while(*ptr)
	{
		fprintf(stderr,"%s
",*ptr);
		++ptr;
	}

	fprintf(stderr,"
Bitstream is %d channel, %ldHz
",vi->channels,vi->rate);
	fprintf(stderr,"
Decoded length: %ld samples
",(long)ov_pcm_total(&vf,-1));
	fprintf(stderr,"Encoded by: %s

",ov_comment(&vf,-1)->vendor);
  
	if(setup_dsp(dev_fd,vi->rate,vi->channels-1))
	{
		printf("dsp setup error.aborting
");
		exit(-1);
	}

	int count=0;
	
	while(!eof)
	{
		long ret=ov_read(&vf,pcmout,sizeof(pcmout),0,2,1,&current_section);
		if (ret == 0)
		{
			/* EOF */
			eof=1;
		}
		else if (ret < 0)
		{
	      		/* 자료에 오류가 있다.  (프로그램에) 중요하다면 보고하지만,
			이 프로그램은 하지 않는다. */
		}
		else 
		{
			printf("Writing %d bytes for the %d time.
",ret,++count);
			write(dev_fd,pcmout,ret);
		}
	}

	ov_clear(&vf);
	fclose(infile);
	
	if (ioctl(dev_fd, SNDCTL_DSP_SYNC) == -1) 
	{
    		perror("ioctl sync");
		return errno;
	}
	close(dev_fd);
	fprintf(stderr,"Done.
");
	return(0);
}

int setup_dsp(int handle,int rate, int channels)
{
	int format;

	if ( ioctl(handle, SNDCTL_DSP_STEREO,&channels) == -1 ) 
	{
		perror("ioctl stereo");
		return errno;
	}
	
	format=AFMT_U8;
	if ( ioctl(handle, SNDCTL_DSP_SETFMT,&format) == -1 )
	{
		perror("ioctl format");
		return errno;
	}
	
	if ( ioctl(handle, SNDCTL_DSP_SPEED,&rate) == -1 )
	{
		perror("ioctl sample rate");
		return errno;
	}
	
	return 0;
}

다음과 같이 프로그램을 컴파일한다
gcc oggplay.c -oggplay -lvorbisfile -I/path/to/vorbis/header/files -L/path/to/vorbis/lib/files

라이브러리파일과 헤더파일이 /usr/lib 이나 /usr/include 같은 일반적인 위치에 있다면 -l 과 -L 명령행 옵션이 필요없다. 모든 대중적인 배포본에 vorbisfile 라이브러리가 포함되있다. 만약 시스템에 설치가 안되있다면 여기에서 다운받을 수 있다. 또, Ogg Vorbis 사이트는 여기다. 프로그램을 컴파일하려면 (개발용) 헤더파일이 필요하다.

setup_dsp 함수는 dsp 장치를 열고(open), 세가지 중요한 파라미터인 채널, 형식, 재생율을 지정한다. 음악 재생을 위해 가장 중요한 세가지 파라미터를 기억하라. main 함수는 오류검사외에, 쉘 아규먼트로 준 Ogg Vorbis 파일을 열고, 파일에 저장된 설명과 파일정보를 출력한다. ov_open() 라이브러리함수가 채우는 구조체에 저장된 샘플링 빈도와 채널 정보를 setup_dsp 함수에 넘긴다. 그후 프로그램은 Ogg Vorbis 파일 내용을 변환하여 버퍼에 넣는 반복문을 실행한다. 반복문은 버퍼에 저장한 raw PCM 자료를 앞에서 설정한 dsp 장치에 쓴다. 파일이 끝날때까지 반복한 후, 마지막으로 정리하고 종료한다. 간단하지 않는가? 쉽고 직관적으로 변환해준 libvorbisfile에게 감사하다.

Ogg Vorbis 파일을 변환하려면 libvorbisfile을 사용하면 된다. MP3 파일을 변환하여 재생하고 싶다면 여러 라이브러리를 선택할 수 있다. 많이 사용하는 라이브러리중에 영상과 음악을 모두 변환할 수 있는 smpeg 라이브러리가 있다. 또, libmad 라이브러리도 찾았다. 나는 smpeg의 음악 변환 기능만 사용하기로 했다. 관심이 있다면 smpeg의 MPEG 영상 변환 기능도 살펴보길 바란다. 라이브러리 사용법을 설명하는 샘플 프로그램 plaympeg도 포함되있다. 영상을 보여주기위해 smpeg은 매우 사용하기 쉽고 기능이 많은 그래픽 프로그래밍 라이브러리인 SDL을 사용한다. 이외에도 MP3를 변환하는 라이브러리는 많지만, 많은 리눅스 배포본에 smpeg이 포함되있기때문에 smpeg을 선택했다. 많이 사용하는 gtv MPEG 영상플레이어도 smpeg 패키지에 포함되있다.

다음은 MP3 파일을 연주하는 예제 프로그램이다. smpeg 라이브러리가 변환한 MP3 자료를 직접 사운드카드로 보낸다. 직접 PCM을 다룰 필요가 없다. 소스 다운로드

#include < stdio.h >
#include < stdlib.h >
#include < string.h >
#include < signal.h >
#include < unistd.h >
#include < errno.h >
#include < sys/types.h >
#include < sys/stat.h >
#include < sys/ioctl.h >
#include < sys/time.h >

#include "smpeg.h"

void usage(char *argv0)
{
	printf("

Hi,

%s 

This is the normal useage.
",argv0);
}

int main(int argc, char *argv[])
{
    int volume;
    SMPEG *mpeg;
    SMPEG_Info info;
   
    volume = 100; //Volume level
    
    /* 아규먼트가 없으면 사용법만 출력한다 */
	if (argc == 1) 
	{
	        usage(argv[0]);
	        return(0);
	}

	mpeg = SMPEG_new(argv[1], &info;, 1);

        if ( SMPEG_error(mpeg) ) 
	{
            fprintf(stderr, "%s: %s
", argv[1], SMPEG_error(mpeg));
            SMPEG_delete(mpeg);
        }
	
        SMPEG_enableaudio(mpeg, 1);
        SMPEG_setvolume(mpeg, volume);

        /* 음악에 대한 정보를 출력한다 */
        if ( info.has_audio ) 
	{
            printf("%s: MPEG audio stream
", argv[1]);
            
	        if ( info.has_audio ) 
		    printf("	Audio %s
", info.audio_string);

        	if ( info.total_size ) 
			printf("	Size: %d
", info.total_size);
	        
		if ( info.total_time )
			printf("	Total time: %f
", info.total_time);

		/* 재생하고 재생이 끌날때까지 기다린다 */
		SMPEG_play(mpeg);
        
		while(SMPEG_status(mpeg)==SMPEG_PLAYING);
        	SMPEG_delete(mpeg); //구조체 반환
	}
	return(0);
}

여기에 있는 smpeg을 설치한후 프로그램을 컴파일한다. smpeg을 설치하기위해서 미리 SDL을 설치해야 한다. 컴파일할때 헤더파일과 라이브러리파일 경로를 컴파일러에게 알려줘야 한다. 쉽게 하려고 smpeg 패키지는 smpeg-config라는 간단한 도구를 제공한다. 다음과 같이 프로그램을 컴파일한다:

gcc playmp3.c `smpeg-config --cflags --libs` -I/usr/include/SDL

smpeg-config 명령어를 감싸는 따옴표문자를 주의하라. 보통 backqoute라고 부르며, US식 키보드에서 틸데(~) 문자와 같이 있다. 쉘에서 이 문자는 안에 있는 명령어의 출력을 그 자리에 대신한다. 그냥 smpeg-config 명령만 실행해보면 내가 무슨 말을 하는지 알 것이다. -I/usr/include/SDL 명령행 옵션을 -I < 실제 SDL 경로 >로 수정하라. SDL 함수를 사용하지 않아도, smpeg.h가 내부적으로 SDL 헤더파일을 사용하므로 SDL 헤더파일 경로를 지정해야 한다. RPM 사용자는 프로그램을 컴파일하기위해 smpeg과 smpeg-devel 패키지를 설치해야 한다.

SMPEG_new 라이브러리함수는 필요한 내부 객체를 할당하고, MP3 파일에 대한 정보를 SMPEG_info 구조체에 채운다. 프로그램은 이 정보를 출력하고, SMPEG_play를 호출하여 재생을 시작한다. 함수는 즉시 반환되기때문에 다른 작업을 하면서 MP3를 연주하는 프로그램에 유용하다. 이 프로그램에서는 따로 할 일이 없기때문에 재생이 끝날 때까지 기다린다. SMPEG_delete는 그동안 라이브러리가 할당한 모든 메모리를 반환하고, 마친다.

이 글이 여러분의 프로그래밍 작업에 조금이나마 도움이 되길 바란다. 의견을 듣고 싶다. 잘못된 점을 알려주거나 제안을 바란다. shuveb@shuvebhussain.org로 내게 연락할 수 있다.


참고로 테스트를 해봤는데 PCM RAW파일로만 가능한다. ^^ 형식이 그렇네. mp3나 다른 형식의 파일을 받아서 cool edit로 변환해 주었다. 쿠울~~~ 에디터 ㄱㄱ 그리고 역시나 속도 그런거 잘 맞춰줘야 한다는 ^^

댓글 없음:

댓글 쓰기