0% found this document useful (0 votes)
5 views6 pages

Fix Bluetooth Audio on Ubuntu 16.04

This Python script fixes issues with Bluetooth stereo headphones on Ubuntu 16.04 and Debian by switching the audio profile between A2DP and HSP. It uses the Bluetoothctl CLI to connect/disconnect devices, list devices, and set the audio profile. The script has options to select the target device, profile, wait time, and number of retries. It finds the device and sink IDs, sets the default sink and moves audio streams.

Uploaded by

trubbish
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views6 pages

Fix Bluetooth Audio on Ubuntu 16.04

This Python script fixes issues with Bluetooth stereo headphones on Ubuntu 16.04 and Debian by switching the audio profile between A2DP and HSP. It uses the Bluetoothctl CLI to connect/disconnect devices, list devices, and set the audio profile. The script has options to select the target device, profile, wait time, and number of retries. It finds the device and sink IDs, sets the default sink and moves audio streams.

Uploaded by

trubbish
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Datei: /home/dietrich/Schreibtisch/a2dp.

py Seite 1 von 6

#! /usr/bin/env python3.5
"""
Fixing bluetooth stereo headphone/headset problem in ubuntu 16.04 and also debian jessie, with bluez5.
Workaround for bug: [Link]
Run it with python3.5 or higher after pairing/connecting the bluetooth stereo headphone.
This will be only fixes the bluez5 problem mentioned above .
Licence: Freeware
See ``python3.5 [Link] -h``.
Shorthands:
$ alias speakers="[Link] 10:08:C1:44:AE:BC"
$ alias headphones="[Link] 00:22:37:3D:DA:50"
$ alias headset="[Link] 00:22:37:F8:A0:77 -p hsp"
$ speakers
Check here for the latest updates: [Link]
Thanks to:
* [Link] for adding the ``-p/--profile`` argument.
* [Link] for mentioning wait before connecting again.
* [Link] for v0.4.0
* [Link] for autodetect & autorun service
* [Link] for systemd service
Change Log
----------
- 0.5.2
* Increasing the number of tries to 15.
- 0.5.2
* Optimizing waits.
- 0.5.1
* Increasing WAIT_TIME and TRIES
- 0.5.0
* Autodetect & autorun service
- 0.4.1
* Sorting device list
- 0.4.0
* Adding ignore_fail argument by @AmploDev.
* Sending all available streams into selected sink, after successfull connection by @AmploDev.
- 0.3.3
* Updating default sink before turning to ``off`` profile.
- 0.3.2
* Waiting a bit: ``-w/--wait`` before connecting again.
- 0.3.0
* Adding -p / --profile option for using the same script to switch between headset and A2DP audio
profiles
- 0.2.5
* Mentioning [mac] argument.
- 0.2.4
* Removing duplicated devices in select device list.
- 0.2.3
* Matching ANSI escape characters. Tested on 16.10 & 16.04
- 0.2.2
* Some sort of code enhancements.
- 0.2.0
* Adding `-V/--version`, `-w/--wait` and `-t/--tries` CLI arguments.
- 0.1.1
* Supporting the `[NEW]` prefix for devices & controllers as advised by @wdullaer
* Drying the code.
or watering
"""

import sys
import re
import asyncio
import subprocess as sb
import argparse

__version__ = '0.5.2'

HEX_DIGIT_PATTERN = '[0-9A-F]'
HEX_BYTE_PATTERN = '%s{2}' % HEX_DIGIT_PATTERN
MAC_ADDRESS_PATTERN = ':'.join((HEX_BYTE_PATTERN, ) * 6)
DEVICE_PATTERN = [Link]('^(?:.*\s)?Device\s(?P<mac>%s)\s(?P<name>.*)' % MAC_ADDRESS_PATTERN)
Datei: /home/dietrich/Schreibtisch/[Link] Seite 2 von 6

CONTROLLER_PATTERN = [Link]('^(?:.*\s)?Controller\s(?P<mac>%s)\s(?P<name>.*)' %
MAC_ADDRESS_PATTERN)
WAIT_TIME = 2.25
TRIES = 15
PROFILE = 'a2dp'

