sndio in FreeBSD

Table of Contents

sndio in FreeBSD

ports/audio/sndioに、こんな広告が出てた。

Sndio is a small audio and MIDI framework part of the OpenBSD project.

It provides an lightweight audio & MIDI server and a fully documented
user-space API to access either the server or directly the hardware in
a uniform way.  Sndio is designed to work for desktop applications,
but pays special attention to synchronization mechanisms and
reliability required by music applications.  Reliability through
simplicity are part of the project goals.

OpenBSDの成果は、頑張って他のOSにも提供されている。どこかの奴みたいに、 自分の事しか考えない狭い心を持ったものとは、大違い。たとえば、systemd みたいな奴ね。

example/play.c

作者さんの手による模範的な音の鳴らし方。

/*
 * defaults parameters
 */
sio_initpar(&par);
par.sig = 1;
par.bits = 16;
par.pchan = 2;
par.rate = 48000;

これが現実的な設定だよな。前回の24Bitって、安物PCの音装置ではありえな い設定だろう。

while ((ch = getopt(argc, argv, "r:c:e:b:x:")) != -1) {
        switch(ch) {
        case 'r':
                if (sscanf(optarg, "%u", &par.rate) != 1) {
                        fprintf(stderr, "%s: bad rate\n", optarg);
                        exit(1);
                }
                break;

sscanfの上手な利用方法。

bufsz = par.bps * par.pchan * par.round;
buf = malloc(bufsz);

適切なバッファーサイズの決め方。

for (;;) {
        len = read(STDIN_FILENO, buf, bufsz);
	:
        if (len == 0) {
                fprintf(stderr, "eof\n");
                break;
        }
        n = sio_write(hdl, buf, len);
        if (n == 0) {
                fprintf(stderr, "sio_write: failed\n");
                exit(1);
        }
	:
}
sio_close(hdl);

メインループ。読み込みバイト数がゼロなら、それはeofに逹っした証拠ですっ て、きちんと明言してるな。前回のオイラーみたいにwhile(read){..} みたい に、判定をwhile任せにしていない。

run

大音量なので、ヴォリュームを絞っておく。

sakae@slfb:~/snd $ mixer vol=0.3:0.3
vol.volume: 0.50:0.50 -> 0.30:0.30
pcm0:mixer: <Realtek ALC269 (Analog 2.0+HP/2.0)> on hdaa0 (play/rec) (default)
    vol       = 0.30:0.30     pbk
    pcm       = 0.98:0.98     pbk
    speaker   = 0.59:0.59     rec
    rec       = 0.37:0.37     pbk
    igain     = 0.00:0.00     pbk
    ogain     = 1.00:1.00     pbk
    monitor   = 0.80:0.80     rec src

こうしておいて、リソースが一時的に読めないか、trussを使って確認。まずは、標準のやつ。 case aucat

read(4,"\0\0\0\a\M^?\M^?\M^?\M^?\0\0\^A"...,40)  = 40 (0x28)
read(4,"\0\0\0\^F\M^?\M^?\M^?\M^?\0\0\^A"...,40) = 40 (0x28)
read(4,0x20a31090,40)                            ERR#35 'Resource temporarily unavailable'
poll({ 4/POLLIN|POLLOUT },1,-1)                  = 1 (0x1)
write(4,"\0\0\0\^E\M^?\M^?\M^?\M^?\0\0\b"...,40) = 40 (0x28)
write(4,"\M-%&c\0\0\0\0\0\M^S\M-Eg\0\0\0"...,2048) = 2048 (0x800)
poll({ 4/POLLIN },1,-1)                          = 1 (0x1)
read(4,"\0\0\0\a\M^?\M^?\M^?\M^?\0\0\^A"...,40)  = 40 (0x28)
read(4,"\0\0\0\^F\M^?\M^?\M^?\M^?\0\0\^A"...,40) = 40 (0x28)
read(4,0x20a31090,40)                            ERR#35 'Resource temporarily unavailable'
poll({ 4/POLLIN|POLLOUT },1,-1)                  = 1 (0x1)
write(4,"\0\0\0\^E\M^?\M^?\M^?\M^?\0\0\b"...,40) = 40 (0x28)
write(4,"\^_\^D\M-L\M^?\0\0\0\0\M-#$\M-E"...,2048) = 2048 (0x800)
poll({ 4/POLLIN },1,-1)                          = 1 (0x1)

次はオイラーが前回作成した恐怖のサンプル。case myapp

read(3,"\0\0\0\a\M^?\M^?\M^?\M^?\0\0\0"...,40)   = 40 (0x28)
read(3,0x20a08090,40)                            ERR#35 'Resource temporarily unavailable'
poll({ 3/POLLIN },1,-1)                          = 1 (0x1)
read(3,"\0\0\0\a\M^?\M^?\M^?\M^?\0\0\0@"...,40)  = 40 (0x28)
read(3,"\0\0\0\^F\M^?\M^?\M^?\M^?\0\0\^A"...,40) = 40 (0x28)
read(3,0x20a08090,40)                            ERR#35 'Resource temporarily unavailable'
poll({ 3/POLLIN|POLLOUT },1,-1)                  = 1 (0x1)
write(3,"\0\0\0\^E\M^?\M^?\M^?\M^?\0\0\b"...,40) = 40 (0x28)
write(3,"\0\M^N\M-H\M-$\0\0\0\0\0bE\240\0"...,2048) = 2048 (0x800)

OSが代わっても、発生するな。安心したぞ。

他の方法でもチェック

河童版の作者さんにも問い合わせ。河童版 7.4 では、どうでしょうか? そうか、タイムマシンで古いOSを使ってみたらというアドバイスだな。 結果は、やはりエラーが発現した。エンバグの可能性は、これで消滅した。

ふと、sndioとの組み合わせで、まずい事が起きていると想像。ならば、音装 置に書き出すのを止めて、普通のファイルへ出力しよう。

oo = open("dmy", O_WRONLY | O_CREAT | O_TRUNC, 0666);

while(read(fd, buf, SZ)) {
  //sio_write(my, buf, SZ);
  write(oo, buf,SZ);
}

もはや、ファイルのコピー・アプリだな。勿論、結果は問題なし。そして、あ たり前だけど、瞬時に終了する。ここまできたら、アドバイザー頼リだな。

ask ChatGPT

Q: ブロックモードでオープンしてるので、こういうエラーは 発生しないと思っていたのですが、いかがでしょうか?

A: `errno 35` (Resource temporarily unavailable) は、通常 `EAGAIN` や `EWOULDBLOCK` として知られ、非ブロッキング I/O の操作で発生します。このエラーは、リソースが一時的に利用できないため、操作が完了できなかったことを意味しますが、OpenBSD の場合でもこのエラーは一般的です。

`fd = open(sndf, O_RDONLY);` はデフォルトでブロッキングモードで開かれますが、`read` システムコールが `errno 35` を返すことがあるのは、音声デバイスの遅延や、オーディオバッファが一時的にデータを受け入れられない状態にある可能性が考えられます。

対策としては、以下のいくつかの方法を試すことができます:

  1. エラー処理を追加: `read` や ` sio_write ` の戻り値が `-1` で、`errno` が `EAGAIN` または `EWOULDBLOCK` の場合は、リトライするか、`select` や `poll` で再度操作が可能になるのを待つロジックを追加することができます。

    while (1) {
        ssize_t n = read(fd, buf, SZ);
        if (n == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // リトライのために待つ処理を追加 (例: sleep, select)
                continue;
            } else {
                // 他のエラー処理
                perror("read error");
                break;
            }
        }
        if (n == 0) {
            // EOF に達した場合
            break;
        }
        sio_write(my, buf, n);
    }
    
  2. ファイルディスクリプタが非ブロッキングモードになっていないか確認: `fcntl` でファイルディスクリプタが非ブロッキングモードに設定されているかどうかを確認し、必要に応じてブロッキングモードに戻すことも可能です。

    int flags = fcntl(fd, F_GETFL, 0);
    if (flags & O_NONBLOCK) {
        fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); // ブロッキングモードに設定
    }
    
  3. バッファサイズの調整: `SZ` の値が適切でない場合、オーディオバッファが一時的に書き込みを拒否することがあります。バッファサイズを調整するか、書き込むデータのサイズを小さくすることも考慮してみてください。

