메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

리눅스 오디오 소개

한빛미디어

|

2007-10-17

|

by HANBIT

20,923

제공 : 한빛 네트워크
저자 : John Littler
역자 : 김도형
원문 : An Introduction to Linux Audio

리눅스 오디오 프로그래밍 소개

리눅스는 지난 10년간 계속 발전했습니다. 당시, 여러분이 다른 운영체제에 있는 메인 오디오와 음악 어플리케이션을 찾고 있었다면 여러분은 리눅스에서 그에 동등하고 완전히 개발된 어플리케이션을 찾기위해 발버둥쳐야 했을 것입니다. 아직 완전히 완료되었다고 말할 수는 없지만, 지금은 실제 작업을 해낼 수 있는 높은 수준의 어플리케이션이 다양하게 존재합니다. 말한 것 처럼, 여전히 현존하는 어플리케이션에서 해야 할 작업은 남아 있고, 미래를 얻기 위해 시도하길 원하는 사람들을 위해 완전한 미래는 지금 시작되었습니다; 그러니까 시퀀서(sequencer)나 루퍼(looper) 또는 무엇이되든 이러한 것들만 있어야 한다는 것과 같은 것을 명시하는 기본 원칙은 없다는 것입니다.

미래를 설계하거나, 또는 단지 사운드를 재생하든지 관념적인 이유뿐 만아니라 실용적인 면에서도 이러한 작업을 시작하기에 리눅스는 괜찮은 선택입니다. 실용적인 이유로 여러분의 바램과 기술에 적합한 방식으로 리눅스 오디오 프로그래밍을 할 수 있게 다양한 API를 이용할 수 있어야 합니다. 여러분이 얼마간의 기술을 한번 습득하게 되면 좋아하는 현존하는 어플리케이션의 개발 팀에 합류하거나, 황야로 떨어져 나와 여러분만의 방식으로 해킹을 할 수 도 있습니다.

또 다른 이슈는 비지니스 모델입니다. 이것은 즐거운 취미면서 직업의 가능성까지 가지고 시작하려는 누구에게나 흥미롭습니다. 우선 첫째로, 이 영역에는 그렇게 많은 직업이 있지 않습니다. 그리고 만약 여러분이 모든 상용의 오디오 소프트웨어 개발회사를 모두 포함한다고 해도 여전히 많지 않습니다. 그래도 그들은 거기에 있을 것입니다. 학구적인 세계는 유사하게 작은 영역입니다. 그러나 또다른 가능성입니다. 분명한 한 가지 사실은 프로젝트 기부금이 임차료를 지불하지 않을 것이라는 것입니다. 그럼에도 불구하고 여러분의 프로젝트 작업의 결과로 컨설팅 작업은 이익을 낼 것입니다. 그리고 여러분이 다음 Big Thing을 제안한다면, 글쎄요, 그것은 다른 이야기입니다.

세부적으로 뛰어들기 전에, 그리고 만약에 여러분이 이쪽의 일반적인 영역에 익숙하지 않다면 CCRMA의 Fernando Lopez-Lescano(Stanford)와 Ardour 프로젝트의 Paul Davis의 두 개의 팟캐스트 대화를 들어보길 바랍니다. 이 팟캐스트는 이 제목에 맞는 주제에 대해 광범위하게 다루고 있으며, Technical University Berlin 과 Mstation.org 에서 들을 수 있습니다.

이제, 우리가 시도하려는 것과 그것을 하기 위한 주요 옵션들을 한번 훑어 보겠습니다.

세 가지 주된 작업은 오디오 녹음(캡쳐, 리코딩)과 재생, 변조입니다. 이 모든 것은 디지털 신호 처리 라고 부르는 DSP(Digital Signal Processing) 에 해당하는 작업입니다. 우리는 이 중 처음 두 가지 작업인 녹음과 재생에 대해 살펴 보겠습니다.

우리가 하고 싶은 것은 컴퓨터에 있는 사운드 카드에게 무엇을 할 것인지, 무슨 데이터의 배치 방법을 가질 것인지 (사운드 카드의 성능을 유념해야합니다), 어디에 그것을 저장할 것인지 와 같은 것들을 말하는 것입니다.

