refactor package structure and make improvements regarding design
This commit is contained in:
0
apps/__init__.py
Normal file
0
apps/__init__.py
Normal file
@@ -33,8 +33,7 @@ import numpy as np
|
||||
import pyee
|
||||
|
||||
import sys
|
||||
sys.path.append('../utils')
|
||||
from utils.le_audio_encoder import LeAudioEncoder
|
||||
from leaudio import LeAudioEncoder
|
||||
|
||||
from bumble.colors import color
|
||||
from bumble import company_ids
|
||||
@@ -49,6 +48,8 @@ import bumble.device
|
||||
import bumble.transport
|
||||
import bumble.utils
|
||||
|
||||
from leaudio import read_wav_file, generate_sine_data
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
@@ -66,30 +67,6 @@ AURACAST_DEFAULT_ATT_MTU = 256
|
||||
iso_index:int = 0
|
||||
|
||||
|
||||
def generate_sine_wave_iso_frames(frequency, sampling_rate, duration):
|
||||
num_samples = int(sampling_rate * duration)
|
||||
|
||||
t = np.linspace(0, duration, num_samples, False)
|
||||
|
||||
sine_wave = np.sin(2 * np.pi * frequency * t)
|
||||
|
||||
# Scale the sine wave to the 16-bit range (-32768 to 32767)
|
||||
scaled_sine_wave = sine_wave * 8191.5
|
||||
|
||||
# Convert to 16-bit integer format
|
||||
int16_sine_wave = scaled_sine_wave.astype(np.int16)
|
||||
|
||||
iso_frame = bytearray()
|
||||
|
||||
for num in int16_sine_wave:
|
||||
|
||||
iso_frame.append(num & 0xFF) # Extract lower 8 bits
|
||||
|
||||
iso_frame.append((num >> 8) & 0xFF) # Extract upper 8 bit
|
||||
|
||||
return iso_frame
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Scan For Broadcasts
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -747,29 +724,6 @@ async def run_receive(
|
||||
await terminated.wait()
|
||||
|
||||
|
||||
def read_wav_file(filename):
|
||||
rate, data = wav.read(filename)
|
||||
num_channels = data.ndim
|
||||
|
||||
if num_channels == 1:
|
||||
left_channel = data[:]
|
||||
else:
|
||||
left_channel = data[:, 1]
|
||||
|
||||
print(len(left_channel))
|
||||
upsampled_data = signal.resample(left_channel, int(
|
||||
48000 / 41000 * left_channel.shape[0]))
|
||||
|
||||
# wav.write("upsampled_stereo_file.wav", app_specific_codec.sampling_frequency.hz, upsampled_data.astype(data.dtype))
|
||||
print("Sample rate:", rate)
|
||||
print("Number channels:", num_channels)
|
||||
print("Audio data (left):", left_channel)
|
||||
print("Bitdepth:", data.dtype.itemsize * 8)
|
||||
|
||||
return upsampled_data.astype(np.int16)
|
||||
|
||||
|
||||
|
||||
async def run_broadcast(
|
||||
transport: str, broadcast_id: int, broadcast_code: str | None, wav_file_path: str
|
||||
) -> None:
|
||||
@@ -781,25 +735,18 @@ async def run_broadcast(
|
||||
print(color('Periodic advertising not supported', 'red'))
|
||||
return
|
||||
|
||||
# encoder = lc3.Encoder(
|
||||
# frame_duration_us=10000,
|
||||
# sample_rate_hz=48000,
|
||||
# num_channels=2,
|
||||
# input_sample_rate_hz=wav.getframerate(),
|
||||
# )
|
||||
|
||||
# create snoop file
|
||||
f = open("log.btsnoop", "wb")
|
||||
Snooper = BtSnooper(f)
|
||||
device.host.snooper = Snooper
|
||||
encoder.setup_encoders(48000,10000,1)
|
||||
frames = list[bytes]()
|
||||
sine = generate_sine_wave_iso_frames(1000,48000,0.01)
|
||||
sine = generate_sine_data(1000,48000,0.01)
|
||||
print(len(sine))
|
||||
|
||||
sample_size = 480
|
||||
print(wav_file_path)
|
||||
upsampled_left_channel = read_wav_file(wav_file_path)
|
||||
upsampled_left_channel = read_wav_file(wav_file_path,48000)
|
||||
num_runs = len(upsampled_left_channel) // sample_size
|
||||
TEST_SINE = 0
|
||||
for i in range(num_runs):
|
||||
@@ -935,102 +882,12 @@ def run_async(async_command: Coroutine) -> None:
|
||||
# -----------------------------------------------------------------------------
|
||||
# Main
|
||||
# -----------------------------------------------------------------------------
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
def auracast(ctx):
|
||||
ctx.ensure_object(dict)
|
||||
|
||||
|
||||
@auracast.command('scan')
|
||||
@click.command()
|
||||
@click.option('transport','-p', type=str)
|
||||
@click.option('wav_file_path','-w' , type=str)
|
||||
@click.option(
|
||||
'--filter-duplicates', is_flag=True, default=False, help='Filter duplicates'
|
||||
)
|
||||
@click.option(
|
||||
'--sync-timeout',
|
||||
metavar='SYNC_TIMEOUT',
|
||||
type=float,
|
||||
default=AURACAST_DEFAULT_SYNC_TIMEOUT,
|
||||
help='Sync timeout (in seconds)',
|
||||
)
|
||||
@click.argument('transport')
|
||||
@click.pass_context
|
||||
def scan(ctx, filter_duplicates, sync_timeout, transport):
|
||||
"""Scan for public broadcasts"""
|
||||
run_async(run_scan(filter_duplicates, sync_timeout, transport))
|
||||
|
||||
|
||||
@auracast.command('assist')
|
||||
@click.option(
|
||||
'--broadcast-name',
|
||||
metavar='BROADCAST_NAME',
|
||||
help='Broadcast Name to tune to',
|
||||
)
|
||||
@click.option(
|
||||
'--source-id',
|
||||
metavar='SOURCE_ID',
|
||||
type=int,
|
||||
help='Source ID (for remove-source command)',
|
||||
)
|
||||
@click.option(
|
||||
'--command',
|
||||
type=click.Choice(
|
||||
['monitor-state', 'add-source', 'modify-source', 'remove-source']
|
||||
),
|
||||
required=True,
|
||||
)
|
||||
@click.argument('transport')
|
||||
@click.argument('address')
|
||||
@click.pass_context
|
||||
def assist(ctx, broadcast_name, source_id, command, transport, address):
|
||||
"""Scan for broadcasts on behalf of a audio server"""
|
||||
run_async(run_assist(broadcast_name, source_id, command, transport, address))
|
||||
|
||||
|
||||
@auracast.command('pair')
|
||||
@click.argument('transport')
|
||||
@click.argument('address')
|
||||
@click.pass_context
|
||||
def pair(ctx, transport, address):
|
||||
"""Pair with an audio server"""
|
||||
run_async(run_pair(transport, address))
|
||||
|
||||
|
||||
@auracast.command('receive')
|
||||
@click.argument('transport')
|
||||
@click.argument('broadcast_id', type=int)
|
||||
@click.option(
|
||||
'--broadcast-code',
|
||||
metavar='BROADCAST_CODE',
|
||||
type=str,
|
||||
help='Broadcast encryption code in hex format',
|
||||
)
|
||||
@click.option(
|
||||
'--sync-timeout',
|
||||
metavar='SYNC_TIMEOUT',
|
||||
type=float,
|
||||
default=AURACAST_DEFAULT_SYNC_TIMEOUT,
|
||||
help='Sync timeout (in seconds)',
|
||||
)
|
||||
@click.option(
|
||||
'--subgroup',
|
||||
metavar='SUBGROUP',
|
||||
type=int,
|
||||
default=0,
|
||||
help='Index of Subgroup',
|
||||
)
|
||||
@click.pass_context
|
||||
def receive(ctx, transport, broadcast_id, broadcast_code, sync_timeout, subgroup):
|
||||
"""Receive a broadcast source"""
|
||||
run_async(
|
||||
run_receive(transport, broadcast_id, broadcast_code, sync_timeout, subgroup)
|
||||
)
|
||||
|
||||
|
||||
@auracast.command('broadcast')
|
||||
@click.argument('transport')
|
||||
@click.argument('wav_file_path', type=str)
|
||||
@click.option(
|
||||
'--broadcast-id',
|
||||
'--broadcast_id',
|
||||
metavar='BROADCAST_ID',
|
||||
type=int,
|
||||
default=123456,
|
||||
@@ -1042,9 +899,10 @@ def receive(ctx, transport, broadcast_id, broadcast_code, sync_timeout, subgroup
|
||||
type=str,
|
||||
help='Broadcast encryption code in hex format',
|
||||
)
|
||||
@click.pass_context
|
||||
def broadcast(ctx, transport, broadcast_id, broadcast_code, wav_file_path):
|
||||
|
||||
def broadcast(transport, broadcast_id, broadcast_code, wav_file_path):
|
||||
"""Start a broadcast as a source."""
|
||||
#ctx.ensure_object(dict)
|
||||
run_async(
|
||||
run_broadcast(
|
||||
transport=transport,
|
||||
@@ -1056,8 +914,9 @@ def broadcast(ctx, transport, broadcast_id, broadcast_code, wav_file_path):
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'ERROR').upper())
|
||||
auracast()
|
||||
broadcast()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1065,8 +924,9 @@ if __name__ == "__main__":
|
||||
main() # pylint: disable=no-value-for-parameter
|
||||
|
||||
|
||||
# NOTES for the IOT747
|
||||
# Baudrate is 9600
|
||||
# ####### NOTES for the IOT747
|
||||
# Set Baudrate to 9600
|
||||
# SCAN 2 OFF 2
|
||||
# open F0F1F2F3F4F5 BROAD 2 0 1e40
|
||||
# MUSIC 91 PLAY
|
||||
|
||||
|
||||
@@ -38,15 +38,17 @@ from bumble.snoop import BtSnooper
|
||||
import functools
|
||||
from bumble.profiles.ascs import AudioStreamControlServiceProxy
|
||||
from bumble.hci import HCI_IsoDataPacket, HCI_LE_1M_PHY, HCI_LE_2M_PHY
|
||||
import scipy.io.wavfile as wav
|
||||
|
||||
import logging
|
||||
from utils.le_audio_encoder import LeAudioEncoder
|
||||
from leaudio import LeAudioEncoder
|
||||
import asyncio
|
||||
import time
|
||||
import os
|
||||
from bumble import hci
|
||||
import sys
|
||||
|
||||
from leaudio import generate_sine_data, read_wav_file
|
||||
|
||||
|
||||
app_specific_codec = CodecSpecificConfiguration(
|
||||
sampling_frequency=SamplingFrequency.FREQ_24000,
|
||||
@@ -76,51 +78,8 @@ def clean_reset(com_port):
|
||||
print(f"Error opening or accessing {com_port}: {e}")
|
||||
|
||||
|
||||
def read_wav_file(filename):
|
||||
|
||||
rate, data = wav.read(filename)
|
||||
num_channels = data.ndim
|
||||
|
||||
if num_channels == 1:
|
||||
left_channel = data[:]
|
||||
else:
|
||||
left_channel = data[:, 1]
|
||||
|
||||
print(len(left_channel))
|
||||
upsampled_data = signal.resample(left_channel, int(
|
||||
app_specific_codec.sampling_frequency.hz / 41000 * left_channel.shape[0]))
|
||||
|
||||
# wav.write("upsampled_stereo_file.wav", app_specific_codec.sampling_frequency.hz, upsampled_data.astype(data.dtype))
|
||||
print("Sample rate:", rate)
|
||||
print("Number channels:", num_channels)
|
||||
print("Audio data (left):", left_channel)
|
||||
print("Bitdepth:", data.dtype.itemsize * 8)
|
||||
|
||||
return upsampled_data.astype(np.int16)
|
||||
|
||||
|
||||
def generate_sine_wave_iso_frames(frequency, sampling_rate, duration):
|
||||
num_samples = int(sampling_rate * duration)
|
||||
|
||||
t = np.linspace(0, duration, num_samples, False)
|
||||
|
||||
sine_wave = np.sin(2 * np.pi * frequency * t)
|
||||
|
||||
# Scale the sine wave to the 16-bit range (-32768 to 32767)
|
||||
scaled_sine_wave = sine_wave * 8191.5
|
||||
|
||||
# Convert to 16-bit integer format
|
||||
int16_sine_wave = scaled_sine_wave.astype(np.int16)
|
||||
|
||||
iso_frame = bytearray()
|
||||
|
||||
for num in int16_sine_wave:
|
||||
|
||||
iso_frame.append(num & 0xFF) # Extract lower 8 bits
|
||||
|
||||
iso_frame.append((num >> 8) & 0xFF) # Extract upper 8 bit
|
||||
|
||||
return iso_frame
|
||||
|
||||
|
||||
class Listener(Device.Listener):
|
||||
@@ -452,7 +411,7 @@ async def client() -> None:
|
||||
app_specific_codec.frame_duration.us / 1000 / 1000)
|
||||
if TEST_SINE == 0:
|
||||
if os.path.isfile(sound_file):
|
||||
upsampled_left_channel = read_wav_file(sound_file)
|
||||
upsampled_left_channel = read_wav_file(sound_file,app_specific_codec.sampling_frequency.hz)
|
||||
else:
|
||||
raise FileNotFoundError(f"The file {sound_file} does not exist.")
|
||||
|
||||
@@ -466,7 +425,7 @@ async def client() -> None:
|
||||
pcm_data = upsampled_left_channel[i *
|
||||
sample_size:i*sample_size+sample_size]
|
||||
else:
|
||||
pcm_data = generate_sine_wave_iso_frames(
|
||||
pcm_data = generate_sine_data(
|
||||
1000, app_specific_codec.sampling_frequency.hz, app_specific_codec.frame_duration.us / 1000000)
|
||||
|
||||
data = encoder.encode(
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from leaudio.encoder import LeAudioEncoder
|
||||
from leaudio.utils import read_wav_file, generate_sine_data
|
||||
|
||||
121
leaudio/encoder.py
Normal file
121
leaudio/encoder.py
Normal file
@@ -0,0 +1,121 @@
|
||||
|
||||
import wasmtime
|
||||
import ctypes
|
||||
from typing import List, cast
|
||||
import wasmtime.loader
|
||||
import leaudio.liblc3 as liblc3 # type: ignore
|
||||
import enum
|
||||
|
||||
store = wasmtime.loader.store
|
||||
|
||||
_memory = cast(wasmtime.Memory, liblc3.memory)
|
||||
|
||||
STACK_POINTER = _memory.data_len(store)
|
||||
|
||||
_memory.grow(store, 1)
|
||||
|
||||
# Mapping wasmtime memory to linear address
|
||||
|
||||
memory = (ctypes.c_ubyte * _memory.data_len(store)).from_address(
|
||||
|
||||
ctypes.addressof(_memory.data_ptr(store).contents) # type: ignore
|
||||
|
||||
)
|
||||
|
||||
|
||||
class Liblc3PcmFormat(enum.IntEnum):
|
||||
|
||||
S16 = 0
|
||||
|
||||
S24 = 1
|
||||
|
||||
S24_3LE = 2
|
||||
|
||||
FLOAT = 3
|
||||
|
||||
|
||||
DEFAULT_PCM_SAMPLE_RATE = 48000
|
||||
MAX_DECODER_SIZE = liblc3.lc3_decoder_size(10000, DEFAULT_PCM_SAMPLE_RATE)
|
||||
MAX_ENCODER_SIZE = liblc3.lc3_encoder_size(10000, DEFAULT_PCM_SAMPLE_RATE)
|
||||
|
||||
DECODER_STACK_POINTER = STACK_POINTER
|
||||
ENCODER_STACK_POINTER = DECODER_STACK_POINTER + MAX_DECODER_SIZE * 2
|
||||
DECODE_BUFFER_STACK_POINTER = ENCODER_STACK_POINTER + MAX_ENCODER_SIZE * 2
|
||||
ENCODE_BUFFER_STACK_POINTER = DECODE_BUFFER_STACK_POINTER + 8192
|
||||
|
||||
|
||||
DEFAULT_PCM_FORMAT = Liblc3PcmFormat
|
||||
|
||||
DEFAULT_PCM_BYTES_PER_SAMPLE = 2
|
||||
|
||||
|
||||
class LeAudioEncoder:
|
||||
|
||||
def __init__(self):
|
||||
self.encoders: List[int] = []
|
||||
pass
|
||||
|
||||
def setup_encoders(self, sample_rate: int, frame_duration_us: int, num_channels: int) -> None:
|
||||
"""Setup LE audio encoders
|
||||
|
||||
Args:
|
||||
sample_rate (int): Sample rate in Hz
|
||||
frame_duration_us (int): Frame duration in microseconds
|
||||
num_channels (int): Number of channels
|
||||
"""
|
||||
self.encoders[:num_channels] = [
|
||||
liblc3.lc3_setup_encoder(
|
||||
frame_duration_us,
|
||||
sample_rate,
|
||||
0, # Input sample rate
|
||||
ENCODER_STACK_POINTER + MAX_ENCODER_SIZE * i,
|
||||
)
|
||||
for i in range(num_channels)
|
||||
]
|
||||
|
||||
def encode(
|
||||
self,
|
||||
sdu_length: int,
|
||||
num_channels: int,
|
||||
input_stride: int,
|
||||
input_data: bytes,
|
||||
) -> bytes:
|
||||
"""Encode a LE audio frame
|
||||
|
||||
Args:
|
||||
sdu_length (int): Length of the SDU
|
||||
num_channels (int): Number of channels
|
||||
input_stride (int): Stride of the input data
|
||||
input_data (bytes): Input data to encode
|
||||
|
||||
Returns:
|
||||
bytes: Encoded data
|
||||
"""
|
||||
if not input_data:
|
||||
return b""
|
||||
|
||||
input_buffer_offset = ENCODE_BUFFER_STACK_POINTER
|
||||
input_buffer_size = len(input_data)
|
||||
|
||||
# Copy into wasm memory
|
||||
memory[input_buffer_offset : input_buffer_offset + input_buffer_size] = input_data
|
||||
|
||||
output_buffer_offset = input_buffer_offset + input_buffer_size
|
||||
output_buffer_size = sdu_length
|
||||
output_frame_size = output_buffer_size // num_channels
|
||||
|
||||
for i in range(num_channels):
|
||||
result = liblc3.lc3_encode(
|
||||
self.encoders[i],
|
||||
0,
|
||||
input_buffer_offset + DEFAULT_PCM_BYTES_PER_SAMPLE * i,
|
||||
input_stride,
|
||||
output_frame_size,
|
||||
output_buffer_offset + output_frame_size * i,
|
||||
)
|
||||
|
||||
if result != 0:
|
||||
raise RuntimeError(f"lc3_encode failed, result={result}")
|
||||
|
||||
# Extract encoded data from the output buffer
|
||||
return bytes(memory[output_buffer_offset : output_buffer_offset + output_buffer_size])
|
||||
BIN
leaudio/liblc3.wasm
Normal file
BIN
leaudio/liblc3.wasm
Normal file
Binary file not shown.
49
leaudio/utils.py
Normal file
49
leaudio/utils.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from scipy import signal
|
||||
import scipy.io.wavfile as wav
|
||||
import numpy as np
|
||||
|
||||
def read_wav_file(filename,target_sample_rate):
|
||||
|
||||
rate, data = wav.read(filename)
|
||||
num_channels = data.ndim
|
||||
|
||||
if num_channels == 1:
|
||||
left_channel = data[:]
|
||||
else:
|
||||
left_channel = data[:, 1]
|
||||
|
||||
print(len(left_channel))
|
||||
upsampled_data = signal.resample(left_channel, int(
|
||||
target_sample_rate / rate * left_channel.shape[0]))
|
||||
|
||||
# wav.write("upsampled_stereo_file.wav", app_specific_codec.sampling_frequency.hz, upsampled_data.astype(data.dtype))
|
||||
print("Sample rate:", rate)
|
||||
print("Number channels:", num_channels)
|
||||
print("Audio data (left):", left_channel)
|
||||
print("Bitdepth:", data.dtype.itemsize * 8)
|
||||
|
||||
return upsampled_data.astype(np.int16)
|
||||
|
||||
|
||||
def generate_sine_data(frequency, sampling_rate, duration):
|
||||
num_samples = int(sampling_rate * duration)
|
||||
|
||||
t = np.linspace(0, duration, num_samples, False)
|
||||
|
||||
sine_wave = np.sin(2 * np.pi * frequency * t)
|
||||
|
||||
# Scale the sine wave to the 16-bit range (-32768 to 32767)
|
||||
scaled_sine_wave = sine_wave * 8191.5
|
||||
|
||||
# Convert to 16-bit integer format
|
||||
int16_sine_wave = scaled_sine_wave.astype(np.int16)
|
||||
|
||||
iso_frame = bytearray()
|
||||
|
||||
for num in int16_sine_wave:
|
||||
|
||||
iso_frame.append(num & 0xFF) # Extract lower 8 bits
|
||||
|
||||
iso_frame.append((num >> 8) & 0xFF) # Extract upper 8 bit
|
||||
|
||||
return iso_frame
|
||||
@@ -8,19 +8,21 @@ dependencies = ["bumble @ git+https://github.com/markusjellitsch/bumble.git@iso
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
requires = ["setuptools>=61", "wheel", "setuptools_scm>=8"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["leaudio" , "apps", "utils*"]
|
||||
namespaces = true
|
||||
|
||||
[tool.setuptools.package-dir]
|
||||
"leaudio" = "."
|
||||
"leaudio.apps" = "apps"
|
||||
|
||||
|
||||
[project.scripts]
|
||||
unicast_client = "leaudio.apps.bap_unicast_client:main"
|
||||
broadcast_source = "leaudio.apps.bap_broadcast_source:main"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = [
|
||||
"leaudio",
|
||||
"leaudio.apps"
|
||||
]
|
||||
|
||||
[tool.setuptools.package-dir]
|
||||
"leaudio" = "leaudio"
|
||||
"leaudio.apps" = "apps"
|
||||
|
||||
Reference in New Issue
Block a user