add bap_le_unicast client
This commit is contained in:
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
0
__init__.py
Normal file
0
__init__.py
Normal file
4
apps/device.json
Normal file
4
apps/device.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Unicast Client",
|
||||
"address": "C0:98:E5:49:00:00"
|
||||
}
|
||||
477
apps/unicast_client.py
Normal file
477
apps/unicast_client.py
Normal file
@@ -0,0 +1,477 @@
|
||||
from scipy import signal
|
||||
import numpy as np
|
||||
import argparse
|
||||
from bumble.profiles.bap import (
|
||||
|
||||
AudioLocation,
|
||||
|
||||
SamplingFrequency,
|
||||
|
||||
FrameDuration,
|
||||
|
||||
CodecSpecificConfiguration,
|
||||
|
||||
)
|
||||
from bumble.profiles.ascs import (
|
||||
|
||||
AudioStreamControlServiceProxy,
|
||||
|
||||
ASE_Config_Codec,
|
||||
|
||||
ASE_Config_QOS,
|
||||
|
||||
ASE_Disable,
|
||||
|
||||
ASE_Enable,
|
||||
|
||||
)
|
||||
from bumble.hci import CodecID, CodingFormat
|
||||
from bumble.profiles.pacs import (
|
||||
|
||||
PacRecord,
|
||||
|
||||
PublishedAudioCapabilitiesServiceProxy,
|
||||
|
||||
)
|
||||
from bumble.transport.common import StreamPacketSource
|
||||
from bumble.profiles import pacs
|
||||
from bumble.utils import AsyncRunner
|
||||
from bumble.transport import open_transport_or_link
|
||||
from bumble.device import Device, Peer, Advertisement, ConnectionParametersPreferences, Connection
|
||||
from bumble.core import ProtocolError, AdvertisingData
|
||||
from bumble.snoop import BtSnooper
|
||||
import functools
|
||||
from bumble.colors import color
|
||||
from bumble.profiles.ascs import AudioStreamControlServiceProxy
|
||||
from bumble.hci import HCI_IsoDataPacket, HCI_LE_1M_PHY, HCI_LE_2M_PHY
|
||||
from bumble import device
|
||||
from typing import Optional, List, cast
|
||||
import scipy.io.wavfile as wav
|
||||
import logging
|
||||
import sys
|
||||
sys.path.append('../utils')
|
||||
from le_audio_encoder import LeAudioEncoder
|
||||
import asyncio
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
from bumble import hci
|
||||
import sys
|
||||
|
||||
|
||||
app_specific_codec = CodecSpecificConfiguration(
|
||||
sampling_frequency=SamplingFrequency.FREQ_24000,
|
||||
frame_duration=FrameDuration.DURATION_10000_US,
|
||||
audio_channel_allocation=AudioLocation.FRONT_RIGHT,
|
||||
octets_per_codec_frame=60,
|
||||
codec_frames_per_sdu=1,
|
||||
)
|
||||
|
||||
|
||||
TEST_SINE = 1
|
||||
|
||||
|
||||
complete_local_name = "BLE_COVER"
|
||||
iso_packets = []
|
||||
|
||||
upsampled_left_channel = None
|
||||
|
||||
|
||||
def read_wav_file(filename):
|
||||
|
||||
rate, data = wav.read(filename)
|
||||
|
||||
print("Bitdepth:", data.dtype.itemsize * 8)
|
||||
|
||||
print("Sample rate:", rate)
|
||||
|
||||
left_channel = data[:, 1]
|
||||
|
||||
print("Audio data (left):", left_channel)
|
||||
|
||||
print(len(left_channel))
|
||||
print(app_specific_codec.sampling_frequency.hz)
|
||||
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))
|
||||
|
||||
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):
|
||||
|
||||
packet_sequence_number = 0
|
||||
|
||||
def __init__(self, device):
|
||||
|
||||
self.device = device
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
async def on_advertisement(self, advertisement):
|
||||
|
||||
def parse_ltv_packet(data):
|
||||
|
||||
packets = []
|
||||
|
||||
i = 0
|
||||
|
||||
while i < len(data)-1:
|
||||
|
||||
length_byte = data[i]
|
||||
|
||||
type_byte = data[i + 1]
|
||||
|
||||
value_bytes = data[i+2:i+length_byte+1]
|
||||
|
||||
packets.append((length_byte, type_byte, value_bytes))
|
||||
|
||||
i += length_byte+1
|
||||
|
||||
return packets
|
||||
|
||||
ltv_packets = parse_ltv_packet(advertisement.data_bytes)
|
||||
|
||||
for _, type, values in ltv_packets:
|
||||
|
||||
if type is AdvertisingData.COMPLETE_LOCAL_NAME:
|
||||
|
||||
to_compare = bytes(complete_local_name, "utf-8")
|
||||
|
||||
if values == to_compare:
|
||||
|
||||
print("found device " + complete_local_name)
|
||||
|
||||
await self.device.stop_scanning()
|
||||
|
||||
params = ConnectionParametersPreferences()
|
||||
|
||||
params.connection_interval_min = (47)
|
||||
|
||||
params.connection_interval_max = (47)
|
||||
|
||||
prefs = {HCI_LE_1M_PHY: params, HCI_LE_2M_PHY: params}
|
||||
|
||||
await self.device.connect(peer_address=advertisement.address, connection_parameters_preferences=prefs)
|
||||
|
||||
@AsyncRunner.run_in_task()
|
||||
async def on_connection(self, connection: Connection):
|
||||
|
||||
notifications = {1: asyncio.Queue()}
|
||||
|
||||
def on_notification(data: bytes, ase_id: int):
|
||||
|
||||
notifications[ase_id].put_nowait(data)
|
||||
|
||||
print(f'=== Connected to {connection}')
|
||||
|
||||
peer = Peer(connection)
|
||||
|
||||
# set PHY to 2M
|
||||
await connection.set_phy(rx_phys=[HCI_LE_2M_PHY], tx_phys=[HCI_LE_2M_PHY])
|
||||
|
||||
# pair with the device
|
||||
await connection.pair()
|
||||
|
||||
# request mtu change
|
||||
mtu = await peer.request_mtu(1691)
|
||||
|
||||
# get remote features
|
||||
remote_features = await self.device.get_remote_le_features(connection)
|
||||
|
||||
print(f"peer supports the following features:{remote_features}")
|
||||
|
||||
# discover services
|
||||
pacs_client = await peer.discover_service_and_create_proxy(PublishedAudioCapabilitiesServiceProxy)
|
||||
ascs_client = await peer.discover_service_and_create_proxy(AudioStreamControlServiceProxy)
|
||||
|
||||
# read sink PACs
|
||||
response = await pacs_client.sink_pac.read_value()
|
||||
pac_record = PacRecord.from_bytes(response[1:])
|
||||
print(pac_record)
|
||||
|
||||
# enable ASCS notifications
|
||||
await ascs_client.ase_control_point.subscribe()
|
||||
await ascs_client.sink_ase[0].subscribe(
|
||||
functools.partial(on_notification, ase_id=1)
|
||||
)
|
||||
|
||||
# read sink ASE state
|
||||
sink_state = await ascs_client.sink_ase[0].read_value()
|
||||
print(sink_state)
|
||||
|
||||
print(app_specific_codec)
|
||||
await ascs_client.ase_control_point.write_value(
|
||||
ASE_Config_Codec(
|
||||
ase_id=[1],
|
||||
target_latency=[0x3],
|
||||
target_phy=[2],
|
||||
codec_id=[CodingFormat(CodecID.LC3)],
|
||||
codec_specific_configuration=[app_specific_codec],
|
||||
)
|
||||
|
||||
|
||||
)
|
||||
|
||||
# wait for notification
|
||||
await notifications[1].get()
|
||||
print("ASE: codec configured")
|
||||
|
||||
# setup the CIG
|
||||
cis_handles = await self.device.setup_cig(
|
||||
cig_id=1,
|
||||
cis_id=[1],
|
||||
sdu_interval=(app_specific_codec.frame_duration.us, app_specific_codec.frame_duration.us),
|
||||
framing=0,
|
||||
max_sdu=(app_specific_codec.octets_per_codec_frame, 0),
|
||||
retransmission_number=15,
|
||||
max_transport_latency=(95, 95),
|
||||
)
|
||||
|
||||
# configure ASE (config QOS)
|
||||
await ascs_client.ase_control_point.write_value(
|
||||
|
||||
ASE_Config_QOS(
|
||||
ase_id=[1],
|
||||
cig_id=[1],
|
||||
cis_id=[1],
|
||||
sdu_interval=[app_specific_codec.frame_duration.us],
|
||||
framing=[0],
|
||||
phy=[2],
|
||||
max_sdu=[app_specific_codec.octets_per_codec_frame],
|
||||
retransmission_number=[15],
|
||||
max_transport_latency=[95],
|
||||
presentation_delay=[40000],
|
||||
)
|
||||
)
|
||||
|
||||
# wait for notifications
|
||||
await notifications[1].get()
|
||||
print("ASE: QOS configured")
|
||||
|
||||
# configure ASE (Enable)
|
||||
await ascs_client.ase_control_point.write_value(
|
||||
ASE_Enable(
|
||||
ase_id=[1],
|
||||
metadata=[bytes([0x03, 0x02, 0x01, 0x00])],
|
||||
)
|
||||
)
|
||||
|
||||
# wait for notifications
|
||||
await notifications[1].get()
|
||||
print('ASE: enabling')
|
||||
|
||||
# create CIS
|
||||
await self.device.create_cis(
|
||||
[
|
||||
(cis_handles[0], connection.handle)
|
||||
]
|
||||
)
|
||||
|
||||
print('ASE: cis established')
|
||||
|
||||
await self.device.send_command(
|
||||
hci.HCI_LE_Setup_ISO_Data_Path_Command(
|
||||
connection_handle=cis_handles[0],
|
||||
data_path_direction=hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.HOST_TO_CONTROLLER,
|
||||
data_path_id=0x00, # Fixed HCI
|
||||
codec_id=hci.CodingFormat(hci.CodecID.TRANSPARENT),
|
||||
controller_delay=0,
|
||||
codec_configuration=b'',
|
||||
))
|
||||
|
||||
# wait for notifications
|
||||
await notifications[1].get()
|
||||
print('ASE: audio stream enabled')
|
||||
|
||||
# prepere the ISO packets
|
||||
self.packet_sequence_number = 0
|
||||
self.iso_packet = HCI_IsoDataPacket(
|
||||
connection_handle=cis_handles[0],
|
||||
data_total_length=app_specific_codec.octets_per_codec_frame + 4,
|
||||
packet_sequence_number=self.packet_sequence_number,
|
||||
pb_flag=0b10,
|
||||
packet_status_flag=0,
|
||||
iso_sdu_length=app_specific_codec.octets_per_codec_frame,
|
||||
iso_sdu_fragment=bytes([0]*app_specific_codec.octets_per_codec_frame),
|
||||
)
|
||||
|
||||
self.send_complete = False
|
||||
def on_iso_pdu_sent(event):
|
||||
if self.packet_sequence_number < len(iso_packets) - 1:
|
||||
# send the next ISO packet
|
||||
self.packet_sequence_number += 1
|
||||
self.iso_packet.packet_sequence_number = self.packet_sequence_number
|
||||
self.iso_packet.iso_sdu_fragment = iso_packets[self.packet_sequence_number]
|
||||
self.device.host.send_hci_packet(self.iso_packet)
|
||||
else:
|
||||
self.send_complete = True
|
||||
|
||||
self.device.host.on('iso_packet_sent', on_iso_pdu_sent)
|
||||
self.device.host.send_hci_packet(self.iso_packet)
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
if self.send_complete:
|
||||
print("send complete. bye bye")
|
||||
await self.device.power_off()
|
||||
self.device.future.set_result(None)
|
||||
break
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
global complete_local_name
|
||||
parser = argparse.ArgumentParser(
|
||||
description="A simple example of argparse")
|
||||
parser.add_argument("-c", "--config", type=str,
|
||||
default="device.json", help="device config file")
|
||||
parser.add_argument(
|
||||
"-p", "--port", help="com port (e.g. serial:/dev/ttyUSB0)")
|
||||
parser.add_argument("-s", "--sample_rate", type=int,
|
||||
default=0, help="choose a sample rate")
|
||||
parser.add_argument("-f", "--frame_duration", type=int,
|
||||
default=0, help="choose a frame duration")
|
||||
parser.add_argument("-w", "--wave", type=str,
|
||||
help="choose a frame duration")
|
||||
parser.add_argument("-t", "--target_name", type=str,
|
||||
help="target complete local name of the peer")
|
||||
parser.add_argument("--verbose", "-v", action="count", default=0)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
if args.verbose > 0:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
else:
|
||||
|
||||
logging.basicConfig(level=logging.ERROR)
|
||||
|
||||
print("sample rate", args.sample_rate)
|
||||
if not args.sample_rate or args.sample_rate == 0:
|
||||
app_specific_codec.sampling_frequency = SamplingFrequency.FREQ_16000
|
||||
app_specific_codec.frame_duration = FrameDuration.DURATION_10000_US
|
||||
app_specific_codec.octets_per_codec_frame = 40
|
||||
elif args.sample_rate == 1:
|
||||
app_specific_codec.sampling_frequency = SamplingFrequency.FREQ_24000
|
||||
app_specific_codec.frame_duration = FrameDuration.DURATION_10000_US
|
||||
app_specific_codec.octets_per_codec_frame = 60
|
||||
elif args.sample_rate == 2:
|
||||
app_specific_codec.sampling_frequency = SamplingFrequency.FREQ_48000
|
||||
app_specific_codec.frame_duration = FrameDuration.DURATION_10000_US
|
||||
app_specific_codec.octets_per_codec_frame = 120
|
||||
else:
|
||||
raise ValueError("unknown sample rate")
|
||||
|
||||
print(f"sample rate: {app_specific_codec.sampling_frequency.hz} Hz")
|
||||
print(f"frame duration: {app_specific_codec.frame_duration.us} us")
|
||||
print(f"octets per codec frame: {app_specific_codec.octets_per_codec_frame} bytes")
|
||||
|
||||
async with await open_transport_or_link(args.port) as hci_transport:
|
||||
|
||||
# Create a device to manage the host, with a custom listener
|
||||
device = Device.from_config_file_with_hci(
|
||||
args.config, hci_transport.source, hci_transport.sink
|
||||
)
|
||||
|
||||
# Connect to
|
||||
if args.target_name:
|
||||
complete_local_name = args.target_name
|
||||
|
||||
device.listener = Listener(device)
|
||||
device.cis_enabled = True
|
||||
|
||||
f = open("log.btsnoop", "wb")
|
||||
|
||||
Snooper = BtSnooper(f)
|
||||
|
||||
device.host.snooper = Snooper
|
||||
|
||||
if args.wave:
|
||||
sound_file = args.wave
|
||||
TEST_SINE = 0
|
||||
else:
|
||||
TEST_SINE = 1
|
||||
|
||||
await device.power_on()
|
||||
encoder = LeAudioEncoder()
|
||||
encoder.setup_encoders(
|
||||
app_specific_codec.sampling_frequency.hz,
|
||||
app_specific_codec.frame_duration.us,
|
||||
1,
|
||||
)
|
||||
|
||||
# prepare the samples
|
||||
num_runs = 0
|
||||
# calculate the number of samples per frame duration.
|
||||
sample_size = int(app_specific_codec.sampling_frequency.hz * 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)
|
||||
else:
|
||||
raise FileNotFoundError(f"The file {sound_file} does not exist.")
|
||||
|
||||
num_runs = len(upsampled_left_channel) // sample_size
|
||||
|
||||
else:
|
||||
|
||||
num_runs = 2000
|
||||
|
||||
|
||||
print("sample size", sample_size)
|
||||
for i in range(num_runs):
|
||||
|
||||
if TEST_SINE == 0:
|
||||
|
||||
pcm_data = upsampled_left_channel[i *sample_size:i*sample_size+sample_size]
|
||||
|
||||
else:
|
||||
|
||||
pcm_data = generate_sine_wave_iso_frames(
|
||||
1000, app_specific_codec.sampling_frequency.hz, app_specific_codec.frame_duration.us / 1000000)
|
||||
|
||||
data = encoder.encode(app_specific_codec.octets_per_codec_frame, 1, 1, bytes(pcm_data))
|
||||
iso_packets.append(data)
|
||||
|
||||
print("finished with encoding", len(iso_packets))
|
||||
|
||||
print(f'start scanning for {complete_local_name}...')
|
||||
await device.start_scanning(scanning_phys=[HCI_LE_1M_PHY], legacy=False)
|
||||
|
||||
device.future = asyncio.get_running_loop().create_future()
|
||||
await device.future
|
||||
print("done")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
25
pyproject.toml
Normal file
25
pyproject.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[tool.poetry]
|
||||
name = "leaudio"
|
||||
version = "0.1.0"
|
||||
description = "A LE Audio package"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
homepage = "https://example.com"
|
||||
repository = "https://github.com/yourusername/leaudio"
|
||||
keywords = ["leaudio", "audio", "networking"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
# Add your dependencies here
|
||||
# Example:
|
||||
requests = "^2.25.1"
|
||||
asyncio = "^3.4.3"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^6.2.4"
|
||||
# Add your development dependencies here
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
BIN
sounds/test0.wav
Normal file
BIN
sounds/test0.wav
Normal file
Binary file not shown.
BIN
sounds/test1.wav
Normal file
BIN
sounds/test1.wav
Normal file
Binary file not shown.
121
utils/le_audio_encoder.py
Normal file
121
utils/le_audio_encoder.py
Normal file
@@ -0,0 +1,121 @@
|
||||
|
||||
import wasmtime
|
||||
import ctypes
|
||||
from typing import List, cast
|
||||
import wasmtime.loader
|
||||
import 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
utils/liblc3.wasm
Normal file
BIN
utils/liblc3.wasm
Normal file
Binary file not shown.
Reference in New Issue
Block a user