이것을 개략적으로 말하면 다음과 같습니다 (Paul Davis 의 ALSA 프로그래밍 튜토리얼에서):
    open_the_device();
    set_the_parameters_of_the_device();
    while (!done) {
       /* 다음 중 하나 또는 두 가지 작업 */
       receive_audio_data_from_the_device();
       deliver_audio_data_to_the_device();
    }
    close the device
사운드 카드를 공부하는데 있어 ALSA 사운드 카드 매트릭스를 훑어보는 것은 좋은 시작점입니다. 그러나 대부분의 경우 여러분은 기계에 대해서 깊이 알 필요 까지는 없습니다. (여러분이 달성하려는 정도에 따라 달라집니다).

이제, 무엇인가가 일어나는 방법을 알아보겠습니다.

OSS

4Front Technologies의 OSS (Open Sound System) 는 1997년부터 1998년까지 리눅스용 사운드 카드 드라이버를 공급하는 유일한 업체였습니다. 그 무렵에는 무료(free) 드라이버와 더 많은 것을 제공하는 상용의(commercial) 드라이버가 있었습니다. 지금은 비영리적인 목적으로는 OSS 드라이버를 무료로 사용할 수 있습니다. 또한 2007년 6월 부터는 드라이버도 오픈 소스가 되었습니다.

지금은 Advanced Linux Sound Architecture (ALSA) 가 리눅스 커널 드라이버를 제공하고 있고, OSS는 추천하지 않습니다(deprecated). 그러나 이미 존재하는 OSS 응용에서 작업할 때와 같이 OSS가 유용한 경우도 있습니다. OSS 리눅스 사운드 지원의 처음 책임자였던 Hannu Savolainen 는 블로그에 이것에 대한 모든 매혹적인 backlog를 제공하고 있습니다.

ALSA API를 좋아하지 않으며, ALSA API의 추가된 기능이나 복잡함을 필요로 하지 않는 개발자들이 많기 때문에, 그는 그들이 OSS를 계속 사용하는 것들을 지지하고 있습니다. 그러나 ALSA를 사용하는 쉬운 방법이 있으며, 이것은 다음 섹션에서 알아 보겠습니다.

만약에 여러분이 cat somefile > /dev/dsp 와 같은 장난스러우 구문을 본적이 있다면 여러분은 OSS의 인터페이스를 본 것입니다. 덧붙여 말하면, 텍스트 파일을 이용해서 저 구문을 실행한다면 결과는 우스꽝스러울 것입니다.

OSS API는 POSIX/Unix 시스템 콜을 포함합니다: open(), close(), read(), write(), ioctl(), select(), poll(), mmap().

