アルアカ - Arcadia Academia

Arcadia Academiaは「エンジニアリングを楽しむ」を合言葉に日本のデジタル競争力を高めることをミッションとするテックコミュニティです。

pw-recordの--targetが無視されてマイクを録り続ける時の直し方

Featured image of the post

「デスクトップ側の音だけを録りたいのに、なぜかマイクの音が入る」 「--target にモニターを指定しているのに、値を変えても録れる音が変わらない」 「エラーは一切出ていないのに、録音結果だけが期待と違う」

PipeWire の pw-record でこうした症状に当たったら、原因はほぼ1つです。--target に渡した値が受け付けられておらず、既定の入力(=マイク)を録り続けています。しかも pw-record は解釈できないターゲット指定を黙って無視するので、エラーもワーニングも出ません。

この記事では、この「黙って別のものを録っている」状態を1回で見抜く測り方と、--target に渡すべき正しい値である object.serial の引き方、名前で指定したい場合の代替手段までをまとめます。

💡
30秒で要点
pw-record --target が受け付けるのは object.serial だけ。node.namewpctl status に出ている ID も黙って無視され、既定のマイクを録り続ける
エラーが出ないので、症状は「モニターにノイズが乗る」「PipeWire とはそういうものだ」という誤った結論に化ける
見抜く決め手は、比較条件に「存在しないターゲット名」を1つ混ぜること。全条件が同じ値になったら指定が効いていない
object.serialpw-dump の props から引く。正しく指定できていれば、無音時の RMS は 0 になる
名前で指定したいなら ffmpeg -f pulse -i <node.name>.monitor が確実

目次を開く

症状: エラーが出ないまま、ずっと既定の入力を録っている

PipeWire でデスクトップ側の音(アプリが鳴らしている音)を録るときは、出力デバイスに付いてくるモニターソースを入力として指定します。pw-record にはそのための --target があるので、素直に書くとこうなります。

pw-record --rate=16000 --channels=1 --format=s16 \
  --target=alsa_output.usb-xxxx.analog-stereo.monitor out.wav

これは通ります。終了コードは 0 で、out.wav もちゃんとできます。ところが録れている中身は既定の入力デバイス、つまりマイクであることがあります。pw-record は解釈できない --target を無視して既定の入力にフォールバックし、その事実をどこにも出しません。

厄介なのは、この状態でも「それらしい音」が録れてしまうことです。マイクは環境音を拾うため、無音のはずのモニターに常時ノイズが乗っているように見え、次のような誤った結論に落ち着きがちです。

  • 「Bluetooth スピーカーのモニターはノイズが乗るから使えない」
  • 「USB オーディオ機器でも同じ値だから、これは PipeWire の作りとしてそういうものだ」

どちらも間違いで、実際には最初から最後までマイクの音を測っていた、ということが起こります。

「絶対に何も録れないはずの条件」を1つ混ぜる

観測値が期待と違うとき、対象の問題なのか観測系の問題なのかを一発で割る方法があります。比較条件の中に「絶対に何も録れないはずの条件」を1つ混ぜることです。ここでは存在しないターゲット名を渡します。

何も再生していない状態で、同じ長さの録音を4条件ぶん取って RMS と最大値を比べた結果が次です。

--target に渡した値 平均RMS 最大RMS
指定なし(既定=マイク) 215 266
HDMI 出力のモニター(音源なし) 210 250
USB オーディオ機器のモニター 212 263
存在しない名前 228 290

存在しない名前を渡した条件でも同じくらい録れている時点で答えが出ます。--target は読まれておらず、4条件すべてが同じ入力を録っていたということです。本当にモニターを録れているなら、何も再生していない条件での RMS はほぼ 0 になります。

RMS は追加のツール無しで測れます。

# 5秒だけ録る(SIGINT で終わらせるとヘッダが正しく書かれる)
timeout -s INT 5 pw-record --rate=16000 --channels=1 --format=s16 \
  --target=this-target-does-not-exist test.wav

# 平均RMSと最大値を出す(Python標準ライブラリのみ)
python3 - test.wav <<'PY'
import struct, sys, wave
with wave.open(sys.argv[1]) as w:
    n = w.getnframes()
    s = struct.unpack("<%dh" % n, w.readframes(n))
print("rms", int((sum(x * x for x in s) / len(s)) ** 0.5), "max", max(abs(x) for x in s))
PY

このやり方は録音に限りません。「観測値が全条件で同じ」ときは、条件ではなく観測系を疑うべきだ、という切り分けにそのまま使えます。

正解は object.serial を渡すこと

--target が受け付けるのは object.serial の値です。node.namealsa_output....monitor のような文字列)でも、wpctl status の一覧に出ている ID でもありません。

object.serialpw-dump の props から引けます。

pw-dump | python3 -c '
import json, sys
for n in json.load(sys.stdin):
    p = (n.get("info") or {}).get("props") or {}
    if p.get("node.name"):
        print(p.get("object.serial"), p.get("media.class"), p.get("node.name"))
'

media.classAudio/Sink の行が出力デバイスです。そのモニターを録りたいので、対応する行の object.serial を控えて --target に数値で渡します。

