LibsoxのABC

概要

課題8.2をやるのに色々な方法の概要を述べたうち, 自分でlibsox (soxコマンドが中で用いているライブラリ) を用いるというものがありました. これを用いれば自分の好きなタイミング (具体的には, クライアントがつなげてきたタイミング) でサウンドデバイスを開いて, 読み始めることができるので, recコマンドと自分のコマンドをパイプでつないだ時のように, クライアントとつながる前からrecコマンドが勝手に音を 読みだしてしまう(結果的にクライアントに, 昔の音を送ってしまう) という問題は根本から解決できます.

教科書にはあとは自分で調べてねと気安く書いてしまったのですが, マニュアルは不親切な上に間違いもあり, かつ簡単なGooglingでは例題も見つからないとあって, 本質的でないところで困難があります. 特に, 通常のファイルではなく, サウンドデバイスを読み出すための方法に全く言及されていないです.

そこでこのページではlibsox でサウンドデバイスを読むための最低限の例題を載せます.

libsoxでサウンドデバイスを読む

  • まずsox.hをincludeする. これがないと言われたら libsox-devをインストールしましょう.
    #include <sox.h>
    
  • サウンドデバイスを開く.
      sox_format_t * ft = sox_open_read("default", 0, 0, "pulseaudio");
    
    肝は, "default", "pulseaudio"という二つの「知らなきゃおしまい」な キーワードである. man pageに書いていない上, man pageのsox_open_readの引数は間違っている. /usr/include/sox.h を読むと引数が実は4つ必要だということはわかるが, 第1, 4引数に具体的に何を渡すかまではわからない. なお, この引数に渡すべき値は環境によって異なる可能性があります. どうやってこの答えを見つけるかは後述する.
  • 実際に音を読む.
    sox_sample_t buf[n];
    size_t m = sox_read(ft, buf, n);
    
    ここはマニュアル通り. なお,
    • nは「サンプルの合計数」.バイト数ではない.
    • sox_sample_t は 32 bitの整数. 16 bitや8 bitにする場合は適宜, スケーリングする必要がある.
    • ここで読み出されるsox_sample_tの配列は, 48000Hz, ステレオ(2チャネル). 2チャンネル分のデータは各チャネルから1サンプルずつ交互に, 配列上に並ぶ

完全なサンプル

単純だが実際にコンパイルして動かせる サンプルファイルを載せます.
 gcc sox_example.c -lsox
としてコンパイルすると, リターンキーを押してから5秒間, 音を録音し, もう一度リターンキーを押すとその音を, 16 bit, モノラル, 48000Hz, の raw形式データとして標準出力に 出力します. したがって,
$ ./a.out | play -b 16 -c 1 -e s -r 48000 - 
 リターン
 ... 5秒間しゃべる ...
 リターン
とすることで, 正確に「... 5秒間しゃべる ...」の部分で 鳴らしていた音がなるはずです. このファイルを参考に, 自分の電話でsox_open_readを読んでみてください.
#include <assert.h>
#include <sox.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char ** argv) {
  (void)argc;
  /* 5秒分 (48000 sample/sec x 2チャンネル x 5秒 */
  size_t n = 48000 * 2 * 5;

  sox_sample_t * buf = malloc(sizeof(sox_sample_t) * n);
  sox_init();

  fprintf(stderr, "使い方: %s | play -t raw -b 16 -c 1 -e s -r 48000 -\n", argv[0]);

  fprintf(stderr, "リターンキーを打つと同時に何かしゃべってください.5秒間録音します>>>");
  fflush(stderr);
  fgetc(stdin);

  sox_format_t * ft = sox_open_read("default", 0, 0, "pulseaudio");
  size_t m = sox_read(ft, buf, n);
  assert(m == n);
  sox_close(ft);
  sox_quit();

  fprintf(stderr, "録音終わりました. リターンキーを押したら, 符号付き16 bit, モノラル, 48000Hzのraw形式にして標準出力に出力します >>>");
  fflush(stderr);
  fgetc(stdin);

  {
    /* 32 bit, 2 channel -> 16 bit, 1 channel */
    size_t i;
    short * sbuf = malloc(sizeof(short) * n / 2);
    for (i = 0; i < n/2; i++) {
      sbuf[i] = (buf[2 * i] >> 16);
    }
    fwrite(sbuf, sizeof(short), n / 2, stdout);
  }

  return 0;
}