다음은 OSS 문서에 있는 간단한 오디오 재생 프로그램의 예제로 이 프로그램은 연속적인 1 kHz 사인파를 재생합니다. 문서화가 잘 되어 있는 이 코드는 우선 합성(신디사이즈, synthesis)의 예제를 제공하고, 파라미터를 설정하고, 오디오 장치를 열고 설정한 후, 마지막으로 장치에 데이터를 기록 합니다.
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 

    int fd_out;
    int sample_rate = 48000;

    static void
    write_sinewave (void)
    {

    /*
    이 루틴은 합성을 이용해 오디오 시그널을 만드는 어플리케이션 루틴의
    전형적인 예제입니다. 덧붙이면 이것은 사실 매우 기초적인 “wave table”
    알고리즘입니다. 이것은 사인 함수의 완전한 순환을 위한 이미 계산된
    사인 함수 값을 이용합니다. 이것은 각각의 샘플을 위해 sin() 함수를
    호출하는 것보다 훨씬 빠릅니다. 다른 어플리케이션에서 이 루틴은
    해당 어플리케이션이 필요로 하는 작업으로 간단하게 대체할 수 있습니다.
    */

      static unsigned int phase = 0;    /* 사인파의 위상 */
      unsigned int p;
      int i;
      short buf[1024];                  /* 1024 샘플/쓰기 는 안전한 선택입니다. */

      int outsz = sizeof (buf) / 2;

      static int sinebuf[48] = {

        0, 4276, 8480, 12539, 16383, 19947, 23169, 25995,
        28377, 30272, 31650, 32486, 32767, 32486, 31650, 30272,
        28377, 25995, 23169, 19947, 16383, 12539, 8480, 4276,
        0, -4276, -8480, -12539, -16383, -19947, -23169, -25995,
        -28377, -30272, -31650, -32486, -32767, -32486, -31650, -30272,
        -28377, -25995, -23169, -19947, -16383, -12539, -8480, -4276
      };

      for (i = 0; i < outsz; i++)
        {

    /*
    sinebuf[] 테이블은 48000 Hz 로 계산되어 있습니다. 우리는 단순한 샘플
    레이트 보정을 사용할 것입니다.  너무 크게 증가하는 위상 변수는 일정 시간
    이후에 산술 오버플로우를 발생할 수 있기 때문에 너무 크게 증가하는
    위상 변수를 막아야합니다.  몇 시간이나 몇 달,  심지어 몇 년 동안 인터럽트
    없이 계속 실행하는 오디오 프로그램을 작성할 때는 이런 종류의 에러 가능성을
    확인해야 합니다.  매 초 192000 샘플을 32비트 정수형 범위로 연산을 하면
    오버플로우가 매우 빨리 발생합니다. 192 kHZ 로 샘플을 재생하면 6시간 후에는
    오버 플로우가 발생할 것입니다.
    */

          p = (phase * sample_rate) / 48000;

          phase = (phase + 1) % 4800;
          buf[i] = sinebuf[p % 48];
        }

    /*
    기록시에는 적당한 에러 체크를 반드시 해야 합니다.
    시스템이 반환하는 에러 코드를 확인하는 것 역시 중요합니다.
    */

      if (write (fd_out, buf, sizeof (buf)) != sizeof (buf))
        {
          perror ("Audio write");
          exit (-1);
        }
    }

    /*
    open_audio_device 는 오디오 장치를 열고 필요한 모드로 초기화 합니다.
    */

    static int
    open_audio_device (char *name, int mode)
    {
      int tmp, fd;

      if ((fd = open (name, mode, 0)) == -1)
        {
          perror (name);
          exit (-1);
        }

    /*
    장치를 설정합니다. 샘플 포맷과 채널의 개수, 샘플 레이트를 정확하게
    이 순서로 설정해야 함을 주의하세요. 어떤 장치는 순서의 영향을 받습니다.
    */

    /* 샘플 포맷을 설정합니다. */

      tmp = AFMT_S16_NE;            /* Native 16 bits */
      if (ioctl (fd, SNDCTL_DSP_SETFMT, &tmp) == -1)
        {
          perror ("SNDCTL_DSP_SETFMT");
          exit (-1);
        }

      if (tmp != AFMT_S16_NE)
        {
          fprintf (stderr,
                   "The device doesn"t support the 16 bit sample format.n");
          exit (-1);
        }

    /* 채널 개수를 설정합니다. */

      tmp = 1;
      if (ioctl (fd, SNDCTL_DSP_CHANNELS, &tmp) == -1)
        {
          perror ("SNDCTL_DSP_CHANNELS");
          exit (-1);
        }

      if (tmp != 1)
        {
          fprintf (stderr, "The device doesn"t support mono mode.n");
          exit (-1);
        }

    /* 샘플 레이트를 설정합니다. */

      sample_rate = 48000;
      if (ioctl (fd, SNDCTL_DSP_SPEED, &sample_rate) == -1)
        {
          perror ("SNDCTL_DSP_SPEED");
          exit (-1);
        }

    /*
    우리는 실제 샘플 레이트를 기준으로  자동으로 시그널을 조정하기 때문에
    에러 체크가 필요 없습니다. 그러나 대부분의 어플리케이션은 샘플 레이트의
    값을 점검하거나 요청한 레이트와 샘플 레이트를 비교해야 합니다.
    레이트 사이의 약간의 차이(10% 또는 그 이하)는 일반적이고, 어플리케이션이
    견뎌낼 수 있지만 큰 차이가 있으면 미키마우스 같은 골치아픈 음감(pitch)
    문제가 발생합니다.
    */

      return fd;
    }

    int
    main (int argc, char *argv[])
    {

    /*
    보통 시스템 관리자들은 ossctl이나 기타 방법을 이용해서 /dev/dsp 장치를
    선택하기 때문에 /dev/dsp 를 기본 장치로 이용합니다. 
    */

      char *name_out = "/dev/dsp";

    /*
    기본 값 대신 다른 장치를 선택할 수 있는 방법을 제공하는 것을 추천합니다.
    우리는 커맨드 라인 인자를 사용하지만 어떤 경우는 환경 변수를 이용하거나
    설정 파일을 이용하는 것이 더 나을 수도 있습니다.
    */

      if (argc > 1)
        name_out = argv[1];

    /*
    재생만 하는 프로그램에서 O_WRONLY 모드를 사용하는 것은 필수입니다.
    다른 모드는 드라이버의 자원 (메모리) 사용을 증가시킵니다.
    또한 다른 어플리케이션에서 동시에 같은 장치를 사용하여 녹음하는 것을
    방지할 수 있습니다.
    */

      fd_out = open_audio_device (name_out, O_WRONLY);

      while (1)
        write_sinewave ();

      exit (0);
    }

    Copyright (C) 4Front Technologies, 2002-2004. Released under GPLv2/CDDL.
ALSA

ALSA는 자유로운 오픈소스 사운드 드라이버의 갭을 채우기 위해 시작되었습니다. In 1998, for example, the OSS free drivers would not allow you to use full duplex 예를 들면, 1998년 OSS의 무료 드라이버는 여러분이 가진 사운드 카드의 양방향 동시 전송(full duplex) 기능읕 사용할 수 없었습니다. (여러분이 녹음하는 동시에 미리 녹음한 트랙을 제어하는 것이 가능한 것) 또한 OSS 드라이버는 RME Hammerfall 과 같은 고성능의 카드(which were becoming available.)의 정교한 부분을 다룰 수 없었습니다. 그 때를 생각하면 CD나 MP3를 재생하려는 일반적인 용도로 OSS는 충분했고, ALSA는 상당한 수준의 스튜디오 작업을 할 때 필요했습니다.

그래서 비록 양쪽 API 모두 하드웨어와 이야기하는 일반적인 방법과 구체적인 방법이 모두 있지만 ALSA API는 OSS 보다 저수준이라고 말할수 도 있습니다: 그리고 구체적인 방법 (OSS에서는 mmap, ALSA 에서는 hw) 은 훨씬 장황하며 카드에 대해 더 자세히 알아야 합니다. -- 예를 들면, 만약 여러분이 고성능의 오디오 카드의 세부적인 기능을 사용하려 할때 이치에 맞는지에 대해 말입니다. (해당 카드를 지원하는지 알기 위해서는 언급했던 사운드 카드 매트릭스를 참조합니다.) 또한 이러한 API들은 드라이버를 통해서 사운드 카드에게 명령을 내려서 장치를 작동 시킨다는 것을 잊지 않도록 합니다; 일반적인 방법을 이용해 장치와 말하는 것은 마법이 아닙니다. 그것은 드라이버에 이미 포함하고 있는 정보를 이용해 말하는 것일 뿐입니다.

다음은 예제는 Paul Davis 의 Tutorial on the ALSA API 에서 가져온 최소한의 인터럽트-구동 방식의 프로그램입니다. 이 프로그램은 재생을 위한 오디오 인터페이스를 열고, 스테레오, 16비트, 44.1kHz 로 설정하고, 전통적인 인터리브(interleaved) 방식으로 읽고/쓰기 접근을 합니다. 그리고나서 재생용 데이터를 위해 준비될 때까지 기다린 뒤 랜덤 데이터를 전송합니다. 이러한 설계로 여러분은 여러분의 프로그램을 JACK 이나, LADSPA, CoreAudio, VST, 그리고 그 외의 많은 콜백-구동 방식의 메커니즘의 시스템에 쉽게 이식할 수 있습니다.
    #include 
    #include 
    #include 
    #include 
    #include 

    snd_pcm_t *playback_handle;
    short buf[4096];

    int
    playback_callback (snd_pcm_sframes_t nframes)
    {
        int err;

        printf ("playback callback called with %u framesn", nframes);

        /* ... buf를 데이터로 채웁니다. ... */

        if ((err = snd_pcm_writei (playback_handle, buf, nframes)) < 0) {
              fprintf (stderr, "write failed (%s)n", snd_strerror (err));
        }

        return err;
    }

    main (int argc, char *argv[])
    {

        snd_pcm_hw_params_t *hw_params;
        snd_pcm_sw_params_t *sw_params;
        snd_pcm_sframes_t frames_to_deliver;
        int nfds;
        int err;
        struct pollfd *pfds;

        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);

        /* 재생을 위한 데이터를 4096 프레임이나 그 이상을 전달 받으면
           깨어나도록 ALSA를 설정합니다. 또한 우리들 스스로 장치를
           시작할 것이라고 ALSA에게 말해줍니다.
        */

        if ((err = snd_pcm_sw_params_malloc (&sw_params)) < 0) {
              fprintf (stderr, "cannot allocate software parameters structure (%s)n",
                       snd_strerror (err));
              exit (1);
        }
        if ((err = snd_pcm_sw_params_current (playback_handle, sw_params)) < 0) {
              fprintf (stderr, "cannot initialize software parameters structure (%s)n",
                       snd_strerror (err));
              exit (1);
        }
        if ((err = snd_pcm_sw_params_set_avail_min (playback_handle, sw_params, 4096)) 
< 0) {
              fprintf (stderr, "cannot set minimum available count (%s)n",
                       snd_strerror (err));
              exit (1);
        }
        if ((err = snd_pcm_sw_params_set_start_threshold (playback_handle, sw_params, 
0U)) < 0) {
              fprintf (stderr, "cannot set start mode (%s)n",
                       snd_strerror (err));
              exit (1);
        }
        if ((err = snd_pcm_sw_params (playback_handle, sw_params)) < 0) {
              fprintf (stderr, "cannot set software parameters (%s)n",
                       snd_strerror (err));
              exit (1);
        }

        /* 인터페이스는 매번 4096 프레임 마다 커널을 인터럽트 할 것입니다.
           그리고 ALSA는 그 때마다 곧 이 프로그램을 깨울 것입니다.
        */

        if ((err = snd_pcm_prepare (playback_handle)) < 0) {
              fprintf (stderr, "cannot prepare audio interface for use (%s)n",
                       snd_strerror (err));
              exit (1);
        }

        while (1) {

              /* 데이터를 위한 인터페이스가 준비되거나 또는
                 1초가 경과할 때까지 기다립니다.
              */

              if ((err = snd_pcm_wait (playback_handle, 1000)) < 0) {
                      fprintf (stderr, "poll failed (%s)n", strerror (errno));
                      break;
              }                  

              /* 데이터를 재생하기 위해 얼마 만큼의 공간을
                 사용할 수 있는지 확인합니다.
              */

              if ((frames_to_deliver = snd_pcm_avail_update (playback_handle)) < 0) {
                      if (frames_to_deliver == -EPIPE) {
                              fprintf (stderr, "an xrun occuredn");
                              break;
                      } else {
                              fprintf (stderr, "unknown ALSA avail update return value (%
d)n", 
                                       frames_to_deliver);
                              break;
                      }
              }

              frames_to_deliver = frames_to_deliver > 4096 ? 4096 : frames_to_deliver;

              /* 데이터를 전송합니다. */

              if (playback_callback (frames_to_deliver) != frames_to_deliver) {
                      fprintf (stderr, "playback callback failedn");
                      break;
              }
        }

        snd_pcm_close (playback_handle);
        exit (0);
    }
