Commit bf763762 by PLN (Algolia)

autoroute: Master follows the default output, and SuperDirt gets no microphone

Master tracks the default sink (HDMI, the jack, a Bluetooth headset) only while
its wiring is the one autoroute made; anything else (qjackctl, several sinks)
is custom and left alone, and disconnecting all of Master's outputs hands
control back. The new link is made before the old one is cut. The default-sink
lookup runs every 2nd tick, not every tick.

SuperDirt takes no mic: capture -> SuperCollider:in_* links are cut, with
TIDAL_AUTOROUTE_SC_MICS=1 to keep them, documented in the script header.
parent 425bf627
...@@ -34,7 +34,7 @@ MASTER_ON_SPEAKER = (f"ardour:Master/audio_out 1\n |-> {SINK}:playback_FL\n" ...@@ -34,7 +34,7 @@ MASTER_ON_SPEAKER = (f"ardour:Master/audio_out 1\n |-> {SINK}:playback_FL\n"
f"ardour:Master/audio_out 2\n |-> {SINK}:playback_FR\n") f"ardour:Master/audio_out 2\n |-> {SINK}:playback_FR\n")
def _reconcile(tmp_path, lo, inputs=INPUTS, default_sink=SINK, extra_env=None): def _reconcile(tmp_path, lo, inputs=INPUTS, default_sink=SINK, extra_env=None, state=None):
stubs = tmp_path / "bin" stubs = tmp_path / "bin"
stubs.mkdir(exist_ok=True) stubs.mkdir(exist_ok=True)
calls = tmp_path / "pw-link.calls" calls = tmp_path / "pw-link.calls"
...@@ -52,7 +52,12 @@ def _reconcile(tmp_path, lo, inputs=INPUTS, default_sink=SINK, extra_env=None): ...@@ -52,7 +52,12 @@ def _reconcile(tmp_path, lo, inputs=INPUTS, default_sink=SINK, extra_env=None):
(stubs / "pactl").write_text(f"#!/bin/sh\n[ \"$1\" = get-default-sink ] && echo '{default_sink}'\n") (stubs / "pactl").write_text(f"#!/bin/sh\n[ \"$1\" = get-default-sink ] && echo '{default_sink}'\n")
for s in stubs.iterdir(): for s in stubs.iterdir():
s.chmod(0o755) s.chmod(0o755)
env = dict(os.environ, PATH=f"{stubs}:{os.environ['PATH']}") state_dir = tmp_path / "state"
state_dir.mkdir(exist_ok=True)
if state is not None:
(state_dir / "master-sink").write_text(state)
env = dict(os.environ, PATH=f"{stubs}:{os.environ['PATH']}",
TIDAL_AUTOROUTE_STATE_DIR=str(state_dir))
env.update(extra_env or {}) env.update(extra_env or {})
out = subprocess.run(["bash", "-c", f"source '{SCRIPT}'\nreconcile"], out = subprocess.run(["bash", "-c", f"source '{SCRIPT}'\nreconcile"],
env=env, capture_output=True, text=True, timeout=20) env=env, capture_output=True, text=True, timeout=20)
...@@ -163,5 +168,149 @@ def test_master_on_a_bluetooth_headset_is_not_dangling(tmp_path): ...@@ -163,5 +168,149 @@ def test_master_on_a_bluetooth_headset_is_not_dangling(tmp_path):
"""Monitoring on the Bose while the default sink is the laptop speaker: linking """Monitoring on the Bose while the default sink is the laptop speaker: linking
Master to the speaker as well would leak the whole set into the room.""" Master to the speaker as well would leak the whole set into the room."""
calls, out = _reconcile(tmp_path, CTRL_LINKED + MASTER_ON_BOSE) calls, out = _reconcile(tmp_path, CTRL_LINKED + MASTER_ON_BOSE)
assert not [c for c in calls if c.startswith("ardour:Master")] assert not [c for c in calls if c.startswith("ardour:Master") or c.startswith("-d")]
assert out == "" assert "custom wiring" in out # said once, not acted on
# --------------------------------------------------------------------------- #
# Master follows the default output, unless wired by hand
# --------------------------------------------------------------------------- #
HDMI = "alsa_output.pci-0000_00_1f.3-platform-sof_sdw.HiFi__hw_sofsoundwire_5__sink"
JACK = "alsa_output.pci-0000_00_1f.3-platform-sof_sdw.HiFi__hw_sofsoundwire__sink"
def _inputs_for(*sinks):
ins = "ardour:Tidal 01/audio_in 1\nardour:Tidal 01/audio_in 2\n"
for sk in sinks:
ins += f"{sk}:playback_FR\n{sk}:playback_FL\n"
return ins
def _master_on(*sinks):
out = ""
for ch, port in (("1", "FL"), ("2", "FR")):
out += f"ardour:Master/audio_out {ch}\n"
out += "".join(f" |-> {sk}:playback_{port}\n" for sk in sinks)
return out
def _state(tmp_path):
f = tmp_path / "state" / "master-sink"
return f.read_text() if f.exists() else None
def test_master_follows_the_default_output_when_it_moves(tmp_path):
"""HDMI today, the jack tomorrow: Master we linked follows the default."""
calls, out = _reconcile(tmp_path, CTRL_LINKED + ORBIT_1_LINKED + _master_on(HDMI),
inputs=_inputs_for(HDMI, JACK), default_sink=JACK, state=HDMI)
assert f"ardour:Master/audio_out 1 {JACK}:playback_FL" in calls
assert f"ardour:Master/audio_out 2 {JACK}:playback_FR" in calls
assert f"-d ardour:Master/audio_out 1 {HDMI}:playback_FL" in calls
assert f"-d ardour:Master/audio_out 2 {HDMI}:playback_FR" in calls
assert _state(tmp_path) == JACK and "followed the default output" in out
def test_the_new_link_is_made_before_the_old_one_is_cut(tmp_path):
"""No silent gap while the output moves."""
calls, _ = _reconcile(tmp_path, CTRL_LINKED + ORBIT_1_LINKED + _master_on(HDMI),
inputs=_inputs_for(HDMI, JACK), default_sink=JACK, state=HDMI)
first_new = next(i for i, c in enumerate(calls) if c.startswith("ardour:Master") and JACK in c)
first_cut = next(i for i, c in enumerate(calls) if c.startswith("-d") and HDMI in c)
assert first_new < first_cut
def test_master_already_on_the_default_is_left_alone(tmp_path):
calls, out = _reconcile(tmp_path, CTRL_LINKED + ORBIT_1_LINKED + _master_on(HDMI),
inputs=_inputs_for(HDMI), default_sink=HDMI, state=HDMI)
assert calls == [] and out == ""
def test_several_sinks_at_once_is_custom_wiring_and_left_alone(tmp_path):
"""qjackctl / Ardour's matrix: the owner's wiring wins."""
calls, out = _reconcile(tmp_path, CTRL_LINKED + ORBIT_1_LINKED + _master_on(HDMI, JACK),
inputs=_inputs_for(HDMI, JACK), default_sink=JACK, state=HDMI)
assert calls == []
assert "custom wiring" in out and "hand it back" in out
def test_a_sink_we_did_not_link_is_custom_wiring(tmp_path):
calls, out = _reconcile(tmp_path, CTRL_LINKED + ORBIT_1_LINKED + _master_on(JACK),
inputs=_inputs_for(HDMI, JACK), default_sink=HDMI, state=HDMI)
assert calls == [] and "rewired by hand" in out
assert _state(tmp_path) == HDMI # we do not overwrite what we did not do
def test_after_a_restart_a_lone_sink_that_is_the_default_is_adopted_silently(tmp_path):
calls, out = _reconcile(tmp_path, CTRL_LINKED + ORBIT_1_LINKED + _master_on(HDMI),
inputs=_inputs_for(HDMI), default_sink=HDMI, state=None)
assert calls == [] and out == "" and _state(tmp_path) == HDMI
def test_after_a_restart_a_lone_sink_that_is_not_the_default_is_custom(tmp_path):
calls, out = _reconcile(tmp_path, CTRL_LINKED + ORBIT_1_LINKED + _master_on(JACK),
inputs=_inputs_for(HDMI, JACK), default_sink=HDMI, state=None)
assert calls == [] and "custom wiring" in out and _state(tmp_path) is None
def test_disconnecting_everything_hands_control_back(tmp_path):
"""The documented way back from custom wiring: Master dangling -> default again."""
calls, out = _reconcile(tmp_path, CTRL_LINKED + ORBIT_1_LINKED + MASTER_DANGLING,
inputs=_inputs_for(HDMI), default_sink=HDMI, state=JACK)
assert f"ardour:Master/audio_out 1 {HDMI}:playback_FL" in calls
assert _state(tmp_path) == HDMI
def test_the_follow_check_runs_on_its_cadence_not_every_tick(tmp_path):
"""The default-sink lookup is a fork: do it every FOLLOW_EVERY ticks."""
stubs = tmp_path / "bin"
_reconcile(tmp_path, CTRL_LINKED + ORBIT_1_LINKED + _master_on(HDMI),
inputs=_inputs_for(HDMI), default_sink=HDMI, state=HDMI)
(stubs / "pactl").write_text(f"#!/bin/sh\necho x >> '{tmp_path}/pactl.calls'\necho '{HDMI}'\n")
env = dict(os.environ, PATH=f"{stubs}:{os.environ['PATH']}",
TIDAL_AUTOROUTE_STATE_DIR=str(tmp_path / "state"), TIDAL_AUTOROUTE_FOLLOW_EVERY="4")
subprocess.run(["bash", "-c", f"source '{SCRIPT}'\nfor i in 1 2 3 4 5 6 7 8; do reconcile; done"],
env=env, capture_output=True, text=True, timeout=30)
assert len((tmp_path / "pactl.calls").read_text().splitlines()) == 2
# --------------------------------------------------------------------------- #
# SuperDirt takes no microphone
# --------------------------------------------------------------------------- #
MIC = "alsa_input.pci-0000_00_1f.3-platform-sof_sdw.HiFi__hw_sofsoundwire_4__source"
MICS_LINKED = (f"{MIC}:capture_FL\n |-> SuperCollider:in_1\n"
f"{MIC}:capture_FR\n |-> SuperCollider:in_2\n")
BASE = CTRL_LINKED + ORBIT_1_LINKED + MASTER_ON_SPEAKER
def test_the_laptop_mics_are_cut_from_superdirt(tmp_path):
calls, out = _reconcile(tmp_path, BASE + MICS_LINKED)
assert f"-d {MIC}:capture_FL SuperCollider:in_1" in calls
assert f"-d {MIC}:capture_FR SuperCollider:in_2" in calls
assert "takes no mic" in out and "TIDAL_AUTOROUTE_SC_MICS=1" in out
def test_a_bluetooth_mic_is_cut_too(tmp_path):
bt = "bluez_input.70_26_05_D0_D5_90.0"
calls, _ = _reconcile(tmp_path, BASE + f"{bt}:capture_MONO\n |-> SuperCollider:in_1\n")
assert f"-d {bt}:capture_MONO SuperCollider:in_1" in calls
def test_the_mic_knob_leaves_the_links_alone(tmp_path):
"""The note a future owner needs: set TIDAL_AUTOROUTE_SC_MICS=1 to listen again."""
calls, out = _reconcile(tmp_path, BASE + MICS_LINKED, extra_env={"TIDAL_AUTOROUTE_SC_MICS": "1"})
assert not [c for c in calls if c.startswith("-d") and "SuperCollider:in_" in c]
assert "takes no mic" not in out
def test_a_sinks_monitor_port_is_not_a_microphone(tmp_path):
mon = f"{SINK}:monitor_FL\n |-> SuperCollider:in_1\n"
calls, _ = _reconcile(tmp_path, BASE + mon)
assert not [c for c in calls if c.startswith("-d") and "SuperCollider:in_" in c]
def test_no_ardour_tracks_means_the_mics_are_left_alone_too(tmp_path):
"""Not a performance: prune nothing (a linkless jack client spins at RT)."""
inputs = f"ardour:MIDI Control In\n{SINK}:playback_FL\n"
calls, _ = _reconcile(tmp_path, CTRL_LINKED + MICS_LINKED, inputs=inputs)
assert not [c for c in calls if c.startswith("-d")]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment