Top / EBUG勉強会 / 20180825_sndio

OpenBSD発の音声フレームワーク - sndio

EBUG 第66回会合
2018年 8月25日、長岡市 ながおか市民センター
川俣吉広、kaw@on.rim.or.jp

sndioとは

sndio(7)は音声を統一的に扱うための枠組み(フレームワーク)で、Alexandre Ratchovらによって2008年リリースのOpenBSD 4.5に始めて導入された。 現在はFreeBSD/NetBSD/Linuxにも移植されている。

ALSA, JACK, OSS, PulseAudioなどの音声フレームワークと同様、sndioは音声を扱うハードウェアとアプリケーションとの橋渡しをする。 具体的には、sndioは以下のような機能を持っている。

音声デバイスを共有する
音声アプリケーションがデバイスに直接アクセスした場合、アクセスできるのは単一のアプリケーションだけ。
sndioを介在させることで複数のアプリケーションが同時に音声を再生・収録できることを可能にする。
音声のフォーマット変換を行う
音声データの形式、サンプルレート、量子化ビット深度、チャンネル数などのパラメータが音声デバイスとアプリケーションとで異っていても、sndioで変換を行うことで支障なく使用できる。
音源の制御を可能にする
sndioは音声データそのもの以外に、起動・停止、音量制御、タイムコードなどをMIDIプロトコルを使用して伝送することができる。これにより、音源の制御を行うことができる。
信号のルーティングを行う
sndioサウンドサーバでは複数の音源をミックスしたり、あるいは複数の音声アプリケーションへ送出するなどのルーティングを行う。
これらの伝送はネットワークを経由して、他ホストで稼動している音声デバイスやアプリケーションを使用することもできる。

構成

以下に、OpenBSDでsndioフレームワークが動作している様子の例を示す。

sndio.png

構成要素

ハードウェア
様々な音声機器は、PC内の音声コーデックに接続される。コーデックはアナログ機器とのA/D, D/A変換や複数入出力のミキシングや分配、そして各信号のレベル制御などをおこなう。
デバイスドライバ
カーネル内には、コーデックに対応したデバイスドライバがあり、コーデックの機種毎の機能に対応した制御を行う。
上図の例で挙げられているIntelのICH8 I/Oコントローラ・ハブではIntel(R) HD Audio規格のコーデックを搭載しており、これに対応するドライバは、azalia(4)である。

コーデックに対応したドライバの上位にはデバイス非依存のaudio(4)があり、ユーザプロセスに対して一貫したAPIを提供する。

このレイヤーを参照・操作するツールとしてaudioctl(1)mixerctl(1)が提供されている。