이 예제 다음으로 Paul은 앞서 이야기했던 양방향(full-duplex) 또는, 캡쳐와 재생을 동시에 수행하는 방법을 언급합니다. Paul은 이것을 달성하기 위해 통상적인 캡쳐와 재생을 결합하는 것은 결함이 크며, 인터럽트는 이것을 달성하기 위한 방법이지만 복잡하기 때문에 JACK 이용하는 것을 추천합니다.

JACK

여러분이 Paul Davis의 팟캐스트를 들었다면 여러분은 이렇게 말하려는 것을 알아챌 것입니다: 리눅스 오디오 프로그래밍을 하는 최선의 방법을 무엇이라고 생각합니까? ALSA? 사실 그리고 그는 고수준의 추상화로 사운드 카드 특성을 다루는 것이 쉽고 코드 양이 줄어들기 때문에 통상적으로 JACK 이 최선의 방법이라고 말합니다.

Paul Davis 와 Linux Audio Dev 메일링 리스트의 사람들은 JACK Audio Connection Kit 을 구성했습니다. POSIX 호환 운영체제 (예. 리눅스, BSD, Mac OS X) 위의 오디오 어플리케이션이 실행중에 데이터를 교환하는 것을 가능하게 하고 고수준의 추상화를 제공해서 개발자가 그들의 프로그램의 핵심 기능에 더욱 집중할 수 있도록 하는 것이 주 아이디어 입니다.