これらの方法で、音声データの読み込みと書き込みが安定するか試してみてく ださい。

example/play.c

付属してたサンプルを、あえてOpenBSDでやってみる。その前に音量制御も確 認。更にその前に、sndiodを走らせておくのを忘れるな。永続的に使うなら、 /etc/rc.confに起動設定を記述しておけ。

vol

制御卓の全てを確認するには、特権が必要らしい。

ob$ doas mixerctl -av
inputs.dac-0:1=126,126
inputs.dac-2:3=126,126
record.adc-0:1_mute=off  [ off on ]
 :
outputs.master=126,126
outputs.master.mute=off  [ off on ]
outputs.master.slaves=dac-0:1,dac-2:3,spkr,hp  { dac-0:1 dac-2:3 spkr hp mic2 }

ユーザー・レベルでは、簡単操作のみが許される?

ob$ sndioctl
input.level=0.490
input.mute=0
output.level=0.494
output.mute=0
server.device=0
app/aout0.level=1.000
app/qemusys0.level=1.000
app/qemusys1.level=1.000
ob$ sndioctl output.mute=1

コンパイル & ラン

play.cをコンパイルするには、tools.[ch]も必要。

ob$ cc -c tools.c -I.
ob$ cc -g tools.o play.c -I. -lsndio

