モニターソース
サーバー操作で、ソースの情報を取得した時に、2つのソースが作成されていたことを思い出してください。
前回の録音で接続されていたのは、「内部オーディオ アナログステレオ」の方です。
実際のハードウェア上のオーディオ入力が扱われるので、サーバーにソースを選択させた場合、通常はこちらが使用されます。
もう一つの「Monitor of 内部オーディオ アナログステレオ」は、仮想的に作成されたソースです。
これは、シンクに送られた、再生用の出力を、オーディオ入力として扱います。
つまり、クライアントの録音ストリームを、このソースに接続すると、デスクトップ上で現在再生されているオーディオを入力として扱い、録音することができます。
このソースは、”モニターソース”と呼ばれます。
モニターソースの場合、シンク情報の monitor_of_sink は、対応するシンクのインデックスになり、monitor_of_sink_name は、そのシンクの名前になります。
通常のソースの場合、monitor_of_sink は PA_INVALID_INDEX、monitor_of_sink_name は NULL になります。
シンクの情報をおさらいしてみると、作成されたシンクは1つで、名前(接続時に指定するシンク名)は、「alsa_output.pci-0000_00_1f.3.analog-stereo」になっていました。
この名前に .monitor を付けると、「alsa_output.pci-0000_00_1f.3.analog-stereo.monitor」となり、モニターソースの名前と一致します。
また、monitor_source は、このシンクに接続しているモニターソースのインデックス、monitor_source_name は、モニターソースの名前です。
=== [0] === name: alsa_output.pci-0000_00_1f.3.analog-stereo.monitor description: 'Monitor of 内部オーディオ アナログステレオ' monitor_of_sink: 0 monitor_of_sink_name: alsa_output.pci-0000_00_1f.3.analog-stereo === [1] === name: alsa_input.pci-0000_00_1f.3.analog-stereo description: '内部オーディオ アナログステレオ' monitor_of_sink: 4294967295 monitor_of_sink_name: (null)
前回の録音で接続されていたのは、「内部オーディオ アナログステレオ」の方です。
実際のハードウェア上のオーディオ入力が扱われるので、サーバーにソースを選択させた場合、通常はこちらが使用されます。
もう一つの「Monitor of 内部オーディオ アナログステレオ」は、仮想的に作成されたソースです。
これは、シンクに送られた、再生用の出力を、オーディオ入力として扱います。
つまり、クライアントの録音ストリームを、このソースに接続すると、デスクトップ上で現在再生されているオーディオを入力として扱い、録音することができます。
このソースは、”モニターソース”と呼ばれます。
モニターソースの場合、シンク情報の monitor_of_sink は、対応するシンクのインデックスになり、monitor_of_sink_name は、そのシンクの名前になります。
通常のソースの場合、monitor_of_sink は PA_INVALID_INDEX、monitor_of_sink_name は NULL になります。
シンクの情報
=== [0] === name: alsa_output.pci-0000_00_1f.3.analog-stereo description: '内部オーディオ アナログステレオ' sample_spcec: s16le 2ch 44100Hz channel_map: 'front-left,front-right' owner_module: 6 volume: '0: 23% 1: 23%' mute: 0 monitor_source: 0 monitor_source_name: alsa_output.pci-0000_00_1f.3.analog-stereo.monitor
シンクの情報をおさらいしてみると、作成されたシンクは1つで、名前(接続時に指定するシンク名)は、「alsa_output.pci-0000_00_1f.3.analog-stereo」になっていました。
この名前に .monitor を付けると、「alsa_output.pci-0000_00_1f.3.analog-stereo.monitor」となり、モニターソースの名前と一致します。
また、monitor_source は、このシンクに接続しているモニターソースのインデックス、monitor_source_name は、モニターソースの名前です。
接続方法
実際に、録音時にモニターソースと接続する場合は、モニターソースの名前 (name) が必要になります。
サーバー上のシンクまたはソース情報が必要になるので、シンクの情報から monitor_source_name を参照するか、ソースの一覧を取得します。
ソースの monitor_of_sink が PA_INVALID_INDEX でなければ、モニターソースです。そのソースの name を使います。
モニターソースの名前が取得できたら、pa_stream_connect_record() の dev 引数に、その名前を指定します。
例えば、"alsa_output.pci-0000_00_1f.3.analog-stereo.monitor" のような名前です。
後は同じように録音を行えば、現在再生中のオーディオを録音できます。
サーバー上のシンクまたはソース情報が必要になるので、シンクの情報から monitor_source_name を参照するか、ソースの一覧を取得します。
ソースの monitor_of_sink が PA_INVALID_INDEX でなければ、モニターソースです。そのソースの name を使います。
モニターソースの名前が取得できたら、pa_stream_connect_record() の dev 引数に、その名前を指定します。
例えば、"alsa_output.pci-0000_00_1f.3.analog-stereo.monitor" のような名前です。
後は同じように録音を行えば、現在再生中のオーディオを録音できます。
特定のシンク入力を対象にする
通常は、対象のシンクに接続されている、すべてのシンク入力(再生ストリーム)のオーディオが録音されることになりますが、特定のストリームが出力している音だけを録音することもできます。
正確には、サーバー上で作成されているシンク入力のうち、その一つを、録音の対象にすることができます。
録音ストリームがモニターソースに接続される場合、指定したシンク入力(インデックス番号)のみを録音対象とします。
※pa_stream_connect_record() の前に呼び出す必要があります。
pa_stream_get_monitor_stream() は、監視に指定されたシンク入力のインデックス番号を返します。
失敗時は PA_INVALID_INDEX が返ります。
サーバー操作で取得できる情報内のプロパティリストは、数値の場合も含めて、すべて文字列の値になっています。
数値として取得したいなら、文字列から変換します。
pa_proplist_gets() は、値が有効な UTF-8 文字列でない場合、NULL が返ります。
正確には、サーバー上で作成されているシンク入力のうち、その一つを、録音の対象にすることができます。
特定のシンク入力のみを監視
int pa_stream_set_monitor_stream(pa_stream *s, uint32_t sink_input_idx); uint32_t pa_stream_get_monitor_stream(const pa_stream *s);
録音ストリームがモニターソースに接続される場合、指定したシンク入力(インデックス番号)のみを録音対象とします。
※pa_stream_connect_record() の前に呼び出す必要があります。
pa_stream_get_monitor_stream() は、監視に指定されたシンク入力のインデックス番号を返します。
失敗時は PA_INVALID_INDEX が返ります。
シンク入力情報
シンク入力のインデックス番号を選ぶ場合は、シンク入力のプロパティリスト (proplist) の情報を元に選択します。
アプリケーション名やストリーム名、アイコン名などを参照することができるので、ユーザーに選択させるなら、この情報を使って、一覧を表示することができます。
アプリケーション名やストリーム名、アイコン名などを参照することができるので、ユーザーに選択させるなら、この情報を使って、一覧を表示することができます。
<proplist> media.icon_name = "SMPlayer" media.name = "test.mp4" application.name = "SMPlayer" native-protocol.peer = "UNIX socket client" native-protocol.version = "35" application.process.id = "***" application.process.user = "***" application.process.host = "***" application.process.binary = "mpv" application.language = "C" window.x11.display = ":0" application.process.machine_id = "***" application.process.session_id = "2" application.icon_name = "mpv" module-stream-restore.id = "sink-input-by-application-name:SMPlayer"
プロパティリストの操作
//指定キーが存在するか int pa_proplist_contains(const pa_proplist *p, const char *key); //指定キーの値の文字列のポインタを返す (UTF-8) const char *pa_proplist_gets(const pa_proplist *p, const char *key);
サーバー操作で取得できる情報内のプロパティリストは、数値の場合も含めて、すべて文字列の値になっています。
数値として取得したいなら、文字列から変換します。
pa_proplist_gets() は、値が有効な UTF-8 文字列でない場合、NULL が返ります。
ウィンドウ情報
ここからさらに、そのクライアントのウィンドウの情報を調べたい場合は、プロセスID (application.process.id) などを使います。
ただし、X11 や Wayland など、PC で使用されているディスプレイサーバーによって、実際の処理は異なります。
そして、XGetWindowProperty() で、各ウィンドウのプロパティを取得します。
"_NET_WM_PID" (CARDINAL) の情報を取得すれば、ウィンドウを所持しているクライアントのプロセス ID がわかるので、これにより、シンク入力のクライアントと X11 ウィンドウを関連付けることができます。
ただし、X11 や Wayland など、PC で使用されているディスプレイサーバーによって、実際の処理は異なります。
X11
X11 の場合は、ルートウィンドウ直下の子ウィンドウが、トップレベルウィンドウなので、XQueryTree() でウィンドウのツリー情報を取得し、ウィンドウ ID を得ます。そして、XGetWindowProperty() で、各ウィンドウのプロパティを取得します。
"_NET_WM_PID" (CARDINAL) の情報を取得すれば、ウィンドウを所持しているクライアントのプロセス ID がわかるので、これにより、シンク入力のクライアントと X11 ウィンドウを関連付けることができます。
プログラム
モニターソースに接続して、5秒間録音し、record.wav に出力するプログラムです。
他のアプリで、何かしら再生を行っている状態で実行してください。
他のアプリで、何かしら再生を行っている状態で実行してください。
$ cc -o 18-record2 18-record2.c util.c -lpulse
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <pulse/pulseaudio.h> #include "util.h" #define SAMPRATE 48000 PulseData *pulse; char *source_name = NULL; //読み込み可能になった時 static void _cb_read(pa_stream *p,size_t nbytes,void *userdata) { printf("- readable: %d\n", (int)nbytes); pa_threaded_mainloop_signal(pulse->mainloop, 0); } //読み込み (5秒分) static uint32_t _read_data(PulseData *p,FILE *fp) { pa_stream *strm = p->stream_rec; const void *readbuf; size_t readsize; uint32_t writesize = 0; pa_threaded_mainloop_lock(p->mainloop); while(writesize < SAMPRATE * 4 * 5) { //読み込み可能なサイズ取得 while(1) { readsize = pa_stream_readable_size(strm); if(readsize == (size_t)-1) goto END; if(readsize) break; pa_threaded_mainloop_wait(p->mainloop); } //読み込み if(pa_stream_peek(strm, &readbuf, &readsize)) break; printf("* peek: %c %d\n", (readbuf)? 'o':'-', (int)readsize); //書き込み if(readbuf) { fwrite(readbuf, 1, readsize, fp); writesize += readsize; } //削除 if(readsize) pa_stream_drop(strm); } END: pa_threaded_mainloop_unlock(p->mainloop); return writesize; } static void _source_callback(pa_context *c,const pa_source_info *i,int eol,void *userdata) { if(!i) { pa_threaded_mainloop_signal(pulse->mainloop, 0); return; } //名前を複製 if(!source_name && i->monitor_of_sink != PA_INVALID_INDEX) source_name = strdup(i->name); } static pa_operation *_get_source(PulseData *p,void *data) { return pa_context_get_source_info_list(pulse->ctx, _source_callback, NULL); } int main(void) { FILE *fp; uint32_t size; pulse = pulse_connect(0); if(!pulse) return 1; //モニターソースの名前を取得 pulse_wait_operation(pulse, _get_source, NULL); if(!source_name) { printf("! unfound monitor source\n"); goto END; } printf("monitor_source: '%s'\n", source_name); //作成&接続 if(pulse_create_stream_record(pulse, PA_SAMPLE_S16LE, SAMPRATE, 2, source_name, 0, NULL)) goto END; //コールバック pa_threaded_mainloop_lock(pulse->mainloop); pa_stream_set_read_callback(pulse->stream_rec, _cb_read, NULL); pa_threaded_mainloop_unlock(pulse->mainloop); //録音 fp = fopen("record.wav", "wb"); if(!fp) goto END; pulse_write_wave_header(fp, SAMPRATE, 2); size = _read_data(pulse, fp); printf("* write size: %u\n", size); pulse_write_wave_result_size(fp, size); fclose(fp); // END: if(source_name) free(source_name); pulse_free(pulse); return 0; }
解説
最初に見つかったモニターソースを使います。
モニターソースの名前を取得する際、コールバック関数に渡されるデータは、コールバックが実行されている間だけ有効なので、文字列は複製する必要があります。
モニターソースの名前を取得する際、コールバック関数に渡されるデータは、コールバックが実行されている間だけ有効なので、文字列は複製する必要があります。