余談: 答えの見つけ方

今回の場合, 最終的な「答え」は, "default", "pulseaudio" という謎なキーワードでした. どうやってこの情報に辿り着いたかの顛末です. 失敗も含め, これを題材にして一般にこのような事態(ドキュメント不足のコードの動作を調べる時) どの辺を見るとよいかを説明します.
  • もちろん最初は, Google先生に, how to open sound default device with libsox とか入れれば答えが出てくることを期待するわけですが, 中々見つかりません
  • それ以前にmanに書かれているべきものですが,どうもlibsoxのman pageは実際との食い違いが多いようです.
  • sox.h というファイル,一般にヘッダファイルを見ると何かコメントが書いてあるというのも期待できます.普通のヘッダファイルは/usr/include/ の下にありますが,そこにない,ないしきちんと確かめたければ,
    $ gcc -M sox_example.c
    
    とするとincludeされたファイルのパス名が表示されます.これで表示された sox.hを開いて,あとは親切なコメントが書かれていることを期待するだけです. なお,libsoxのman pageには他にも色々説明がない点があるので, sox.hを見てどんな引数を渡せばよいかを見る必要は結局生じます.
  • しかし, sox_open_readに何を渡したらサウンドデバイスを開けるのか, という件に関する情報は, sox.hを真面目に読んでも出てきません.
  • 今回の場合の手がかりはともかく, soxコマンドやrecコマンドには それができているということです.なのでまっとうなドキュメント探しが 失敗したあとの作戦としては, これらのコマンドの動作を探るという手があります.つまり,
    $ rec a.wav
    
    $ sox -d a.wav
    
    はsox_open_readを呼んでいるはずで, その時の引数が何かがわかれば良いという寸法です.
  • コマンドの内部動作を知るのに便利なコマンドに strace, ltraceがあります. 前者はあるプログラムが呼んだシステムコールを,引数や返り値と共に表示してくれるものです.後者はシステムコールではなくライブラリ関数の呼び出しを表示してくれます.今の場合,sox_open_readはシステムコールではないので, ltraceを使います.
    $ ltrace sox -d a.wav
    
    とすることで, 以下のような表示が得られます.
    $ ltrace sox -d a.wav
      ... 省略 ...
    strlen("a.wav")                           = 5
    lsx_realloc(0, 6, 224, 2064)              = 0x72c5b0
    strcpy(0x72c5b0, "a.wav")                 = 0x72c5b0
    lsx_realloc(0x71c730, 16, 118, 0x61772e61) = 0x71c730
    globfree(0x7fff11fb6460, 0x3a000, 1, 2)   = 0x71c800
    sox_init_encodinginfo(0x7fff11fb64e0, 0, 0x7f5998990768, 0) = 0
    memcmp(0x7fff11fb64b0, 0x7fff11fb6600, 336, 0) = 0
    sox_get_globals(0x7fff11fb6600, 0x7fff11fb6750, 0, 0x7f5998732c5f) = 0x7f5999149ac0
    sox_write_handler(0x72c5b0, 0, 0, 0x7f5998732c5f) = 0x7f5999148320
    signal(SIGINT, 0x1)                       = 0
    sox_open_read(0x714220, 0x72a590, 0x72a5b0, 0x40a6ec) = 0x72b9e0
    sox_num_comments(0, 16, 0, -1)            = 0
    signal(SIGINT, 0)                         = 0x1
    lsx_realloc(0, 8, 0, -1)                  = 0x7319a0
    
       ...
    
    目当ての関数は見つかりましたが引数の文字列が, ポインタ(アドレス)だけの表示になっており, これまた答えまでは到達しません.
  • 最後の手段として, デバッガでsoxがsox_open_readを呼んだ瞬間を捕捉して, 引数を見てやります.
    $ which sox
    /usr/bin/sox
    
    で, soxコマンドのありかがわかったら, gdbを起動します.
    $ gdb /usr/bin/sox
      ... 省略 ...
    Type "apropos word" to search for commands related to "word"...
    Reading symbols from /usr/bin/sox...(no debugging symbols found)...done.
    (gdb) 
    
    debugging symbols foundというのは, -gを付けてコンパイルされていないということで, 例えば変数名を指定してその値を表示させるなどもできません. ですが,関数名でブレークポイントをはったりすることはできます. ここでは, sox_open_readにブレークポイントをはってそこまで走らせます.
    (gdb) b sox_open_read
    Breakpoint 1 at 0x402920
    (gdb) r -d a.wav
    Starting program: /usr/bin/sox -d a.wav
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    [New Thread 0x7fffeb80f700 (LWP 7504)]
    [Thread 0x7fffeb80f700 (LWP 7504) exited]
    
    Breakpoint 1, 0x00007ffff7b5fc00 in sox_open_read ()
       from /usr/lib/x86_64-linux-gnu/libsox.so.2
    
    何やら目的に場所で止まった感じがします.バックトレースも見て一応確認.
    (gdb) bt
    #0  0x00007ffff7b5fc00 in sox_open_read ()
       from /usr/lib/x86_64-linux-gnu/libsox.so.2
    #1  0x0000000000402c25 in ?? ()
    #2  0x00007ffff7280ec5 in __libc_start_main (main=0x402970, argc=3, 
         ... 省略 ...
    #3  0x0000000000403bfb in ?? ()
    
    さて問題はここで渡されている第一引数と第四引数の値, 正確にはそのアドレスの先にある文字列です. ここから先は知らないとどうしようもないですが, 64 bitのIntel CPUでは以下のようになっています.
    • 第1引数 = rdiレジスタ
    • 第2引数 = rsiレジスタ
    • 第3引数 = rdxレジスタ
    • 第4引数 = rcxレジスタ
    gdbでrdiレジスタの値を見たければ,
    (gdb) p $rdi
    $4 = 6361632
    
    です.ここで表示された 6361632 は実際にはアドレスです. このアドレスの先にある(6361632番地を先頭とする)文字列 を表示したければ(char *)にキャスト してあげればよいです.
    (gdb) p (char*)$rdi
    $5 = 0x611220 "default"
    
    ということでついに答えらしきものにたどり着きました. 第4引数は,
    (gdb) p (char*)$rcx
    $7 = 0x40a6ec "pulseaudio"
    
  • もちろんこんなことをしないと使えないライブラリは, イヤなのですが世の中,こういうこともあります. コード自体はありがたく使っているわけなので文句だけを 言うのではただの評論家です. フリーソフトの世界でも普及しているソフトはドキュメントが しっかりしていますが,そうでないものもあります. ただ文句を言うだけでは,最後は, 「だったら使わなければいい」と言われておしまいです. 最後は自分で中身を見て使うということもあります. 他に良い方法や情報源があったら教えてください.
  • なお今回は不要でしたが, ソースを見るというのもひとつの手です.
    $ apt-get source libsox-dev
    $ apt-get source sox
    
    などとするとそれぞれのソースが見れます. ただしソースのどこを眺めるかは簡単にはわかりません. 今の問題はsox_open_read関数に何を渡すべきか, ということなので,sox_open_read関数の中身を見ても 答えがすぐにわかるか,保証はありません. 結局soxコマンドをビルドしてそれがsox_open_readを どう呼び出しているかを探るという作戦になると思われます. ソースがあると, debug symbol (-g)付きでコンパイルできるので, 中身を調べやすくなるという利点はあります. ですが今回の場合やってみると, soxパッケージでインストールされるものとは, 微妙に動作が異なるものが出来てしまい, こちらはここで一旦引き返しました.