これで実行できる。

ob$ cat z.raw | ./a.out
1920 bytes buffer, max latency 17280 frames
playpos = 0, writepos = 69120, latency = 69120
playpos = 1920, writepos = 71040, latency = 69120
playpos = 3840, writepos = 72960, latency = 69120
 :
playpos = 1080960, writepos = 1150080, latency = 69120
playpos = 1082880, writepos = 1152000, latency = 69120
eof

on qemu

qemuでアプリを動かし、カーネルを追求。結論は、現象が発現せず。タイミン グの問題なのかなあ?

(gdb) bt 2
#0  dofilereadv (p=0xd19824ec, fd=3, uio=0xf3aa2d50, flags=0, retval=0xf3aa2e2c\
) at /usr/src/sys/kern/sys_generic.c:248
#1  0xd0a4e783 in sys_read (p=0xd19824ec, v=0xf3aa2e34, retval=0xf3aa2e2c) at /\
usr/src/sys/kern/sys_generic.c:167
(More stack frames follow...)

sys_read() の実体は、dofilereadv。

        cnt = uio->uio_resid;
        error = (*fp->f_ops->fo_read)(fp, uio, flags);
=>      if (error) {
                if (uio->uio_resid != cnt && (error == ERESTART ||
                    error == EINTR || error == EWOULDBLOCK))
                        error = 0;
        }

エラーを握り潰してしまう事のあるのね。EWOULDBLOCKはChatGPも指摘してく れているけど、実はEAGAIN と同値だったりする。

(gdb) p *fp->f_ops->fo_read
$7 = {int (struct file *, struct uio *, int)} 0xd098a0a0 <vn_read>
(gdb) c
Continuing.

Breakpoint 3, vn_read (fp=0xd181ade4, uio=0xf3aa2d50, fflags=0) at /usr/src/sys\
/kern/vfs_vnops.c:343

動的に関数がcallされる場合、関数名を割り出しておいてBPを設定するのが、 楽だったりする。

掘り下げる

こんなアドバイスを河豚板の作者さんから頂いた。

そうすると、sndioは全二重なので、音声の入力ストリーム側の何かですかね。
それだと、バッファが空という状況は有り得ますから、resource unavailable
とかも出そうですね。

それから、GPTさんのアドバイスも参考に、基本に立ち返ってみる。

while (1) {
  n = read(fd, buf, SZ);
  printf("n = %d\n" , n);
  if (n == 0)    {   break;  }
  n = sio_write(my, buf, n);
  printf(" n = %d\n", n);
}

何の事は無い。読み書きしたバイト数を、正直に報告する様にしただけ。

ob$ ./a.out
 :
n = 9600
 n = 9600
n = 9600
 n = 9600
n = 0

実行結果の最後の部分。EOFに逹っすると、読みだしが0で、while(1)を脱出。 それに対するktrace/kdumpの結果(見易い様に編集)。

 CALL  read(4,0x38e35138,0x2580)   -> RET   read 9600/0x2580
 CALL  write(1,0x5c8f4000,0x9)     -> RET   write 9
 CALL  write(3,0x767356f8,0x28)    -> RET   write 40/0x28
 CALL  write(3,0x38e35138,0x780)   -> RET   write 1920/0x780
*CALL  poll(0xcf7f8580,1,INFTIM)   -> RET   poll 1
*CALL  read(3,0x767356d0,0x28)     -> RET   read 40/0x28
*CALL  read(3,0x767356d0,0x28)     -> RET   read 40/0x28
*CALL  read(3,0x767356d0,0x28)     -> RET   read -1 errno 35 Resource temporarily unavailable
*CALL  poll(0xcf7f8580,1,INFTIM)   -> RET   poll 1
 CALL  write(3,0x767356f8,0x28)    -> RET   write 40/0x28
 CALL  write(3,0x38e358b8,0xf00)   -> RET   write 3840/0xf00
