Add web mixer with native input volume and output selection
This commit is contained in:
+46
-16
@@ -66,7 +66,7 @@ def save(path, text, mode=0o644):
|
||||
|
||||
|
||||
def mixer(pcm, rate):
|
||||
return f'''# Managed by radxa-audio; each input has 6 dB of headroom.
|
||||
return f'''# Managed by radxa-audio; each input is attenuated by 3 dB.
|
||||
pcm.radxa_dmix {{
|
||||
type dmix
|
||||
ipc_key 553001
|
||||
@@ -87,7 +87,7 @@ pcm.radxa_dmix {{
|
||||
pcm.radxa_mix {{
|
||||
type plug
|
||||
slave.pcm "radxa_dmix"
|
||||
ttable {{ 0.0 0.5 1.1 0.5 }}
|
||||
ttable {{ 0.0 0.707945784 1.1 0.707945784 }}
|
||||
}}
|
||||
'''
|
||||
|
||||
@@ -115,7 +115,7 @@ diagnostics = {{ statistics = "yes"; log_verbosity = 1; }};
|
||||
''',
|
||||
'/etc/systemd/system/bluealsa.service.d/10-radxa-sink.conf': '''[Service]
|
||||
ExecStart=
|
||||
ExecStart=/usr/bin/bluealsa -S -p a2dp-sink -p a2dp-source --initial-volume=50
|
||||
ExecStart=/usr/bin/bluealsa -S -p a2dp-sink -p a2dp-source --initial-volume=50 --sbc-quality=medium
|
||||
''',
|
||||
'/etc/systemd/system/bluealsa-aplay.service.d/10-radxa-mixer.conf': '''[Unit]
|
||||
Requires=bluealsa.service
|
||||
@@ -143,7 +143,7 @@ StartLimitIntervalSec=0
|
||||
User=shairport-sync
|
||||
Group=audio
|
||||
ExecStartPre=-/usr/bin/bluetoothctl --timeout 8 connect {addr} a2dp-sink
|
||||
ExecStart=/usr/bin/alsaloop -C hw:Loopback,1,0 -P bluealsa:DEV={addr},PROFILE=a2dp -c 2 -r 48000 -f S16_LE -t 200000 -S 4 -A 2
|
||||
ExecStart=/usr/bin/alsaloop -C hw:Loopback,1,0 -P bluealsa:DEV={addr},PROFILE=a2dp -c 2 -r 48000 -f S16_LE -t 500000 -S 4 -A 2
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStartSec=30
|
||||
@@ -199,12 +199,16 @@ def check_dac(pcm):
|
||||
raise ValueError("DAC 출력 장치가 없습니다. aplay -l에서 카드 ID를 확인해 주세요.")
|
||||
|
||||
|
||||
def apply(c, initial=False):
|
||||
def apply(c, initial=False, reconfigure=False):
|
||||
root()
|
||||
files = templates(c)
|
||||
if c["output"] == "dac":
|
||||
if reconfigure:
|
||||
previous = load()
|
||||
if any(c.get(k) != previous.get(k) for k in ('output', 'dac_pcm', 'bluetooth_device')):
|
||||
raise ValueError('현재 출력 경로를 바꾸는 작업은 연결 확인이 필요합니다.')
|
||||
if c["output"] == "dac" and not reconfigure:
|
||||
check_dac(c["dac_pcm"])
|
||||
if c['output'] == 'bluetooth':
|
||||
if c['output'] == 'bluetooth' and not reconfigure:
|
||||
# Preflight before changing a working DAC route.
|
||||
run('modprobe', 'snd-aloop')
|
||||
result = run('bluetoothctl', '--timeout', '10', 'connect',
|
||||
@@ -229,6 +233,8 @@ def apply(c, initial=False):
|
||||
run('systemctl', 'enable', 'avahi-daemon', 'bluetooth', 'nqptp',
|
||||
'shairport-sync', 'bluealsa', 'bluealsa-aplay')
|
||||
run('systemctl', 'restart', 'bluetooth')
|
||||
if run('systemctl', 'cat', 'aic-bluetooth.service', check=False, capture=True).returncode == 0:
|
||||
run('systemctl', 'restart', 'aic-bluetooth')
|
||||
run('systemctl', 'start', 'avahi-daemon', 'nqptp')
|
||||
run('systemctl', 'restart', 'bluealsa')
|
||||
if c['output'] == 'bluetooth':
|
||||
@@ -238,12 +244,12 @@ def apply(c, initial=False):
|
||||
run('systemctl', 'disable', BT_SERVICE, check=False, capture=True)
|
||||
run('systemctl', 'start', 'shairport-sync', 'bluealsa-aplay')
|
||||
required = ['shairport-sync', 'bluealsa-aplay']
|
||||
if c['output'] == 'bluetooth':
|
||||
if c['output'] == 'bluetooth' and not reconfigure:
|
||||
required.append(BT_SERVICE)
|
||||
if not all(active(u) for u in required):
|
||||
raise ValueError('서비스 시작에 실패했습니다.')
|
||||
alias = c['name'] + ' Bluetooth'
|
||||
run('bluetoothctl', 'system-alias', alias)
|
||||
run('bluetoothctl', 'system-alias', alias, check=not reconfigure, capture=reconfigure)
|
||||
print('출력:', c['output'], c.get('bluetooth_device', '') if c['output'] == 'bluetooth' else c['dac_pcm'])
|
||||
except Exception:
|
||||
run('systemctl', 'stop', *units, check=False)
|
||||
@@ -257,12 +263,12 @@ def apply(c, initial=False):
|
||||
raise
|
||||
|
||||
|
||||
def pair(addr, output=False):
|
||||
def pair(addr, output=False, bond=False):
|
||||
root()
|
||||
if not MAC_RE.fullmatch(addr):
|
||||
raise ValueError('장비 주소 형식이 잘못됐습니다.')
|
||||
info = run('bluetoothctl', 'info', addr, check=False, capture=True).stdout
|
||||
if 'Paired: yes' not in info:
|
||||
if 'Paired: yes' not in info or (bond and 'Bonded: yes' not in info):
|
||||
# Outgoing pairing agent lives only for this operation.
|
||||
controller = run('bluetoothctl', 'show', capture=True).stdout
|
||||
was_pairable = 'Pairable: yes' in controller
|
||||
@@ -275,6 +281,8 @@ def pair(addr, output=False):
|
||||
info = run('bluetoothctl', 'info', addr, capture=True).stdout
|
||||
if 'Paired: yes' not in info:
|
||||
raise ValueError('페어링되지 않았습니다. 장비를 페어링 모드로 켜 주세요.')
|
||||
if bond and 'Bonded: yes' not in info:
|
||||
raise ValueError('영구 등록되지 않았습니다. 장비를 페어링 모드로 다시 켠 뒤 재시도해 주세요.')
|
||||
run('bluetoothctl', 'trust', addr)
|
||||
run('bluetoothctl', '--timeout', '10', 'connect', addr,
|
||||
'a2dp-sink' if output else 'a2dp-source', check=False)
|
||||
@@ -292,16 +300,27 @@ def main():
|
||||
sub.add_parser('status')
|
||||
sub.add_parser('devices')
|
||||
q = sub.add_parser('scan'); q.add_argument('--seconds', type=int, default=30)
|
||||
q = sub.add_parser('pair'); q.add_argument('address'); q.add_argument('--output', action='store_true')
|
||||
q = sub.add_parser('pair'); q.add_argument('address'); q.add_argument('--output', action='store_true'); q.add_argument('--bond', action='store_true')
|
||||
q = sub.add_parser('pair-input'); q.add_argument('--seconds', type=int, default=120)
|
||||
q = sub.add_parser('output'); q.add_argument('mode', choices=['dac', 'bluetooth', 'bose']); q.add_argument('address', nargs='?')
|
||||
q = sub.add_parser('output'); q.add_argument('mode', choices=['dac', 'bluetooth', 'bose']); q.add_argument('address', nargs='?'); q.add_argument('--pcm')
|
||||
q = sub.add_parser('name'); q.add_argument('name')
|
||||
q = sub.add_parser('dac'); q.add_argument('pcm')
|
||||
sub.add_parser('apply')
|
||||
q = sub.add_parser('init'); q.add_argument('--name', required=True); q.add_argument('--dac', default='hw:CARD=Audio,DEV=0')
|
||||
q.add_argument('--adopt', action='store_true'); q.add_argument('--address', default='')
|
||||
q.add_argument('--output', choices=['dac', 'bluetooth'], default='dac')
|
||||
sub.add_parser('inputs')
|
||||
q = sub.add_parser('volume'); q.add_argument('input'); q.add_argument('percent', type=int)
|
||||
q = sub.add_parser('mute'); q.add_argument('input'); q.add_argument('state', choices=['on', 'off'])
|
||||
a = p.parse_args()
|
||||
if a.cmd in ('inputs', 'volume', 'mute'):
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
sys.path.insert(0, '/usr/local/lib/radxa-mixer')
|
||||
import input_controls
|
||||
if a.cmd == 'inputs': print(json.dumps(input_controls.inputs(), ensure_ascii=False, indent=2))
|
||||
elif a.cmd == 'volume': root(); input_controls.set_input(a.input, volume=a.percent)
|
||||
else: root(); input_controls.set_input(a.input, mute=a.state == 'on')
|
||||
return
|
||||
if a.cmd == 'devices':
|
||||
run('bluetoothctl', 'devices'); run('aplay', '-l'); run('bluealsa-aplay', '-l')
|
||||
elif a.cmd == 'status':
|
||||
@@ -318,7 +337,7 @@ def main():
|
||||
run('bluetoothctl', 'scan', 'off', check=False)
|
||||
run('bluetoothctl', 'devices')
|
||||
elif a.cmd == 'pair':
|
||||
pair(a.address, a.output)
|
||||
pair(a.address, a.output, a.bond)
|
||||
elif a.cmd == 'pair-input':
|
||||
root()
|
||||
if not 15 <= a.seconds <= 900: raise ValueError('페어링 시간: 15~900초')
|
||||
@@ -338,12 +357,15 @@ def main():
|
||||
c = load()
|
||||
if a.cmd == 'output':
|
||||
c['output'] = 'bluetooth' if a.mode in ('bluetooth', 'bose') else 'dac'
|
||||
if a.pcm:
|
||||
if c['output'] != 'dac': raise ValueError('--pcm은 DAC 출력에서만 지정합니다.')
|
||||
c['dac_pcm'] = a.pcm
|
||||
if a.address:
|
||||
if c['output'] != 'bluetooth': raise ValueError('장비 주소는 bluetooth 출력에서만 지정합니다.')
|
||||
c['bluetooth_device'] = a.address.upper()
|
||||
elif a.cmd == 'name': c['name'] = a.name
|
||||
elif a.cmd == 'dac': c['dac_pcm'] = a.pcm
|
||||
apply(c)
|
||||
apply(c, reconfigure=a.cmd == 'apply')
|
||||
|
||||
|
||||
def interactive_pair(seconds):
|
||||
@@ -381,7 +403,15 @@ def interactive_pair(seconds):
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
import fcntl
|
||||
if len(sys.argv) > 1 and sys.argv[1] not in ('status', 'devices', 'inputs', '-h', '--help'):
|
||||
root()
|
||||
Path('/run/lock').mkdir(parents=True, exist_ok=True)
|
||||
with open('/run/lock/radxa-audio.lock', 'w') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
main()
|
||||
else:
|
||||
main()
|
||||
except (ValueError, subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError) as e:
|
||||
print('오류:', e, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user