만약 여러분이 X11이나 커맨드 라인 어플리케이션을 사용한다면 리눅스에서 Mac OS X로 이식하는 것이 쉬워졌다는 것이 추가적인 장점입니다.

여기에 JACK의 기본적인 기능을 보여주는 간단한 클라이언트가 있습니다. JACK 사이트에 나와있는 전형적인 순서는 다음과 같습니다:
  • JACK 서버에 접속하기 위해 jack_client_open() 를 호출
  • 여러분의 어플리케이션으로 데이터를 보내거나 받는 것을 가능하게 하기 위해 "ports" 등록
  • JACK 서버가 적절한 시점에 호출하기 위한 "process callback" 등록
  • 여러분의 어플리케이션이 데이터르 처리할 준비가 되었음을 JACK 에게 알림
다음 예제에서 오직 호출하는 중요한 인터페이스는 jack.h 입니다. (SourceForge 의 LXR project 페이지에서 simple_client.c 예제를 볼 수 있습니다.) 두말 할 것도 없이, 존재하는 코드를 이용한(코드 재사용!) 완전한 스터디는 시작하기에 적당합니다.
    /** @file simple_client.c
      *
      * @brief 이것은 많은 어플리케이션에서 사용하는 JACK의 기본적인 특징을
      * 시연하는 매우 간단한 예제입니다.
      */

      #include 
      #include 
      #include 
      #include 
      #include 
      #include 

      jack_port_t *input_port;
      jack_port_t *output_port;

      /**
       * JACK 어플리케이션을 위한 프로세스 콜백
       * 적절한 시점에 JACK이 이 콜백 함수를 호출 합니다.
       */
      int
      process (jack_nframes_t nframes, void *arg)
      {
           jack_default_audio_sample_t *out = (jack_default_audio_sample_t *) 
jack_port_get_buffer (output_port, nframes);
           jack_default_audio_sample_t *in = (jack_default_audio_sample_t *) 
jack_port_get_buffer (input_port, nframes);

           memcpy (out, in, sizeof (jack_default_audio_sample_t) * nframes);

           return 0;      
      }

      /**
       * 이것은 JACK 어플리케이션의 shutdown 콜백입니다.
       * 서버를 종료하거나 클라이언트를 접속 종료하기로 결정했을때
       * JACK이 이 콜백 함수를 호출합니다.
       */
      void
      jack_shutdown (void *arg)
      {

           exit (1);
      }

      int
      main (int argc, char *argv[])
      {
           jack_client_t *client;
           const char **ports;

           if (argc < 2) {
                   fprintf (stderr, "usage: jack_simple_client n");
                   return 1;
           }

           /* JACK 서버의 클라이언트가 되기 위해 시도합니다. */

           if ((client = jack_client_new (argv[1])) == 0) {
                   fprintf (stderr, "jack server not running?n");
                   return 1;
           }

           /* 처리해야할 작업이 있다면 JACK 서버가
              `process()" 를 호출하게 합니다.
           */

           jack_set_process_callback (client, process, 0);

           /* 
           /* 종료하거나 우리를 호출하는 것을 그만두리고 결정했을 때
              JACK 서버가 `jack_shutdown()" 를 호출하게 합니다.
           */

           jack_on_shutdown (client, jack_shutdown, 0);

           /* 현재 샘플 레이트를 보여줍니다.
           */

           printf ("engine sample rate: %" PRIu32 "n",
                   jack_get_sample_rate (client));

           /* 두개의 포트를 생성합니다. */

           input_port = jack_port_register (client, "input", JACK_DEFAULT_AUDIO_TYPE, 
JackPortIsInput, 0);
           output_port = jack_port_register (client, "output", JACK_DEFAULT_AUDIO_TYPE, 
JackPortIsOutput, 0);

           /* 우리가 준비되었음을 JACK 서버에게 알려줍니다. */

           if (jack_activate (client)) {
                   fprintf (stderr, "cannot activate client");
                   return 1;
           }

           /* ports를 연결합니다. 주의: 여러분은 클라이언트가
              활성화 되기 전에 이 작업을 할 수 없습니다.
              이것은 동작하고 있지 않은 클라이언트를 연결하는
              것을 허용할 수 없기 때문입니다.
           */

           if ((ports = jack_get_ports (client, NULL, NULL, 
JackPortIsPhysical|JackPortIsOutput)) == NULL) {
                  fprintf(stderr, "Cannot find any physical capture portsn");
                  exit(1);
           }

           if (jack_connect (client, ports[0], jack_port_name (input_port))) {
                  fprintf (stderr, "cannot connect input portsn");
           }

           free (ports);

           if ((ports = jack_get_ports (client, NULL, NULL, 
JackPortIsPhysical|JackPortIsInput)) == NULL) {
                  fprintf(stderr, "Cannot find any physical playback portsn");
                  exit(1);
           }

           if (jack_connect (client, jack_port_name (output_port), ports[0])) {
                  fprintf (stderr, "cannot connect output portsn");
           }

           free (ports);

           /* 이것은 단지 장난감이므로 몇초간만 실행하고 종료하도록 합니다. */

           sleep (10);
           jack_client_close (client);
           exit (0);
     }
