はじめに
EEIC Advent Calendar 2018 その2 の 14日目です。
この記事は、東京大学工学部 電気電子・電子情報工学科3年生必修「前期実験」のうち、IP電話を作る I1I2I3実験 のヒントになればと思って書いたものであり、特に面白くもない技術記事であります。
IP電話サーバについて、recコマンドとIP電話サーバをパイプでつなぐと待ち受け中も録音してしまう という問題を回避する1つの策として、テキストには libsox を使うようヒントが書かれていますが、どうやら libsox は難しいようです。これと近いアプローチを検討した結果、 libpulse を用いて音声を再生・録音することに成功しました。これについて日本語情報が無いようなので紹介します。
PulseAudio について
自分も正確には分かっていませんが、音を鳴らしたいソフトの取りまとめをしてくれるデーモン的なソフトと認識しています。サウンドカードとデータのやり取りをする ALSA というソフトの上位に位置し、各アプリがサウンドデバイスを独占しているように見せるため仮想化を行っているそうです。詳しくは公式サイトや Wikipedia などで調べてください。
ともかく、 pulseaudio プロセスが起動していれば、そこのサーバへ音声データを送れば再生されるし、そこのサーバへ要求すれば録音された音声データが得られるということです。
本題
環境
Linux Mint 18.3 x86_64
pulseaudio 8.0
gcc 5.4.0
Ubuntu でいうと 16.04 LTS に相当する環境です。古いです。
pulseaudioが動いているかを確認しておきます。
$ ps ax | grep "pulseaudio"
2289 ? S<l 0:00 /usr/bin/pulseaudio --start --log-target=syslog
開発に必要なパッケージを入れておいてください。
$ sudo apt-get install libpulse-dev
方針
まずは公式ドキュメントを見ます。
バージョンは違いますが API は変わっていないようです。
Simple API と Asynchronous API の2種類のAPIが利用できますが、 Simple API の方が簡単ですのでこちらを使います。同期的取得なので、 PulseAudio API 呼び出しをしたら結果が用意されるまでそのスレッドの実行が止まってしまいますが、それはアプリ側で何とかしましょう。
音声の再生
早速サンプルコードです。
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pulse/error.h> /* pulseaudio */
#include <pulse/simple.h> /* pulseaudio */
#define APP_NAME "pulseaudio_sample"
#define STREAM_NAME "play"
#define DATA_SIZE 1024
int main() {
int pa_errno, pa_result, read_bytes;
pa_sample_spec ss;
ss.format = PA_SAMPLE_S16LE;
ss.rate = 48000;
ss.channels = 1;
pa_simple *pa = pa_simple_new(NULL, APP_NAME, PA_STREAM_PLAYBACK, NULL, STREAM_NAME, &ss, NULL, NULL, &pa_errno);
if (pa == NULL) {
fprintf(stderr, "ERROR: Failed to connect pulseaudio server: %s\n", pa_strerror(pa_errno));
return 1;
}
char data[DATA_SIZE];
while (1) {
read_bytes = read(STDIN_FILENO, data, DATA_SIZE);
if (read_bytes == 0) {
break;
} else if (read_bytes < 0) {
fprintf(stderr, "ERROR: Failed to read data from stdin: %s\n", strerror(errno));
return 1;
}
pa_result = pa_simple_write(pa, data, read_bytes, &pa_errno);
if (pa_result < 0) {
fprintf(stderr, "ERROR: Failed to write data to pulseaudio: %s\n", pa_strerror(pa_errno));
return 1;
}
}
pa_simple_free(pa);
return 0;
}
標準入力から音声データを読み込んで再生します。簡単のため音声ファイルは raw 形式(≒ wav ファイルのヘッダーを取ったもの)としていますので、お手持ちの音楽ファイルなどでテスト再生するときは例えば SoX で変換してパイプでつないでください。
pa_sample_spec
構造体は、音声ファイルの形式を指定するもので 必須 です。サンプルコードでの指定内容は以下の通りです。
- format = 16bit 符号つき整数 PCM リトルエンディアン 1 でサンプリング
- rate = 標本化周波数 48kHz
- channels = モノラル
続いて、 pa_simple_new
関数で、 PulseAudio サーバと接続します。最低限必要なパラメータの説明は以下の通りです。
- server: 接続するサーバ (NULL = default)
- name: クライアントの名前 (自由)
- dir: 再生ならば
PA_STREAM_PLAYBACK
、録音ならばPA_STREAM_RECORD
を書く - dev: (NULL = default)
- stream_name: ストリームの名前 (自由)
- ss: 作っておいた
pa_sample_spec
構造体 - map: (NULL = default)
- attr: (NULL = default)
- error: ポインタを指定しておくと errno を入れてくれるが NULL でもいい
メインループ内にある pa_simple_write
関数で、 PulseAudio サーバに再生したい音声データを送ります。第2引数で指定する data から送り出すデータ量は第3引数に入れますが、サンプル数ではなくバイト単位で指定します。従って、標準入力から読んだ音声データは char 型の配列で持っておくのがよいです。
最後に pa_simple_free
関数で、 PulseAudio サーバとの接続を切ります。
コンパイル・リンク時には、gcc に -lpulse -lpulse-simple
のオプションを付けることをお忘れなく。
音声の録音
早速サンプルコードです。
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pulse/error.h> /* pulseaudio */
#include <pulse/simple.h> /* pulseaudio */
#define APP_NAME "pulseaudio_sample"
#define STREAM_NAME "rec"
#define DATA_SIZE 1024
int main() {
int pa_errno, pa_result, written_bytes;
pa_sample_spec ss;
ss.format = PA_SAMPLE_S16LE;
ss.rate = 48000;
ss.channels = 1;
pa_simple *pa = pa_simple_new(NULL, APP_NAME, PA_STREAM_RECORD, NULL, STREAM_NAME, &ss, NULL, NULL, &pa_errno);
if (pa == NULL) {
fprintf(stderr, "ERROR: Failed to connect pulseaudio server: %s\n", pa_strerror(pa_errno));
return 1;
}
char data[DATA_SIZE];
while (1) {
pa_result = pa_simple_read(pa, data, DATA_SIZE, &pa_errno);
if (pa_result < 0) {
fprintf(stderr, "ERROR: Failed to read data from pulseaudio: %s\n", pa_strerror(pa_errno));
return 1;
}
written_bytes = write(STDOUT_FILENO, data, DATA_SIZE);
if (written_bytes < DATA_SIZE) {
fprintf(stderr, "ERROR: Failed to write data to stdout: %s\n", strerror(errno));
return 1;
}
}
pa_simple_free(pa);
return 0;
}
PA_STREAM_PLAYBACK
が PA_STREAM_RECORD
に、 pa_simple_write
が pa_simple_read
に変わった以外はほとんど共通です。 pa_simple_read
関数についても、引数は write のときと同じです。きっちり bytes (第3引数) バイト分が data (第2引数) に詰め込まれるか、エラーになるかです。
再生と録音の両方ができましたので、 録音 | 再生
とパイプでつなげば、マイクで拾った音声をほぼリアルタイムにイヤホン等で聞くことができます。
-
Intel 系の CPU も wav ファイルもリトルエンディアン ↩