From 4e19d7279f23b2f793ca14587139a4bfb50a944c Mon Sep 17 00:00:00 2001 From: Hakan Ekin Date: Sun, 2 Aug 2026 20:47:44 +0300 Subject: [PATCH] Handle pw-record versions without raw support --- audio.py | 21 ++++++++++++++++++++- tests/test_audio.py | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/audio.py b/audio.py index 0ef5038..284c341 100644 --- a/audio.py +++ b/audio.py @@ -197,7 +197,7 @@ def recording_command(target=""): return cmd if shutil.which("pw-record"): cmd = [ - "pw-record", "--raw", f"--rate={RATE}", + "pw-record", *_pw_record_raw_option(), f"--rate={RATE}", f"--channels={CHANNELS}", "--format=s16", ] if target: @@ -207,6 +207,25 @@ def recording_command(target=""): return [] +def _pw_record_raw_option(): + """Use --raw only on pw-record releases that provide it. + + PipeWire 1.0, including Ubuntu 24.04's build, writes raw PCM to stdout but + rejects the newer --raw option. A rejected option ends the recorder before + it receives sound, so ask the installed binary which form it understands. + """ + try: + result = subprocess.run( + ["pw-record", "--help"], capture_output=True, text=True, timeout=2 + ) + help_text = (result.stdout or "") + (result.stderr or "") + except (subprocess.SubprocessError, OSError): + return ["--raw"] # preserve the existing command when probing itself fails + if not help_text.strip(): + return ["--raw"] + return ["--raw"] if "--raw" in help_text else [] + + class MeetingRecorder(QObject): """Microphone and speaker output into one stereo file: left is you, right is everyone else. diff --git a/tests/test_audio.py b/tests/test_audio.py index 7595d1b..9f2adf7 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -230,6 +230,23 @@ class RecordingCommand(DikteTest): with only_these_tools("pw-record"): self.assertEqual(audio.recording_command()[0], "pw-record") + def test_pw_record_uses_raw_when_the_installed_version_supports_it(self): + help_result = FakeCompleted(stdout=" --raw Write raw samples\n") + with only_these_tools("pw-record"), \ + mock.patch.object(audio.subprocess, "run", return_value=help_result): + self.assertIn("--raw", audio.recording_command()) + + def test_pw_record_omits_raw_when_pipewire_1_0_rejects_it(self): + help_result = FakeCompleted(stdout=" --rate Sample rate\n") + with only_these_tools("pw-record"), \ + mock.patch.object(audio.subprocess, "run", return_value=help_result): + self.assertNotIn("--raw", audio.recording_command()) + + def test_pw_record_help_failure_keeps_the_existing_command(self): + with only_these_tools("pw-record"), \ + mock.patch.object(audio.subprocess, "run", side_effect=OSError): + self.assertIn("--raw", audio.recording_command()) + def test_neither_is_installed(self): with only_these_tools(): self.assertEqual(audio.recording_command(), []) @@ -282,6 +299,7 @@ class RecorderChain(DikteTest): recorder.failed.connect(failures.append) proc = FakeProcess(data) with only_these_tools("pw-record"), \ + mock.patch.object(audio, "_pw_record_raw_option", return_value=[]), \ mock.patch.object(subprocess, "Popen", return_value=proc) as popen: recorder.start(target=target, max_seconds=max_seconds) recorder._thread.join(timeout=5) @@ -328,6 +346,7 @@ class RecorderChain(DikteTest): recorder.stopped.connect(lambda *args: results.append(args)) proc = FakeProcess(tone(1.0)) with only_these_tools("pw-record"), \ + mock.patch.object(audio, "_pw_record_raw_option", return_value=[]), \ mock.patch.object(subprocess, "Popen", return_value=proc): recorder.start() recorder._thread.join(timeout=5)