audioctlの実行例
$ audioctl
name=azalia0
mode=play,record
pause=0
active=1
nblks=8
blksz=960
rate=48000
encoding=s16le
play.channels=2
play.bytes=3796930560
play.errors=883200
record.channels=2
record.bytes=3796930560
record.errors=552960
mixerctlの実行例
$ mixerctl -v | sort
inputs.dac-0:1=234,234 
inputs.beep=119 
inputs.beep_mute=off  [ off on ]
inputs.dac-2:3=234,234 
inputs.hp_source=sel6,mix6  { sel6 mix6 }
inputs.mic2=0,0 
inputs.mic3=0,0 
inputs.mic3_source=sel7,mix6  { sel7 mix6 }
inputs.mic=0,0 
inputs.mix4_source=sel3,mix6  { sel3 mix6 }
inputs.mix6_mic2=0,0 
inputs.mix6_mic=0,0 
inputs.mix6_source=mic,mic2  { mic mic2 }
inputs.sel3_source=dac-0:1  [ dac-0:1 dac-2:3 ]
inputs.sel4_source=dac-0:1  [ dac-0:1 dac-2:3 ]
inputs.sel6_source=dac-0:1  [ dac-0:1 dac-2:3 ]
inputs.sel7_source=dac-0:1  [ dac-0:1 dac-2:3 ]
inputs.spkr_source=dac-2:3,mix6  { dac-2:3 mix6 }
outputs.hp_boost=off  [ off on ]
outputs.hp_mute=off  [ off on ]
outputs.hp_sense=plugged  [ unplugged plugged ]
outputs.master.mute=off  [ off on ]
outputs.master.slaves=dac-0:1,dac-2:3,hp,spkr  { dac-0:1 dac-2:3 beep hp spkr mic3 mix6 mic3 }
outputs.master=255,255 
outputs.mic2_dir=input-vr80  [ none input input-vr0 input-vr50 input-vr80 input-vr100 ]
outputs.mic3_dir=input-vr80  [ none output input input-vr0 input-vr50 input-vr80 input-vr100 ]
outputs.mic3_mute=off  [ off on ]
outputs.mic3_sense=unplugged  [ unplugged plugged ]
outputs.mic_dir=input-vr80  [ none input input-vr0 input-vr50 input-vr80 input-vr100 ]
outputs.mic_sense=plugged  [ unplugged plugged ]
outputs.mix6=0,0 
outputs.mix6_mute=off  [ off on ]
outputs.spkr_boost=off  [ off on ]
outputs.spkr_eapd=on  [ off on ]
outputs.spkr_mute=on  [ off on ]
outputs.spkr_muters=hp,mic3  { hp mic3 }
record.adc-0:1=200,200 
record.adc-0:1_mute=off  [ off on ]
record.adc-0:1_source=mic  [ mic mic2 ]
record.adc-2:3=200,200 
record.adc-2:3_mute=off  [ off on ]
record.adc-2:3_source=mic2  [ mic mic2 ]
record.volume.mute=off  [ off on ]
record.volume.slaves=adc-2:3,adc-0:1  { adc-2:3 adc-0:1 mic mic2 }
record.volume=200,200 
音声サーバ - sndiod(8)
sndioフレームワークの中核で、ブート時に起動されデフォルトの音声デバイス/dev/audioへのアクセスを提供する。前節の「概要」で述べた機能の殆どは、このsndiodによってサポートされる。

sndiodと音声アプリケーションはソケットインターフェースを使ってデータのやり取りをする。よって、ネットワーク経由で他ホストのsndiodや音声アプリケーションとデータのやり取りをすることも可能。
ユーザコマンド - aucat(1)
コマンドレベルでsndioにアクセスするためのツール。aucatもsndiod同様、sndioの機能の殆どを提供する。
sndiodがデーモンとしてバックグラウンドで機能を提供するのに対し、aucatはユーザが直接オンライン、あるいはオフラインでsndioの機能を利用することを意図して作成されている。
例えば、aucatには処理を行う音声データをファイルから入力したり、ファイルへ出力したりする機能がある。

aucatコマンド自体はOpenBSD 2.0から存在し、sndioフレームワークが登場する以前のOpenBSD 4.3までは単に複数の音声ファイルを連結して再生する(concatenate and play audio files)コマンドだった。
OpenBSD 4.5でsndioフレームワークが登場した時点ではsndiodはなく、aucatがデーモンの役目も負っていた。OpenBSD 5.1以降はsndiodとaucatとに役割が分割された。
音声アプリケーション
音声を扱うアプリケーションでsndioを使用するには、以下のパターンが考えられる。
  • 音声アプリケーション自体が、最初からsndioに対応している。
    ...OpenBSDネイティブのアプリケーション(cdio(1)など)。
    あるいは外部プロジェクトのアプリケーションであっても、開発元でsndioに対応している場合。
    勿論パッチ等の対応なしでそのまま使用できる。

  • 音声アプリケーション自体は対応していないが、アプリケーションがリンクするライブラリが対応している。
    ...例えば音声編集ソフトのAudacityなど。Audacity自体はsndioに対応していないが、クロスプラットフォームの音声ライブラリであるPortAudioをリンクしてビルドするとPortAudioはsndioをサポートしているため、Audacityもsndio対応となる。
    portaudioを使っているportsを数えてみる
    $ cd /usr/ports
    $ find * -type d -name patches | xargs grep -rils 'portaudio' | cut -d/ -f1,2 \
    | sort | uniq | wc -l
    12

  • 音声アプリケーションにsndio対応のパッチを当てる。
    ...次項で説明するsndio APIを用いるように、アプリケーションのソースコードを変更する。
    ports/packagesでは、このケースが最も多いようだ。
    sndio対応のパッチを当てているportsを数えてみる
    $ cd /usr/ports
    $ find * -type d -name patches | xargs fgrep -rls 'sndio.h' | cut -d/ -f1,2 \
    | sort | uniq | wc -l
    41

  • sndioに対応していない音声アプリケーションとsndioツールを組合せて使う。
    ...音声アプリケーションが再生あるいは収録のみで、リアルタイム性を要求されないのであれば、パイプやファイルを経由してaucatなどとデータのやりとり行うことで対応できる。

