diff --git a/apps/auracast.py b/apps/auracast.py index 9ba84698..37e19b6d 100644 --- a/apps/auracast.py +++ b/apps/auracast.py @@ -25,9 +25,7 @@ import dataclasses import functools import logging import os -import pathlib import struct -import sys from typing import ( cast, Any, @@ -80,164 +78,6 @@ AURACAST_DEFAULT_SAMPLE_RATE = 48000 AURACAST_DEFAULT_TRANSMIT_BITRATE = 80000 -# ----------------------------------------------------------------------------- -# Audio I/O Support -# ----------------------------------------------------------------------------- -def check_audio_output(output: str) -> bool: - if output == 'device' or output.startswith('device:'): - try: - import sounddevice # type: ignore[import-untyped] - except ImportError as exc: - raise ValueError( - 'audio output not available (sounddevice python module not installed)' - ) from exc - - if output == 'device': - # Default device - return True - - # Specific device - device = output[7:] - if device == '?': - print(color('Audio Devices:', 'yellow')) - for device_info in [ - device_info - for device_info in sounddevice.query_devices() - if device_info['max_output_channels'] > 0 - ]: - device_index = device_info['index'] - is_default = ( - color(' [default]', 'green') - if sounddevice.default.device[1] == device_index - else '' - ) - print( - f'{color(device_index, "cyan")}: {device_info["name"]}{is_default}' - ) - return False - - try: - device_info = sounddevice.query_devices(int(device)) - except sounddevice.PortAudioError as exc: - raise ValueError('No such audio device') from exc - - if device_info['max_output_channels'] < 1: - raise ValueError( - f'Device {device} ({device_info["name"]}) does not have an output' - ) - - return True - - -async def create_audio_output(output: str) -> audio_io.AudioOutput: - if output == 'stdout': - return audio_io.StreamAudioOutput(sys.stdout.buffer) - - if output == 'device' or output.startswith('device:'): - device_name = '' if output == 'device' else output[7:] - return audio_io.SoundDeviceAudioOutput(device_name) - - if output == 'ffplay': - return audio_io.SubprocessAudioOutput( - command=( - 'ffplay -probesize 32 -fflags nobuffer -analyzeduration 0 ' - '-ar {sample_rate} ' - '-ch_layout {channel_layout} ' - '-f f32le pipe:0' - ) - ) - - if output.startswith('file:'): - return audio_io.FileAudioOutput(output[5:]) - - raise ValueError('unsupported audio output') - - -def check_audio_input(input: str) -> bool: - if input == 'device' or input.startswith('device:'): - try: - import sounddevice - except ImportError as exc: - raise ValueError( - 'audio input not available (sounddevice python module not installed)' - ) from exc - - if input == 'device': - # Default device - return True - - # Specific device - device = input[7:] - if device == '?': - print(color('Audio Devices:', 'yellow')) - for device_info in [ - device_info - for device_info in sounddevice.query_devices() - if device_info['max_input_channels'] > 0 - ]: - device_index = device_info["index"] - is_mono = device_info['max_input_channels'] == 1 - max_channels = color(f'[{"mono" if is_mono else "stereo"}]', 'cyan') - is_default = ( - color(' [default]', 'green') - if sounddevice.default.device[0] == device_index - else '' - ) - print( - f'{color(device_index, "cyan")}: {device_info["name"]}' - f' {max_channels}{is_default}' - ) - return False - - try: - device_info = sounddevice.query_devices(int(device)) - except sounddevice.PortAudioError as exc: - raise ValueError('No such audio device') from exc - - if device_info['max_input_channels'] < 1: - raise ValueError( - f'Device {device} ({device_info["name"]}) does not have an input' - ) - - return True - - -async def create_audio_input(input: str, input_format: str) -> audio_io.AudioInput: - pcm_format: audio_io.PcmFormat | None - if input_format == 'auto': - pcm_format = None - else: - pcm_format = audio_io.PcmFormat.from_str(input_format) - - if input == 'stdin': - if not pcm_format: - raise ValueError('input format details required for stdin') - return audio_io.StreamAudioInput(sys.stdin.buffer, pcm_format) - - if input == 'device' or input.startswith('device:'): - if not pcm_format: - raise ValueError('input format details required for device') - device_name = '' if input == 'device' else input[7:] - return audio_io.SoundDeviceAudioInput(device_name, pcm_format) - - # If there's no file: prefix, check if we can assume it is a file. - if pathlib.Path(input).is_file(): - input = 'file:' + input - - if input.startswith('file:'): - filename = input[5:] - if filename.endswith('.wav'): - if input_format != 'auto': - raise ValueError(".wav file only supported with 'auto' format") - return audio_io.WaveAudioInput(filename) - - if pcm_format is None: - raise ValueError('input format details required for raw PCM files') - return audio_io.FileAudioInput(filename, pcm_format) - - raise ValueError('input not supported') - - # ----------------------------------------------------------------------------- # Utils # ----------------------------------------------------------------------------- @@ -833,7 +673,7 @@ async def run_receive( ) -> None: # Run a pre-flight check for the output. try: - if not check_audio_output(output): + if not audio_io.check_audio_output(output): return except ValueError as error: print(error) @@ -907,7 +747,7 @@ async def run_receive( packet_stats = [0, 0] async with contextlib.aclosing( - await create_audio_output(output) + await audio_io.create_audio_output(output) ) as audio_output: await audio_output.open( audio_io.PcmFormat( @@ -978,7 +818,7 @@ async def run_transmit( ) -> None: # Run a pre-flight check for the input. try: - if not check_audio_input(input): + if not audio_io.check_audio_input(input): return except ValueError as error: print(error) @@ -1073,7 +913,7 @@ async def run_transmit( print('Start Periodic Advertising') await advertising_set.start_periodic() - audio_input = await create_audio_input(input, input_format) + audio_input = await audio_io.create_audio_input(input, input_format) pcm_format = await audio_input.open() if pcm_format.channels != 2: print("Only 2 channels PCM configurations are supported") diff --git a/bumble/audio/io.py b/bumble/audio/io.py index 06e44821..7bc2b40a 100644 --- a/bumble/audio/io.py +++ b/bumble/audio/io.py @@ -23,15 +23,19 @@ from concurrent.futures import ThreadPoolExecutor import dataclasses import enum import logging +import pathlib from typing import ( AsyncGenerator, BinaryIO, TYPE_CHECKING, ) +import sys import wave +from bumble.colors import color + if TYPE_CHECKING: - import sounddevice + import sounddevice # type: ignore[import-untyped] # ----------------------------------------------------------------------------- @@ -78,9 +82,10 @@ def check_audio_output(output: str) -> bool: if output == 'device' or output.startswith('device:'): try: import sounddevice - except ImportError as exc: + except (ImportError, OSError) as exc: raise ValueError( - 'audio output not available (sounddevice python module not installed)' + 'audio output not available ' + '(sounddevice python module not installed or failed to load)' ) from exc if output == 'device': @@ -289,9 +294,10 @@ def check_audio_input(input: str) -> bool: if input == 'device' or input.startswith('device:'): try: import sounddevice - except ImportError as exc: + except (ImportError, OSError) as exc: raise ValueError( - 'audio input not available (sounddevice python module not installed)' + 'audio input not available ' + '(sounddevice python module not installed or failed to load)' ) from exc if input == 'device': diff --git a/bumble/gatt.py b/bumble/gatt.py index 8192f87e..f237b805 100644 --- a/bumble/gatt.py +++ b/bumble/gatt.py @@ -315,6 +315,7 @@ GATT_CENTRAL_ADDRESS_RESOLUTION__CHARACTERISTIC = UUID.from_16_bi GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC = UUID.from_16_bits(0x2B29, 'Client Supported Features') GATT_DATABASE_HASH_CHARACTERISTIC = UUID.from_16_bits(0x2B2A, 'Database Hash') GATT_SERVER_SUPPORTED_FEATURES_CHARACTERISTIC = UUID.from_16_bits(0x2B3A, 'Server Supported Features') +GATT_LE_GATT_SECURITY_LEVELS_CHARACTERISTIC = UUID.from_16_bits(0x2BF5, 'E GATT Security Levels') # fmt: on # pylint: enable=line-too-long diff --git a/examples/run_gatt_with_adapters.py b/examples/run_gatt_with_adapters.py index f5430b8e..97fb8917 100644 --- a/examples/run_gatt_with_adapters.py +++ b/examples/run_gatt_with_adapters.py @@ -25,7 +25,7 @@ import struct import sys from typing import Any, List, Union -from bumble.device import Connection, Device, Peer +from bumble.device import Device, Peer from bumble import transport from bumble import gatt from bumble import hci @@ -82,19 +82,19 @@ async def client(device: Device, address: hci.Address) -> None: for index in range(1, 9): characteristics.append( service.get_characteristics_by_uuid( - CHARACTERISTIC_UUID_BASE + f"{index:02X}" + core.UUID(CHARACTERISTIC_UUID_BASE + f"{index:02X}") )[0] ) # Read all characteristics as raw bytes. for characteristic in characteristics: value = await characteristic.read_value() - print(f"### {characteristic} = {value} ({value.hex()})") + print(f"### {characteristic} = {value!r} ({value.hex()})") # Static characteristic with a bytes value. c1 = characteristics[0] c1_value = await c1.read_value() - print(f"@@@ C1 {c1} value = {c1_value} (type={type(c1_value)})") + print(f"@@@ C1 {c1} value = {c1_value!r} (type={type(c1_value)})") await c1.write_value("happy π day".encode("utf-8")) # Static characteristic with a string value. @@ -136,7 +136,7 @@ async def client(device: Device, address: hci.Address) -> None: # Dynamic characteristic with a bytes value. c7 = characteristics[6] c7_value = await c7.read_value() - print(f"@@@ C7 {c7} value = {c7_value} (type={type(c7_value)})") + print(f"@@@ C7 {c7} value = {c7_value!r} (type={type(c7_value)})") await c7.write_value(bytes.fromhex("01020304")) # Dynamic characteristic with a string value. diff --git a/examples/run_vcp_renderer.py b/examples/run_vcp_renderer.py index 1002bc69..116d4d66 100644 --- a/examples/run_vcp_renderer.py +++ b/examples/run_vcp_renderer.py @@ -174,16 +174,10 @@ async def main() -> None: ws = websocket async for message in websocket: volume_state = json.loads(message) - vcs.volume_state_bytes = bytes( - [ - volume_state['volume_setting'], - volume_state['muted'], - volume_state['change_counter'], - ] - ) - await device.notify_subscribers( - vcs.volume_state, vcs.volume_state_bytes - ) + vcs.volume_setting = volume_state['volume_setting'] + vcs.muted = volume_state['muted'] + vcs.change_counter = volume_state['change_counter'] + await device.notify_subscribers(vcs.volume_state) ws = None await websockets.serve(serve, 'localhost', 8989) diff --git a/tests/import_test.py b/tests/import_test.py index 95425112..b868c0da 100644 --- a/tests/import_test.py +++ b/tests/import_test.py @@ -53,7 +53,7 @@ def test_import(): le_audio, pacs, pbp, - vcp, + vcs, ) assert att @@ -87,7 +87,7 @@ def test_import(): assert le_audio assert pacs assert pbp - assert vcp + assert vcs # -----------------------------------------------------------------------------