Python: PyAudio 와 Gstreamer를 이용한 PyGst

JACK 보다 더 고수준의 방식은 Gstreamer 용 Python 바인딩인 PyGst를 사용하거나, 크로스 플랫폼 오디오 라이브러리인 PortAudio 바인딩인 PyAudio를 사용하는 것입니다. (역주: Perl의 경우 CPAN의 Gstreamer 또는 Audio::PortAudio 모듈을 사용합니다. :)

아래는 PyAudio 문서의 예제입니다. 먼저 audio 라이브러리를 import 한 후 파일 읽기와 쓰기를 가능하게 하는 sys 라이브러리를 import 합니다. 그 후 우리가 모든 예제에서 처리했던 오디오 포맷과 오디오 장치를 설정하는 작업을 수행합니다. 우리는 스트림을 열고, 데이터를 낚아챈 다음 저장합니다.
    """ 몇 초간 오디오를 녹음하고 WAVE 파일로 저장합니다. """
    import pyaudio
    import wave
    import sys

    chunk = 1024
    FORMAT = pyaudio.paInt16
    CHANNELS = 1
    RATE = 44100
    RECORD_SECONDS = 5
    WAVE_OUTPUT_FILENAME = "output.wav"

    p = pyaudio.PyAudio()

    stream = p.open(format = FORMAT,
                    channels = CHANNELS,
                    rate = RATE,
                    input = True,
                    frames_per_buffer = chunk)

    print "* recording"
    all = []
    for i in range(0, RATE / chunk * RECORD_SECONDS):
        data = stream.read(chunk)
        all.append(data)
    print "* done recording"

    stream.close()
    p.terminate()

    # write data to WAVE file
    data = "".join(all)
    wf = wave.open(WAVE_OUTPUT_FILENAME, "wb")
    wf.setnchannels(CHANNELS)
    wf.setsampwidth(p.get_sample_size(FORMAT))
    wf.setframerate(RATE)
    wf.writeframes(data)
    wf.close()