音声デバイスへのアクセス制御

音声デバイスが無制限に共有されないように、sndioではセッションクッキーを使った認証を行う。
sndiodに接続時、音声アプリケーションは$HOME/.aucat_cookieに可能された128ビットのランダムなデータをクッキーとしてsndioに提示する。

  • 他にsndioに接続している音声アプリケーションがない場合クッキーは無条件で登録され、接続は許可される。
  • 2番目以降のアプリケーションがクッキーを提出した時、sndiodに登録済みのものと同じ場合に接続が許可される。
  • 接続しているアプリケーションがなくなった時点で登録されていたクッキーは破棄される。

クッキーとして$HOME/.aucat_cookieを使うため、通常は同一のユーザからの接続のみが同時に受け入れられるが、$HOME/.aucat_cookieを同じ内容にすれば異ったユーザ間でもsndiodの共有が可能となる。

sndiodはデフォルトではUNIXドメインソケットでの接続のみ受け付ける。INETドメインでの接続を受け付ける場合は-Lオプションで明示的に指定する必要がある。

sndioのAPI

sndio APIを使用するには、libsndioより提供される各種関数を使う。

sio_*が音声ストリーム関連、mio_*がMIDIストリーム関連の関数となる。

処理の流れ:

sio_initpar()  /* パラメータの初期化 */
   ↓
sio_open()     /* 音声デバイスや音声サーバに接続 */
   ↓
sio_setpar()   /* パラメータの設定 */       ←←←←
sio_getpar()   /* 設定結果の確認 */                ↑
   ↓                                              ↑
sio_start()    /* 処理の起動 */             ←←←-↑
   ↓                                              ↑
sio_read(), sio_write()  /* 入出力 */              ↑
   ↓                                              ↑
   ↓→→パラメータを変更する場合→→ sio_stop() →↑
   ↓    処理を一時的に中断する場合など
   ↓
sio_close()     /* 接続を閉じる */

MIDI接続の場合も、これに準ずる。

sio_open()では、一番目の引数で、以下の記法によって接続先を指定する:

type[@hostname][,unit]/devnum[.option]

type
音声デバイスの種類 ... rsnd, rmidi, snd, midithru, midi, default
hostname
リモートに接続するばあいのホスト名
unit
接続するサーバの番号
devnum
デバイスの番号
option
サブデバイス文字列

この記法はsndiodやaucatなどで音源のデバイスやサーバに接続する場合の接続先指定としても使用される。

例:

rsnd/0 ... /dev/audio0 に直接アクセス (sndiodを介さない)
snd/0.rear ... デフォルトで起動しているsndiodのリアスピーカ出力

参考:

