mirror of
https://github.com/google/bumble.git
synced 2026-07-10 13:30:50 +00:00
wip
This commit is contained in:
+4
-164
@@ -25,9 +25,7 @@ import dataclasses
|
|||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
|
||||||
import struct
|
import struct
|
||||||
import sys
|
|
||||||
from typing import (
|
from typing import (
|
||||||
cast,
|
cast,
|
||||||
Any,
|
Any,
|
||||||
@@ -80,164 +78,6 @@ AURACAST_DEFAULT_SAMPLE_RATE = 48000
|
|||||||
AURACAST_DEFAULT_TRANSMIT_BITRATE = 80000
|
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
|
# Utils
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -833,7 +673,7 @@ async def run_receive(
|
|||||||
) -> None:
|
) -> None:
|
||||||
# Run a pre-flight check for the output.
|
# Run a pre-flight check for the output.
|
||||||
try:
|
try:
|
||||||
if not check_audio_output(output):
|
if not audio_io.check_audio_output(output):
|
||||||
return
|
return
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
print(error)
|
print(error)
|
||||||
@@ -907,7 +747,7 @@ async def run_receive(
|
|||||||
packet_stats = [0, 0]
|
packet_stats = [0, 0]
|
||||||
|
|
||||||
async with contextlib.aclosing(
|
async with contextlib.aclosing(
|
||||||
await create_audio_output(output)
|
await audio_io.create_audio_output(output)
|
||||||
) as audio_output:
|
) as audio_output:
|
||||||
await audio_output.open(
|
await audio_output.open(
|
||||||
audio_io.PcmFormat(
|
audio_io.PcmFormat(
|
||||||
@@ -978,7 +818,7 @@ async def run_transmit(
|
|||||||
) -> None:
|
) -> None:
|
||||||
# Run a pre-flight check for the input.
|
# Run a pre-flight check for the input.
|
||||||
try:
|
try:
|
||||||
if not check_audio_input(input):
|
if not audio_io.check_audio_input(input):
|
||||||
return
|
return
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
print(error)
|
print(error)
|
||||||
@@ -1073,7 +913,7 @@ async def run_transmit(
|
|||||||
print('Start Periodic Advertising')
|
print('Start Periodic Advertising')
|
||||||
await advertising_set.start_periodic()
|
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()
|
pcm_format = await audio_input.open()
|
||||||
if pcm_format.channels != 2:
|
if pcm_format.channels != 2:
|
||||||
print("Only 2 channels PCM configurations are supported")
|
print("Only 2 channels PCM configurations are supported")
|
||||||
|
|||||||
+11
-5
@@ -23,15 +23,19 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
|
import pathlib
|
||||||
from typing import (
|
from typing import (
|
||||||
AsyncGenerator,
|
AsyncGenerator,
|
||||||
BinaryIO,
|
BinaryIO,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
import sys
|
||||||
import wave
|
import wave
|
||||||
|
|
||||||
|
from bumble.colors import color
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
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:'):
|
if output == 'device' or output.startswith('device:'):
|
||||||
try:
|
try:
|
||||||
import sounddevice
|
import sounddevice
|
||||||
except ImportError as exc:
|
except (ImportError, OSError) as exc:
|
||||||
raise ValueError(
|
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
|
) from exc
|
||||||
|
|
||||||
if output == 'device':
|
if output == 'device':
|
||||||
@@ -289,9 +294,10 @@ def check_audio_input(input: str) -> bool:
|
|||||||
if input == 'device' or input.startswith('device:'):
|
if input == 'device' or input.startswith('device:'):
|
||||||
try:
|
try:
|
||||||
import sounddevice
|
import sounddevice
|
||||||
except ImportError as exc:
|
except (ImportError, OSError) as exc:
|
||||||
raise ValueError(
|
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
|
) from exc
|
||||||
|
|
||||||
if input == 'device':
|
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_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC = UUID.from_16_bits(0x2B29, 'Client Supported Features')
|
||||||
GATT_DATABASE_HASH_CHARACTERISTIC = UUID.from_16_bits(0x2B2A, 'Database Hash')
|
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_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
|
# fmt: on
|
||||||
# pylint: enable=line-too-long
|
# pylint: enable=line-too-long
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import struct
|
|||||||
import sys
|
import sys
|
||||||
from typing import Any, List, Union
|
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 transport
|
||||||
from bumble import gatt
|
from bumble import gatt
|
||||||
from bumble import hci
|
from bumble import hci
|
||||||
@@ -82,19 +82,19 @@ async def client(device: Device, address: hci.Address) -> None:
|
|||||||
for index in range(1, 9):
|
for index in range(1, 9):
|
||||||
characteristics.append(
|
characteristics.append(
|
||||||
service.get_characteristics_by_uuid(
|
service.get_characteristics_by_uuid(
|
||||||
CHARACTERISTIC_UUID_BASE + f"{index:02X}"
|
core.UUID(CHARACTERISTIC_UUID_BASE + f"{index:02X}")
|
||||||
)[0]
|
)[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Read all characteristics as raw bytes.
|
# Read all characteristics as raw bytes.
|
||||||
for characteristic in characteristics:
|
for characteristic in characteristics:
|
||||||
value = await characteristic.read_value()
|
value = await characteristic.read_value()
|
||||||
print(f"### {characteristic} = {value} ({value.hex()})")
|
print(f"### {characteristic} = {value!r} ({value.hex()})")
|
||||||
|
|
||||||
# Static characteristic with a bytes value.
|
# Static characteristic with a bytes value.
|
||||||
c1 = characteristics[0]
|
c1 = characteristics[0]
|
||||||
c1_value = await c1.read_value()
|
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"))
|
await c1.write_value("happy π day".encode("utf-8"))
|
||||||
|
|
||||||
# Static characteristic with a string value.
|
# 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.
|
# Dynamic characteristic with a bytes value.
|
||||||
c7 = characteristics[6]
|
c7 = characteristics[6]
|
||||||
c7_value = await c7.read_value()
|
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"))
|
await c7.write_value(bytes.fromhex("01020304"))
|
||||||
|
|
||||||
# Dynamic characteristic with a string value.
|
# Dynamic characteristic with a string value.
|
||||||
|
|||||||
@@ -174,16 +174,10 @@ async def main() -> None:
|
|||||||
ws = websocket
|
ws = websocket
|
||||||
async for message in websocket:
|
async for message in websocket:
|
||||||
volume_state = json.loads(message)
|
volume_state = json.loads(message)
|
||||||
vcs.volume_state_bytes = bytes(
|
vcs.volume_setting = volume_state['volume_setting']
|
||||||
[
|
vcs.muted = volume_state['muted']
|
||||||
volume_state['volume_setting'],
|
vcs.change_counter = volume_state['change_counter']
|
||||||
volume_state['muted'],
|
await device.notify_subscribers(vcs.volume_state)
|
||||||
volume_state['change_counter'],
|
|
||||||
]
|
|
||||||
)
|
|
||||||
await device.notify_subscribers(
|
|
||||||
vcs.volume_state, vcs.volume_state_bytes
|
|
||||||
)
|
|
||||||
ws = None
|
ws = None
|
||||||
|
|
||||||
await websockets.serve(serve, 'localhost', 8989)
|
await websockets.serve(serve, 'localhost', 8989)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def test_import():
|
|||||||
le_audio,
|
le_audio,
|
||||||
pacs,
|
pacs,
|
||||||
pbp,
|
pbp,
|
||||||
vcp,
|
vcs,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert att
|
assert att
|
||||||
@@ -87,7 +87,7 @@ def test_import():
|
|||||||
assert le_audio
|
assert le_audio
|
||||||
assert pacs
|
assert pacs
|
||||||
assert pbp
|
assert pbp
|
||||||
assert vcp
|
assert vcs
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user