2010년 4월 10일 토요일

리눅스 사운드 프로그래밍

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

 

by Shuveb Hussain

한글 번역 전정호

이 글은 한글번역판을 요약한 것입니다.

 

*** 사운드 카드 프로그래밍 ***

 

 (1) 샘플링

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

 

 

 (2) 비트와 채널

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

  • 이제 이론은 충분하니 실제 프로그램을 시작하자.

 

 

 (3) 사운드 카드를 프로그래밍 하기

  • 사운드 카드를 프로그래밍하는 방법이 몇가지 있다.
  •  초기 리눅스 드라이버가 만들어지던 때 두 그룹이 있었다.
    • 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만 직접 접근할 수 있다.

 (4) 코드

  • 이제 코드로 넘어가자. 이 코드는 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===\n");
    return 0;
}


 

(5) 코드 설명

  • 이 프로그램은 raw PCM, 22KHz, stereo 자료파일을 5초간 연주한다. 프로그램은 크게 3가지 작업을 한다.
    • sound 장치 열기(open)
    • 재생을 위한 파라미터 설정
    • 장치에 자료 쓰기(write)
  • open() / write() / close() 시스템 호출은 일반 파일과 동일하다.
  • ioctl()(Input/Output ConTroL) 시스템 호출을 사용하여 재생에 사용할 파라미터를 설정한다. ioctl() 시스템 호출은 입출력 장치와 통신하거나 파라미터를 설정할 때 사용한다. 또 시스템 호출의 개수를 줄이기 위한 편법으로 사용되기도 한다. 그래서 흔히 프로그래머의 다용도칼이라고 부른다. 함수의 원형
  • int ioctl( int fd, int command, ... );

    • fd : 장치의 파일 기술자(file descriptor)
    • command : 장치에 대한 요청/명령
    • ... : command에 따라 달라진다.
  • ioctl(handle, SNDCTL_DSP_SPEED, &rate)
    • DSP 재생을 설정한다. rate는 장치 드라이버로 전달되는 실제 재생율이다.
  • 예제 프로그램에는 3가지 ioctl()이 있다.
    • 재생에 사용할 채널 개수(channels)를 설정
    • PCM 형식(format)을 설정
    • 재생율(sampling rate)을 설정
  • 이것으로 장치에 필요한 정보는 충분하고, 다음은 장치파일에 PCM 자료를 직접 쓰면(write) 재생이 된다. 마지막 ioctl()은 장치를 비우고(flush), 그 다음 PCM 자료를 저장하기 위해 할당한 메모리를 해제한다. 이게 끝이다.

 

 

(6) 음악 저장 형식

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

 

 

 (7) 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 패키지에 포함되있다.

 

 (8) 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로 변환해 주었다. 쿠울~~~ 에디터 ㄱㄱ 그리고 역시나 속도 그런거 잘 맞춰줘야 한다는 ^^

 

출처 : http://www.whiterabbitpress.com/lg/issue97tag/shuveb.html

 

출처 : 한국어 출처, 원문(영어)

댓글 없음:

댓글 쓰기