sndioの使用例

  • ffmpegを使用したscreen castの例
    sndiodにサブデバイスとしてmonを追加(-m play,mon -s mon)。
    これにより、音声アプリケーションの出力を他のアプリケーションへ入力できるようになる。
    # rcctl set sndiod flags '-s default -m play,mon -s mon'
    # rcctl restart sndiod
    この設定を行っておくとffmpeg実行時、-i snd/0.mon を指定することで、スクリーンキャプチャ時にアプリケーションの再生音も同時に収録できる。
    $ ffmpeg -f x11grab -s 800x600 -i :0.0+100,100 -f sndio -i snd/0.mon screencapt.mpg
  • sndio非対応のアプリケーションでsndioを使用する例
    lameはpkg_addで導入してもsndio非対応であるが、aucatと組合せてsndioデバイスの音声出力をMP3に変換してファイルに保存できる。
    以下の例ではリモートマシンの音声入力をaucatで受信し、パイプ経由でlameに渡すことで音声データをon the flyでMP3形式に変換している。
    受け渡しするデータはheaderless raw形式なので、lameの入力フォーマットとして-rを指定し、サンプリングレートも明示的に指定している。

    リモートホストでの事前設定
    remote_host # rcctl set sndiod flags '-L -'
    remote_host # rcctl restart sndiod
    ローカルでのコマンド実行
    local_host $ aucat -f snd@remote_host/0 -o - | lame -r -s 48 - recinput.mp3
  • サンプルプログラム
    このプログラムはステレオ音声のL (Ch1)に1kHz, R (Ch2)に400Hzの正弦波を1分間出力する。
    /* sin_osc ... generates sine waves
     *             and outputs to sndio default device
     *
     *   Ch. 1:  1kHz, -6dBFS
     *   Ch. 2: 400Hz, -6dBFS
     *
     *   Duration: 60 secs
     */
    
    #include <stdint.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <math.h>
    
    #include <sndio.h>
    
    /* frequency parameters in Hz */
    #define SAMPLE_FREQ 48000.0
    #define OUTPUT_FREQ1 1000.0
    #define OUTPUT_FREQ2  400.0
    
    /* buffer size of waveform */
    #define SAMPLES 480
    
    /* SAMPLES per CYCLE */
    #define SPC1 (SAMPLE_FREQ/OUTPUT_FREQ1)
    #define SPC2 (SAMPLE_FREQ/OUTPUT_FREQ2)
    
    /* output level of sine wave with:
     *   sampling point 't',
     *   samples per cycle 'spc'
     */
    #define SINVAL(t, spc)    ((int16_t)(INT16_MAX*sin(2.0*M_PI*t/spc)/2))
    
    void fail_exit(const char *msg) {
      fprintf(stderr, "%s failed\n", msg);
      exit(1);
    }
    
    int main() {
      /* waveform buffer */
      int16_t wf[2*SAMPLES], /* signed 16bit per sample */
              *p_wf;
    
      /* fill buffer */
      p_wf = wf;
      for (int i = 0; i<SAMPLES; i++) {
        *p_wf++ = SINVAL(i, SPC1);  /*  1kHz to Ch1 */
        *p_wf++ = SINVAL(i, SPC2);  /* 400Hz to Ch2 */
      }
    
    
      /* initialization of sndio */
    
      struct sio_par par;
      struct sio_hdl *hdl;
      ssize_t n;
    
      sio_initpar(&par);
      par.sig = 1;
      par.bits = 16;
      par.pchan = 2;
      par.rate = 48000;
    
      hdl = sio_open("snd/0", SIO_PLAY, 0); /* "snd/0" may be SIO_DEVANY */
      if (hdl == NULL)                      /* or "default".             */
        fail_exit("sio_open()");
    
      if (!sio_setpar(hdl, &par))
        fail_exit("sio_setpar()");
    
      if (!sio_getpar(hdl, &par))
        fail_exit("sio_getpar()");
      
      if (!sio_start(hdl))
        fail_exit("sio_start()");
    
      /* output for 60 secs */
      for (int i=0; i<6000; i++) {
        n = sio_write(hdl, wf, sizeof(wf));
        if (n == 0)
          fail_exit("sio_write()");
      }
    
      sio_close(hdl);
    
      return 0;
    }
    実行例
    $ LDFLAGS='-lm -lsndio' make sin_osc 
    cc -O2 -pipe   -lm -lsndio  -o sin_osc sin_osc.c 
    $ ./sin_osc                                                      
    実際に出力された音声*1- filesin_osc.mp3

情報源


Top / EBUG勉強会 / 20180825_sndio


*1 実際にはWAV形式の音声ファイルが出力されるが、サイズが大きいためMP3形式に変換してアップロードした。
Attach file: filesin_osc.mp3 10 download [Information] filesndio.png 11 download [Information] filesndio.fig 6 download [Information]
Reload  New Edit Freeze Diff Attach Copy Rename  Top Index Search Recent Backups  Help  RSS
Last-modified: 2018-08-26 (Sun) 11:49:42 (24d)