*CALL  poll(0xcf7f8580,1,INFTIM)   -> RET   poll 1
*CALL  read(3,0x767356d0,0x28)     -> RET   read 40/0x28
*CALL  read(3,0x767356d0,0x28)     -> RET   read 40/0x28
*CALL  read(3,0x767356d0,0x28)     -> RET   read -1 errno 35 Resource temporarily unavailable
*CALL  poll(0xcf7f8580,1,INFTIM)   -> RET   poll 1
 CALL  write(3,0x767356f8,0x28)    -> RET   write 40/0x28
 CALL  write(3,0x38e367b8,0xf00)   -> RET   write 3840/0xf00
 CALL  write(1,0x5c8f4000,0xa)     -> RET   write 10/0xa
 CALL  read(4,0x38e35138,0x2580)   -> RET   read 0
 CALL  write(1,0x5c8f4000,0x6)     -> RET   write 6

冒頭に付けた*は、ソース上には存在しない余分な物。尚、read/writeのファ イルデイスクリプターは、1: printf, 3: sio_write, 4: readを担当してる (はず)。

pollが出現するのが特徴的なんで、そこにBPを置いてみる。なお、ここからは FreeBSDのportsにあるsndioのlibsndioをgdb出来るようにした環境で実施する。 理由は、OpenBSDの環境だとlibsndioが一応gdbできる様になってるけど、-O2 でコンパイルされてて具合が悪いため。

(gdb) bt
#0  poll (pfd=0xffbfea0c, nfds=1, timeout=-1) at /usr/src/lib/libc/sys/poll.c:43
#1  0x2044c822 in sio_psleep (hdl=0x20a08000, event=4) at sio.c:269
#2  0x2044cb1b in sio_write (hdl=0x20a08000, buf=0x403ab8 <buf>, len=9600) at sio.c:391
#3  0x00401855 in main () at s.c:31

read(fd=3 ..)で止めてみると、こんな感じ。

(gdb) bt
#0  read (fd=3, buf=0x20a08090, nbytes=40) at /usr/src/lib/libc/sys/read.c:44
#1  0x204480cd in _aucat_rmsg (hdl=0x20a0808c, eof=0x20a08020) at aucat.c:95
#2  0x2044e986 in sio_aucat_runmsg (hdl=0x20a08000) at sio_aucat.c:91
#3  0x2044e73e in sio_aucat_revents (sh=0x20a08000, pfd=0xffbfea0c) at sio_aucat.c:476
#4  0x2044cd91 in sio_revents (hdl=0x20a08000, pfd=0xffbfea0c) at sio.c:433
#5  0x2044c8a8 in sio_psleep (hdl=0x20a08000, event=4) at sio.c:276
#6  0x2044cb1b in sio_write (hdl=0x20a08000, buf=0x403ab8 <buf>, len=9600) at sio.c:391
#7  0x00401855 in main () at s.c:31

hdlな中にはpar情報の他に、関数ポインターが多数隠されている。

(gdb) p *hdl->ops
$5 = {
  close = 0x2044da30 <sio_aucat_close>,
  setpar = 0x2044dab0 <sio_aucat_setpar>,
  getpar = 0x2044dc30 <sio_aucat_getpar>,
  getcap = 0x2044de80 <sio_aucat_getcap>,
  write = 0x2044e220 <sio_aucat_write>,
  read = 0x2044e380 <sio_aucat_read>,
  start = 0x2044e420 <sio_aucat_start>,
  stop = 0x2044e5e0 <sio_aucat_stop>,
  flush = 0x2044e610 <sio_aucat_flush>,
  nfds = 0x2044e650 <sio_aucat_nfds>,
  pollfd = 0x2044e660 <sio_aucat_pollfd>,
  revents = 0x2044e6e0 <sio_aucat_revents>,
  setvol = 0x2044e820 <sio_aucat_setvol>,
  getvol = 0x2044e850 <sio_aucat_getvol>
}

FreeBSD-update

13.3でアップデートすると、こんなメッセージが出てくる様になった。

WARNING: FreeBSD 13.3-RELEASE-p5 is approaching its End-of-Life date.
It is strongly recommended that you upgrade to a newer
release within the next 2 months.

これは、脅迫か。

freebsd-update -r 13.4-RELEASE upgrade

これをヤレって事かな。ヤレヤレですよ。

さくらの VPS で FreeBSD 13.1-RELEASE をセットアップ

こんな頑張りさんも中にはおられるけど。 そう言えば、OpenBSDもそろそろ降ってくるなあ。こちらは半年の猶予が有る んで、どっしりと構えていれば良い。

The current release is OpenBSD 7.6, released October 8, 2024. This is the 57th release.

来たね。


This year's Index

Home