pw-record --rate=16000 --channels=1 --format=s16 --target=213467 out.wav

正しく指定できていれば、何も再生していない状態での RMS は 0 になります。 前の節のスクリプトでもう一度測って、0 になることを必ず確認してください。ここが 0 にならないなら、まだマイクを録っています。

ひとつ注意点があります。object.serial はデバイスの抜き差しや PipeWire の再起動で変わります。スクリプトに数字を直書きすると、翌日には別のものを録り始めます。

名前で指定したいなら ffmpeg の PulseAudio 互換レイヤー

node.name で書きたい場合は、ffmpeg の PulseAudio 入力を使うほうが確実です。

ffmpeg -f pulse -i alsa_output.usb-xxxx.analog-stereo.monitor \
  -ac 1 -ar 16000 -f wav out.wav

同じ条件で、無音時 0・発話中のピーク 2340 と正しく録れます。名前で書けるのでスクリプトに埋め込んでも壊れにくく、object.serial が変わる問題も同時に消えます。デバイス名は先ほどの pw-dump の一覧から node.name をそのまま使い、末尾に .monitor を付けてください。

恒久対策: 名前から serial を引くヘルパを1本用意する

pw-record を使い続けるなら、node.name から object.serial を引くヘルパを1本作って、シェルからは必ずそれ越しに呼ぶ形にします。数値を直書きする箇所をゼロにするのが目的です。

#!/usr/bin/env bash
# pw-serial <node.name> → object.serial を1つ標準出力に返す
set -euo pipefail
pw-dump | python3 -c '
import json, sys
want = sys.argv[1]
for n in json.load(sys.stdin):
    p = (n.get("info") or {}).get("props") or {}
    if p.get("node.name") == want:
        print(p["object.serial"]); break
else:
    sys.exit("not found: " + want)
' "$1"

呼び出し側はこうなります。

SINK=alsa_output.usb-xxxx.analog-stereo
pw-record --rate=16000 --channels=1 --format=s16 \
  --target="$(pw-serial "$SINK.monitor")" out.wav

見つからなければ pw-serial が非ゼロで落ちるので、間違ったターゲットで静かに録り続ける事態そのものが起きなくなりますpw-dump の JSON を扱う処理は後から増えがちなので、この手のヘルパは1ファイルにまとめておくと事故が減ります。

まとめ

  • pw-record --target が受け付けるのは object.serial だけ。node.namewpctl status の ID も黙って無視され、既定のマイクにフォールバックする
  • エラーが出ないため、「モニターにノイズが乗る」という別の問題に見えてしまう。誤診したまま何日も進める種類のハマりどころ
  • 比較条件に「存在しないターゲット名」を1つ混ぜると、対象の問題か観測系の問題かが1回で割れる
  • object.serialpw-dump の props から引く。指定が効いていれば無音時 RMS は 0 になるので、必ず実測で確認する
  • object.serial は環境の変化で変わるため直書きしない。名前で書きたいなら ffmpeg -f pulse を使うか、名前から serial を引くヘルパ越しに呼ぶ

よくある質問(FAQ)

wpctl status に出ている番号を渡してはいけませんか

wpctl status の一覧に出ている ID は object.serial とは別の値なので、渡しても --target には効きません。しかもエラーにならず既定の入力を録るだけなので、動いているように見えてしまいます。ID の見た目が数値で object.serial も数値なぶん、取り違えに気づきにくい組み合わせです。

録音せずに「指定が効いているか」を確かめられますか

確実なのは実測です。何も再生していない状態で録って RMS が 0 になるかを見るのが、いちばん短い確認方法になります。オプションの受理そのものはツールの版によって挙動が変わり得るので、手元の版の --help と実測の両方で確かめてください。

モニターを録ると通知音やブラウザの音も混ざりますか

混ざります。モニターはその出力デバイスに流れている音をまとめて拾うため、目的のアプリ以外の通知音やブラウザの音も一緒に入ります。特定のアプリだけを録りたい場合は、そのアプリの出力を専用の仮想シンクへ回して、そちらのモニターを録る構成にします。

無音のはずなのに RMS が 0 になりません

まだマイクを録っている可能性がまず疑わしいので、存在しないターゲット名との比較をもう一度試してください。値が変わらなければ指定が効いていません。値が明確に下がっているのに 0 にならない場合は、そのデバイスに実際に何かの音が流れている(通知音や常駐アプリの出力)か、録音した区間に音の出ている時間が含まれています。

プログラミング学習でお悩みですか?

現役エンジニアがあなたの学習をマンツーマンでサポートします。

  • 学習の進め方がわからない
  • ポートフォリオの作り方を知りたい
  • 現場で使える技術を学びたい
まずは30分の無料相談

相談は完全無料・オンラインで気軽に

あなたを爆速で成長させるメンタリングプログラムはこちら

メンタープログラムバナー

学習・開発のお悩みは現役エンジニアに相談

メンタープログラムの詳細を見る

エンジニアの基礎学習ゲーム

プログラミング学習支援

無料相談はこちら