mirror of
https://github.com/google/bumble.git
synced 2026-04-18 00:45:32 +00:00
wip
This commit is contained in:
168
apps/auracast.py
168
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")
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user