PulseAudio:録音(2)-モニターソース

モニターソース
サーバー操作で、ソースの情報を取得した時に、2つのソースが作成されていたことを思い出してください。

=== [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" のような名前です。

後は同じように録音を行えば、現在再生中のオーディオを録音できます。
特定のシンク入力を対象にする
通常は、対象のシンクに接続されている、すべてのシンク入力(再生ストリーム)のオーディオが録音されることになりますが、特定のストリームが出力している音だけを録音することもできます。

正確には、サーバー上で作成されているシンク入力のうち、その一つを、録音の対象にすることができます。
特定のシンク入力のみを監視
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 で使用されているディスプレイサーバーによって、実際の処理は異なります。

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;
}
解説
最初に見つかったモニターソースを使います。

モニターソースの名前を取得する際、コールバック関数に渡されるデータは、コールバックが実行されている間だけ有効なので、文字列は複製する必要があります。