얼마나 코드가 간단한지 유념하길 바랍니다. There is no GUI, of course, so that helps as well. 물론, 이해를 돕기 위해 GUI는 없습니다.

Gstreamer과 PyGst 바인딩의 동작을 설명하는 Jono Bacon 의 기사는 Gstreamer의 좋은 소개 입니다. Gstreamer는 대단히 많은 독창적인 가능성을 제공하고, 많은 다른 특징들 중에서도, ESD와는 다르게 한 번에 하나 이상의 오디오 스트림을 재생하는 것을 가능하게 합니다.

아래의 예제는 PyGst 문서에 있는 간단한 오디오 재생기 예제 코드입니다. 우리는 sys와 windowing, pygst 기능을 사용하고 window와 재생기를 설정한 후, 나머지 작업을 처리합니다. 몇몇 구현은 소형 장치들을 위한 필수적인 비트를 빼먹었음을 주의하세요. 예를 들면 Nokia 770 의 커맨드 라인에서 재생 루프를 실행하려고 했으나 가능하지 않았습니다.
    #!/usr/bin/env python

    import sys, os, os.path
    import pygtk, gtk, gobject
    import pygst
    pygst.require("0.10")
    import gst

    class GTK_Main:

            def __init__(self):
                 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
                 window.set_title("Audio-Player")
                 window.set_default_size(400, 300)
                 window.connect("destroy", gtk.main_quit, "WM destroy")
                 vbox = gtk.VBox()
                 window.add(vbox)
                 self.entry = gtk.Entry()
                 vbox.pack_start(self.entry, False, True)
                 self.button = gtk.Button("Start")
                 self.button.connect("clicked", self.start_stop)
                 vbox.add(self.button)
                 window.show_all()

                 self.player = gst.element_factory_make("playbin", "player")
                 fakesink = gst.element_factory_make("fakesink", "my-fakesink")
                 self.player.set_property("video-sink", fakesink)
                 bus = self.player.get_bus()
                 bus.add_signal_watch()
                 bus.connect("message", self.on_message)

            def start_stop(self, w):
                 if self.button.get_label() == "Start":
                       filepath = self.entry.get_text()
                       if os.path.exists(filepath):
                             self.button.set_label("Stop")
                             self.player.set_property("uri", "file://" + filepath)
                                    self.player.set_state(gst.STATE_PLAYING)
                 else:
                       self.player.set_state(gst.STATE_NULL)
                       self.button.set_label("Start")

            def on_message(self, bus, message):
                 t = message.type
                 if t == gst.MESSAGE_EOS:
                       self.player.set_state(gst.STATE_NULL)
                       self.button.set_label("Start")
                 elif t == gst.MESSAGE_ERROR:
                       self.player.set_state(gst.STATE_NULL)
                       self.button.set_label("Start")

    gtk.gdk.threads_init()
    GTK_Main()
    gtk.main()