_profiles = {
'a2dp': 'a2dp_sink',
'hsp': 'headset_head_unit',
'off': 'off'
}

# CLI Arguments
parser = [Link](prog=[Link][0])
parser.add_argument('-e', '--echo', action='store_true', default=False,
help='If given, the subprocess stdout will be also printed on stdout.')
parser.add_argument('-w', '--wait', default=WAIT_TIME, type=float,
help='The seconds to wait for subprocess output, default is: %s' % WAIT_TIME)
parser.add_argument('-t', '--tries', default=TRIES, type=int,
help='The number of tries if subprocess is failed. default is: %s' % TRIES)
parser.add_argument('-p', '--profile', default=PROFILE,
help='The profile to switch to. available options are: hsp, a2dp. default is: %s'
% PROFILE)
parser.add_argument('-V', '--version', action='store_true', help='Show the version.')
parser.add_argument('mac', nargs='?', default=None)

# Exceptions
class SubprocessError(Exception):
pass

class RetryExceededError(Exception):
pass

class BluetoothctlProtocol([Link]):
def __init__(self, exit_future, echo=True):
self.exit_future = exit_future
[Link] = None
[Link] = None
[Link] = echo

def listen_output(self):
[Link] = ''

def not_listen_output(self):
[Link] = None

def pipe_data_received(self, fd, raw):


d = [Link]()
if [Link]:
print(d, end='')

if [Link] is not None:


[Link] += d

def process_exited(self):
self.exit_future.set_result(True)

def connection_made(self, transport):


[Link] = transport
print('Connection MADE')

async def send_command(self, c):


stdin_transport = [Link].get_pipe_transport(0)
# noinspection PyProtectedMember
stdin_transport._pipe.write(('%s\n' % c).encode())

async def search_in_output(self, expression, fail_expression=None):


if [Link] is None:
Datei: /home/dietrich/Schreibtisch/[Link] Seite 3 von 6

return None

for l in [Link]():
if fail_expression and [Link](fail_expression, l, [Link]):
raise SubprocessError('Expression "%s" failed with fail pattern: "%s"' % (l,
fail_expression))

if [Link](expression, l, [Link]):
return True

async def send_and_wait(self, cmd, wait_expression, fail_expression='fail'):


try:
self.listen_output()
await self.send_command(cmd)
while not await self.search_in_output(wait_expression.lower(),
fail_expression=fail_expression):
await wait()
finally:
self.not_listen_output()

async def disconnect(self, mac):


print('Disconnecting the device.')
await self.send_and_wait('disconnect %s' % ':'.join(mac), 'Successful disconnected')

async def connect(self, mac):


print('Connecting again.')
await self.send_and_wait('connect %s' % ':'.join(mac), 'Connection successful')

async def trust(self, mac):


await self.send_and_wait('trust %s' % ':'.join(mac), 'trust succeeded')

async def quit(self):


await self.send_command('quit')

async def get_list(self, command, pattern):


result = set()
try:
self.listen_output()
await self.send_command(command)
await wait()
for l in [Link]():
m = [Link](l)
if m:
[Link]([Link]())
return sorted(list(result), key=lambda i: i[1])
finally:
self.not_listen_output()

async def list_devices(self):


return await self.get_list('devices', DEVICE_PATTERN)

async def list_paired_devices(self):


return await self.get_list('paired-devices', DEVICE_PATTERN)

async def list_controllers(self):


return await self.get_list('list', CONTROLLER_PATTERN)

async def select_paired_device(self):


print('Selecting device:')
devices = await self.list_paired_devices()
count = len(devices)

if count < 1:
raise SubprocessError('There is no connected device.')
elif count == 1:
return devices[0]

for i, d in enumerate(devices):
print('%d. %s %s' % (i+1, d[0], d[1]))
print('Select device[1]:')
selected = input()
return devices[0 if not [Link]() else (int(selected) - 1)]
Datei: /home/dietrich/Schreibtisch/[Link] Seite 4 von 6

async def wait(delay=None):


return await [Link](WAIT_TIME or delay)

async def execute_command(cmd, ignore_fail=False):


p = await asyncio.create_subprocess_shell(cmd, stdout=[Link], stderr=[Link])
stdout, stderr = await [Link]()
stdout, stderr = \
[Link]() if stdout is not None else '', \
[Link]() if stderr is not None else ''
if [Link] != 0 or [Link]() != '':
message = 'Command: %s failed with status: %s\nstderr: %s' % (cmd, [Link], stderr)
if ignore_fail:
print('Ignoring: %s' % message)
else:
raise SubprocessError(message)
return stdout

async def execute_find(cmd, pattern, tries=0, fail_safe=False):


tries = tries or TRIES

message = 'Cannot find `%s` using `%s`.' % (pattern, cmd)


retry_message = message + ' Retrying %d more times'
while True:
stdout = await execute_command(cmd)
match = [Link](pattern, stdout)

if match:
return [Link]()
elif tries > 0:
await wait()
print(retry_message % tries)
tries -= 1
continue

if fail_safe:
return None

raise RetryExceededError('Retry times exceeded: %s' % message)

async def find_dev_id(mac, **kw):


return await execute_find('pactl list cards short', 'bluez_card.%s' % '_'.join(mac), **kw)

async def find_sink(mac, **kw):


return await execute_find('pacmd list-sinks', 'bluez_sink.%s' % '_'.join(mac), **kw)

async def set_profile(device_id, profile):


print('Setting the %s profile' % profile)
try:
return await execute_command('pactl set-card-profile %s %s' % (device_id, _profiles[profile]))
except KeyError:
print('Invalid profile: %s, please select one one of a2dp or hsp.' % profile, file=[Link])
raise SystemExit(1)

async def set_default_sink(sink):


print('Updating default sink to %s' % sink)
return await execute_command('pacmd set-default-sink %s' % sink)

async def move_streams_to_sink(sink):


streams = await execute_command('pacmd list-sink-inputs | grep "index:"', True)
for i in [Link]():
i = ''.join(n for n in i if [Link]())
if i != '':
print('Moving stream %s to sink' % i)
Datei: /home/dietrich/Schreibtisch/[Link] Seite 5 von 6

await execute_command('pacmd move-sink-input %s %s' % (i, sink))


return sink

async def main(args):


global WAIT_TIME, TRIES

if [Link]:
print(__version__)
return 0

mac = [Link]

# Hacking, Changing the constants!


WAIT_TIME = [Link]
TRIES = [Link]

exit_future = [Link]()
transport, protocol = await asyncio.get_event_loop().subprocess_exec(
lambda: BluetoothctlProtocol(exit_future, echo=[Link]), 'bluetoothctl'
)

try:

if mac is None:
mac, _ = await protocol.select_paired_device()

mac = [Link](':' if ':' in mac else '_')


print('Device MAC: %s' % ':'.join(mac))

device_id = await find_dev_id(mac, fail_safe=True)


if device_id is None:
print('It seems device: %s is not connected yet, trying to connect.' % ':'.join(mac))
await [Link](mac)
await [Link](mac)
device_id = await find_dev_id(mac)

sink = await find_sink(mac, fail_safe=True)


if sink is None:
await set_profile(device_id, [Link])
sink = await find_sink(mac)

print('Device ID: %s' % device_id)


print('Sink: %s' % sink)

await set_default_sink(sink)
await wait()

await set_profile(device_id, 'off')

if [Link] is 'a2dp':
await [Link](mac)
await wait()
await [Link](mac)

device_id = await find_dev_id(mac)


print('Device ID: %s' % device_id)

await wait(2)
await set_profile(device_id, [Link])
await set_default_sink(sink)
await move_streams_to_sink(sink)

except (SubprocessError, RetryExceededError) as ex:


print(str(ex), file=[Link])
return 1
finally:
print('Exiting bluetoothctl')
await [Link]()
await exit_future

# Close the stdout pipe


Datei: /home/dietrich/Schreibtisch/[Link] Seite 6 von 6

[Link]()

if [Link] == 'a2dp':
print('"Enjoy" the HiFi stereo music :)')
else:
print('"Enjoy" your headset audio :)')

if __name__ == '__main__':
[Link](asyncio.get_event_loop().run_until_complete(main(parser.parse_args())))

You might also like