이 예제는 실제로 GUI를 포함하지만 저수준의 언어와 비교했을 때 코드양은 여전히 작습니다.

결론

여러분이 리눅스 오디오를 접근하는 것은 여러분이 달성하고자 하는 것에 따라 달라집니다. 만약 여러분이 단지 재미로 배우는 것을 목적으로 한다면 어디에서 시작해도 좋습니다. 만약 재미가 아이디어를 유발한다면, 여러분은 곧 장치에 대해 더 깊은 지식을 필요로 하는지 단지 여러분은 단지 표면에 스케칭하는 것을 필요로 하는지 곧 알게 될 것입니다.

다만 여러분의 레벨이 높아질 수록 일이 어떻게 돌아가야 하는지에 대한 다른 사람들의 생각에 의해 한계를 접하게 될 수도 있음을 명심하기 바랍니다. 이것은 편안함과 구현 시점에서 보면 대개 좋습니다만 사고의 범위를 제한한다는 점에서는 나쁩니다.

만약 여러분이 해결하기 어려운 문제를 당면하면 Linux Audio Dev 리스트는 참여하기 좋은 메일링 리스트 입니다. Linux Audio Dev 는 자신을 이렇게 설명합니다:

“LAD (The Linux Audio Developers) 는 리눅스 운영체제에서의 사운드 아키텍쳐와 사운드 어플리케이션 개발 전용 리스트입니다. 안정성과 확장성을 가지고 있기 때문에 많은 양의 오디오 데이터를 다루고 처리하기 위한 완벽한 환경입니다. 우리의 목표는 코드 재사용과 협동을 널리 퍼질 수 있게 격려하고, 모든 오디오 관련 소프트웨어 프로젝트를 위한 공공의 포럼과 다수의 다른 특정 관심 분야의 메일링 리스트를 위한 교환 지점을 제공하는 것입니다.”

또한 이 리스트의 아카이브는 krugle.com 이나 Google의 Codesearch 를 사용하는 것 처럼 질문에 답변하기에 유용합니다.

현 시점에서 여러분은 글쎄, 사실 내가 정말로 하고 싶었던 것이 잡음이나 음악같지 않은 것을 만드는 것이었는지 여러분 자신에게 말할 것입니다. 그런 경우에는 여러분은 Csound 나 Pure Data를 살펴볼 수 있습니다. 또한 여러분은 이 기사에서 다루지 않은 부분인 신디사이즈와 DSP 프로그래밍을 볼 수 있습니다.
TAG :
댓글 입력
자료실

최근 본 책0