diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7938dbf..f3623a0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -90,4 +90,5 @@ jobs: with: python-version: '3.10' cache: 'pip' - - run: pip install . + - run: pip install . --group dev + - run: pytest python/tests diff --git a/meson.build b/meson.build index 0fa0904..d49d157 100644 --- a/meson.build +++ b/meson.build @@ -36,5 +36,6 @@ if get_option('tools') endif if get_option('python') + add_languages('cpp') subdir('python') endif diff --git a/pyproject.toml b/pyproject.toml index 3de5361..d574c9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["meson-python"] +requires = ["meson-python", "pybind11"] build-backend = "mesonpy" [project] @@ -12,7 +12,7 @@ authors = [ description = "LC3 Codec library wrapper" requires-python = ">=3.10" -[project.optional-dependencies] +[dependency-groups] dev = ["pytest"] [project.urls] diff --git a/python/_lc3.pyi b/python/_lc3.pyi new file mode 100644 index 0000000..962ded0 --- /dev/null +++ b/python/_lc3.pyi @@ -0,0 +1,56 @@ +import collections.abc + +class Decoder: + def __init__( + self, + hrmode: bool, + dt_us: int, + sr_hz: int, + sr_pcm_hz: int, + nchannels: int, + ) -> None: ... + def decode( + self, data_obj: collections.abc.Buffer | None, pcm_fmt: int + ) -> bytes: ... + def get_frame_samples(self) -> int: ... + +class Encoder: + def __init__( + self, + hrmode: bool, + dt_us: int, + sr_hz: int, + sr_pcm_hz: int, + nchannels: int, + ) -> None: ... + def encode( + self, + pcm: collections.abc.Buffer, + pcm_fmt: int, + num_bytes: int, + ) -> bytes: ... + def get_frame_samples(self) -> int: ... + +def lc3_hr_delay_samples( + hrmode: bool, + dt_us: int, + sr_hz: int, +) -> int: ... +def lc3_hr_frame_block_bytes( + hrmode: bool, + dt_us: int, + sr_hz: int, + nchannels: int, + bitrate: int, +) -> int: ... +def lc3_hr_frame_samples( + hrmode: bool, + dt_us: int, + sr_hz: int, +) -> int: ... +def lc3_hr_resolve_bitrate( + hrmode: bool, + dt_us: int, + sr_hz: int, + nbytes: int, +) -> int: ... diff --git a/python/lc3.py b/python/lc3.py index eda76fa..b320a6e 100644 --- a/python/lc3.py +++ b/python/lc3.py @@ -16,16 +16,12 @@ from __future__ import annotations import array -import ctypes import enum -import glob -import os import typing - -from ctypes import c_bool, c_byte, c_int, c_uint, c_size_t, c_void_p -from ctypes.util import find_library from collections.abc import Iterable +import _lc3 + class BaseError(Exception): """Base error raised by liblc3.""" @@ -76,79 +72,19 @@ def __init__( if self.sample_rate_hz not in allowed_samplerate: raise InvalidArgumentError(f"Invalid sample rate: {sample_rate_hz} Hz") - if libpath is None: - mesonpy_lib = glob.glob( - os.path.join(os.path.dirname(__file__), ".lc3py.mesonpy.libs", "*lc3*") - ) - - if mesonpy_lib: - libpath = mesonpy_lib[0] - else: - libpath = find_library("lc3") - if not libpath: - raise InitializationError("LC3 library not found") - - lib = ctypes.cdll.LoadLibrary(libpath) - - if not all( - hasattr(lib, func) - for func in ( - "lc3_hr_frame_samples", - "lc3_hr_frame_block_bytes", - "lc3_hr_resolve_bitrate", - "lc3_hr_delay_samples", - ) - ): - if self.hrmode: - raise InitializationError("High-Resolution interface not available") - - lc3_hr_frame_samples = lambda hrmode, dt_us, sr_hz: lib.lc3_frame_samples( - dt_us, sr_hz - ) - lc3_hr_frame_block_bytes = ( - lambda hrmode, dt_us, sr_hz, num_channels, bitrate: num_channels - * lib.lc3_frame_bytes(dt_us, bitrate // 2) - ) - lc3_hr_resolve_bitrate = ( - lambda hrmode, dt_us, sr_hz, nbytes: lib.lc3_resolve_bitrate( - dt_us, nbytes - ) - ) - lc3_hr_delay_samples = lambda hrmode, dt_us, sr_hz: lib.lc3_delay_samples( - dt_us, sr_hz - ) - setattr(lib, "lc3_hr_frame_samples", lc3_hr_frame_samples) - setattr(lib, "lc3_hr_frame_block_bytes", lc3_hr_frame_block_bytes) - setattr(lib, "lc3_hr_resolve_bitrate", lc3_hr_resolve_bitrate) - setattr(lib, "lc3_hr_delay_samples", lc3_hr_delay_samples) - - lib.lc3_hr_frame_samples.argtypes = [c_bool, c_int, c_int] - lib.lc3_hr_frame_block_bytes.argtypes = [c_bool, c_int, c_int, c_int, c_int] - lib.lc3_hr_resolve_bitrate.argtypes = [c_bool, c_int, c_int, c_int] - lib.lc3_hr_delay_samples.argtypes = [c_bool, c_int, c_int] - self.lib = lib - - if not (libc_path := find_library("c")): - raise InitializationError("Unable to find libc") - libc = ctypes.cdll.LoadLibrary(libc_path) - - self.malloc = libc.malloc - self.malloc.argtypes = [c_size_t] - self.malloc.restype = c_void_p - - self.free = libc.free - self.free.argtypes = [c_void_p] + if libpath is not None: + pass def get_frame_samples(self) -> int: """ Returns the number of PCM samples in an LC3 frame. """ - ret = self.lib.lc3_hr_frame_samples( - self.hrmode, self.frame_duration_us, self.pcm_sample_rate_hz - ) - if ret < 0: - raise InvalidArgumentError("Bad parameters") - return ret + try: + return _lc3.lc3_hr_frame_samples( + self.hrmode, self.frame_duration_us, self.pcm_sample_rate_hz + ) + except ValueError as e: + raise InvalidArgumentError("Bad parameters") from e def get_frame_bytes(self, bitrate: int) -> int: """ @@ -156,51 +92,48 @@ def get_frame_bytes(self, bitrate: int) -> int: A target `bitrate` equals 0 or `INT32_MAX` returns respectively the minimum and maximum allowed size. """ - ret = self.lib.lc3_hr_frame_block_bytes( - self.hrmode, - self.frame_duration_us, - self.sample_rate_hz, - self.num_channels, - bitrate, - ) - if ret < 0: - raise InvalidArgumentError("Bad parameters") - return ret + try: + return _lc3.lc3_hr_frame_block_bytes( + self.hrmode, + self.frame_duration_us, + self.sample_rate_hz, + self.num_channels, + bitrate, + ) + except ValueError as e: + raise InvalidArgumentError("Bad parameters") from e def resolve_bitrate(self, num_bytes: int) -> int: """ Returns the bitrate in bits per seconds, from the size of LC3 frames. """ - ret = self.lib.lc3_hr_resolve_bitrate( - self.hrmode, self.frame_duration_us, self.sample_rate_hz, num_bytes - ) - if ret < 0: - raise InvalidArgumentError("Bad parameters") - return ret + try: + return _lc3.lc3_hr_resolve_bitrate( + self.hrmode, self.frame_duration_us, self.sample_rate_hz, num_bytes + ) + except ValueError as e: + raise InvalidArgumentError("Bad parameters") from e def get_delay_samples(self) -> int: """ Returns the algorithmic delay, as a number of samples. """ - ret = self.lib.lc3_hr_delay_samples( - self.hrmode, self.frame_duration_us, self.pcm_sample_rate_hz - ) - if ret < 0: - raise InvalidArgumentError("Bad parameters") - return ret + try: + return _lc3.lc3_hr_delay_samples( + self.hrmode, self.frame_duration_us, self.pcm_sample_rate_hz + ) + except ValueError as e: + raise InvalidArgumentError("Bad parameters") from e @classmethod - def _resolve_pcm_format(cls, bit_depth: int | None) -> tuple[ - _PcmFormat, - type[ctypes.c_int16] | type[ctypes.Array[ctypes.c_byte]] | type[ctypes.c_float], - ]: + def _resolve_pcm_format(cls, bit_depth: int | None) -> _PcmFormat: match bit_depth: case 16: - return (_PcmFormat.S16, ctypes.c_int16) + return _PcmFormat.S16 case 24: - return (_PcmFormat.S24_3LE, 3 * ctypes.c_byte) + return _PcmFormat.S24_3LE case None: - return (_PcmFormat.FLOAT, ctypes.c_float) + return _PcmFormat.FLOAT case _: raise InvalidArgumentError("Could not interpret PCM bit_depth") @@ -224,9 +157,6 @@ class Encoder(_Base): libpath : LC3 library path and name """ - class c_encoder_t(c_void_p): - pass - def __init__( self, frame_duration_us: int, @@ -246,63 +176,16 @@ def __init__( libpath, ) - lib = self.lib - - if not all( - hasattr(lib, func) - for func in ("lc3_hr_encoder_size", "lc3_hr_setup_encoder") - ): - if self.hrmode: - raise InitializationError("High-Resolution interface not available") - - lc3_hr_encoder_size = lambda hrmode, dt_us, sr_hz: lib.lc3_encoder_size( - dt_us, sr_hz - ) - - lc3_hr_setup_encoder = ( - lambda hrmode, dt_us, sr_hz, sr_pcm_hz, mem: lib.lc3_setup_encoder( - dt_us, sr_hz, sr_pcm_hz, mem - ) - ) - setattr(lib, "lc3_hr_encoder_size", lc3_hr_encoder_size) - setattr(lib, "lc3_hr_setup_encoder", lc3_hr_setup_encoder) - - lib.lc3_hr_encoder_size.argtypes = [c_bool, c_int, c_int] - lib.lc3_hr_encoder_size.restype = c_uint - - lib.lc3_hr_setup_encoder.argtypes = [c_bool, c_int, c_int, c_int, c_void_p] - lib.lc3_hr_setup_encoder.restype = self.c_encoder_t - - lib.lc3_encode.argtypes = [ - self.c_encoder_t, - c_int, - c_void_p, - c_int, - c_int, - c_void_p, - ] - - def new_encoder(): - return lib.lc3_hr_setup_encoder( + try: + self._impl = _lc3.Encoder( self.hrmode, self.frame_duration_us, self.sample_rate_hz, self.pcm_sample_rate_hz, - self.malloc( - lib.lc3_hr_encoder_size( - self.hrmode, self.frame_duration_us, self.pcm_sample_rate_hz - ) - ), + self.num_channels, ) - - self.__encoders = [new_encoder() for _ in range(num_channels)] - - def __del__(self) -> None: - - try: - (self.free(encoder) for encoder in self.__encoders) - finally: - return + except Exception as e: + raise InitializationError("Failed to initialize encoder") from e @typing.overload def encode( @@ -331,44 +214,31 @@ def encode(self, pcm, num_bytes: int, bit_depth: int | None = None) -> bytes: its length is less than the required input samples for the encoder. Channels concatenation of encoded LC3 frames, of `nbytes`, is returned. """ - - nchannels = self.num_channels + pcm_fmt = self._resolve_pcm_format(bit_depth) frame_samples = self.get_frame_samples() - - (pcm_fmt, pcm_t) = self._resolve_pcm_format(bit_depth) - pcm_len = nchannels * frame_samples + pcm_len = self.num_channels * frame_samples if bit_depth is None: - pcm_buffer = array.array("f", pcm) - + pcm_buffer: typing.Any = array.array("f", pcm) # Invert test to catch NaN if not abs(sum(pcm_buffer)) / frame_samples < 2: raise InvalidArgumentError("Out of range PCM input") padding = max(pcm_len - frame_samples, 0) - pcm_buffer.extend(array.array("f", [0] * padding)) - + pcm_buffer.extend(array.array("f", [0.0] * padding)) else: - padding = max(pcm_len * ctypes.sizeof(pcm_t) - len(pcm), 0) - pcm_buffer = bytearray(pcm) + bytearray(padding) # type: ignore + sample_size = 2 if pcm_fmt == _PcmFormat.S16 else 3 + expected_bytes = pcm_len * sample_size - data_buffer = (c_byte * num_bytes)() - data_offset = 0 - - for ich, encoder in enumerate(self.__encoders): - - pcm_offset = ich * ctypes.sizeof(pcm_t) - pcm = (pcm_t * (pcm_len - ich)).from_buffer(pcm_buffer, pcm_offset) - - data_size = num_bytes // nchannels + int(ich < num_bytes % nchannels) - data = (c_byte * data_size).from_buffer(data_buffer, data_offset) - data_offset += data_size - - ret = self.lib.lc3_encode(encoder, pcm_fmt, pcm, nchannels, len(data), data) - if ret < 0: - raise InvalidArgumentError("Bad parameters") + if len(pcm) < expected_bytes: + pcm_buffer = bytearray(pcm) + bytearray(expected_bytes - len(pcm)) + else: + pcm_buffer = pcm - return bytes(data_buffer) + try: + return self._impl.encode(pcm_buffer, int(pcm_fmt), num_bytes) + except Exception as e: + raise InvalidArgumentError("Bad parameters or encoding failed") from e class Decoder(_Base): @@ -390,9 +260,6 @@ class Decoder(_Base): libpath : LC3 library path and name """ - class c_decoder_t(c_void_p): - pass - def __init__( self, frame_duration_us: int, @@ -412,63 +279,16 @@ def __init__( libpath, ) - lib = self.lib - - if not all( - hasattr(lib, func) - for func in ("lc3_hr_decoder_size", "lc3_hr_setup_decoder") - ): - if self.hrmode: - raise InitializationError("High-Resolution interface not available") - - lc3_hr_decoder_size = lambda hrmode, dt_us, sr_hz: lib.lc3_decoder_size( - dt_us, sr_hz - ) - - lc3_hr_setup_decoder = ( - lambda hrmode, dt_us, sr_hz, sr_pcm_hz, mem: lib.lc3_setup_decoder( - dt_us, sr_hz, sr_pcm_hz, mem - ) - ) - setattr(lib, "lc3_hr_decoder_size", lc3_hr_decoder_size) - setattr(lib, "lc3_hr_setup_decoder", lc3_hr_setup_decoder) - - lib.lc3_hr_decoder_size.argtypes = [c_bool, c_int, c_int] - lib.lc3_hr_decoder_size.restype = c_uint - - lib.lc3_hr_setup_decoder.argtypes = [c_bool, c_int, c_int, c_int, c_void_p] - lib.lc3_hr_setup_decoder.restype = self.c_decoder_t - - lib.lc3_decode.argtypes = [ - self.c_decoder_t, - c_void_p, - c_int, - c_int, - c_void_p, - c_int, - ] - - def new_decoder(): - return lib.lc3_hr_setup_decoder( + try: + self._impl = _lc3.Decoder( self.hrmode, self.frame_duration_us, self.sample_rate_hz, self.pcm_sample_rate_hz, - self.malloc( - lib.lc3_hr_decoder_size( - self.hrmode, self.frame_duration_us, self.pcm_sample_rate_hz - ) - ), + self.num_channels, ) - - self.__decoders = [new_decoder() for i in range(num_channels)] - - def __del__(self) -> None: - - try: - (self.free(decoder) for decoder in self.__decoders) - finally: - return + except Exception as e: + raise InitializationError("Failed to initialize decoder") from e @typing.overload def decode( @@ -476,7 +296,9 @@ def decode( ) -> array.array[float]: ... @typing.overload - def decode(self, data: bytes | bytearray | memoryview | None, bit_depth: int) -> bytes: ... + def decode( + self, data: bytes | bytearray | memoryview | None, bit_depth: int + ) -> bytes: ... def decode( self, data: bytes | bytearray | memoryview | None, bit_depth: int | None = None @@ -495,36 +317,14 @@ def decode( The machine endianness, or little endian, is used for 16 or 24 bits width, respectively. """ + pcm_fmt = self._resolve_pcm_format(bit_depth) - num_channels = self.num_channels - - (pcm_fmt, pcm_t) = self._resolve_pcm_format(bit_depth) - pcm_len = num_channels * self.get_frame_samples() - pcm_buffer = (pcm_t * pcm_len)() - - if data is not None: - data_buffer = bytearray(data) - data_offset = 0 - - for ich, decoder in enumerate(self.__decoders): - pcm_offset = ich * ctypes.sizeof(pcm_t) - pcm = (pcm_t * (pcm_len - ich)).from_buffer(pcm_buffer, pcm_offset) + try: + decoded_bytes = self._impl.decode(data, int(pcm_fmt)) + except Exception as e: + raise InvalidArgumentError("Bad parameters or decoding failed") from e - if data is None: - ret = self.lib.lc3_decode( - decoder, None, 0, pcm_fmt, pcm, self.num_channels - ) - else: - data_size = len(data_buffer) // num_channels + int( - ich < len(data_buffer) % num_channels - ) - buf = (c_byte * data_size).from_buffer(data_buffer, data_offset) - data_offset += data_size - ret = self.lib.lc3_decode( - decoder, buf, len(buf), pcm_fmt, pcm, self.num_channels - ) - - if ret < 0: - raise InvalidArgumentError("Bad parameters") - - return array.array("f", pcm_buffer) if bit_depth is None else bytes(pcm_buffer) + if bit_depth is None: + return array.array("f", decoded_bytes) + else: + return decoded_bytes diff --git a/python/lc3_pybind.cpp b/python/lc3_pybind.cpp new file mode 100644 index 0000000..2e13e97 --- /dev/null +++ b/python/lc3_pybind.cpp @@ -0,0 +1,192 @@ +/****************************************************************************** + * + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +#include +#include +#include +#include +#include +#include + +namespace py = pybind11; + +class Encoder { + std::vector encoders; + std::vector> mem; + int nchannels; + int frame_samples; + +public: + Encoder(bool hrmode, int dt_us, int sr_hz, int sr_pcm_hz, int nchannels) + : nchannels(nchannels) { + frame_samples = lc3_hr_frame_samples(hrmode, dt_us, sr_pcm_hz); + if (frame_samples < 0) { + throw std::invalid_argument("Invalid parameters for frame samples"); + } + + unsigned size = lc3_hr_encoder_size(hrmode, dt_us, sr_pcm_hz); + if (size == 0) { + throw std::invalid_argument("Invalid parameters for encoder size"); + } + + for (int i = 0; i < nchannels; ++i) { + mem.emplace_back(size); + lc3_encoder_t enc = lc3_hr_setup_encoder(hrmode, dt_us, sr_hz, sr_pcm_hz, mem.back().data()); + if (!enc) { + throw std::runtime_error("Failed to setup encoder"); + } + encoders.push_back(enc); + } + } + + py::bytes encode(py::buffer pcm, int pcm_fmt, int num_bytes) { + auto info = pcm.request(); + uint8_t* pcm_ptr = static_cast(info.ptr); + + int sample_size = 0; + switch(pcm_fmt) { + case LC3_PCM_FORMAT_S16: sample_size = 2; break; + case LC3_PCM_FORMAT_S24_3LE: sample_size = 3; break; + case LC3_PCM_FORMAT_FLOAT: sample_size = 4; break; + default: throw std::invalid_argument("Unsupported PCM format"); + } + + int expected_samples = nchannels * frame_samples; + if (info.size * info.itemsize < expected_samples * sample_size) { + throw std::invalid_argument("PCM buffer too small"); + } + + std::vector out_buf(num_bytes); + int data_offset = 0; + + for (int ich = 0; ich < nchannels; ++ich) { + int data_size = num_bytes / nchannels + (ich < (num_bytes % nchannels) ? 1 : 0); + uint8_t* ch_pcm = pcm_ptr + ich * sample_size; + + int ret = lc3_encode(encoders[ich], static_cast(pcm_fmt), + ch_pcm, nchannels, data_size, out_buf.data() + data_offset); + + if (ret < 0) { + throw std::runtime_error("lc3_encode failed"); + } + + data_offset += data_size; + } + + return py::bytes(reinterpret_cast(out_buf.data()), num_bytes); + } + + int get_frame_samples() const { return frame_samples; } +}; + +class Decoder { + std::vector decoders; + std::vector> mem; + int nchannels; + int frame_samples; + +public: + Decoder(bool hrmode, int dt_us, int sr_hz, int sr_pcm_hz, int nchannels) + : nchannels(nchannels) { + frame_samples = lc3_hr_frame_samples(hrmode, dt_us, sr_pcm_hz); + if (frame_samples < 0) { + throw std::invalid_argument("Invalid parameters for frame samples"); + } + + unsigned size = lc3_hr_decoder_size(hrmode, dt_us, sr_pcm_hz); + if (size == 0) { + throw std::invalid_argument("Invalid parameters for decoder size"); + } + + for (int i = 0; i < nchannels; ++i) { + mem.emplace_back(size); + lc3_decoder_t dec = lc3_hr_setup_decoder(hrmode, dt_us, sr_hz, sr_pcm_hz, mem.back().data()); + if (!dec) { + throw std::runtime_error("Failed to setup decoder"); + } + decoders.push_back(dec); + } + } + + py::bytes decode(py::object data_obj, int pcm_fmt) { + int sample_size = 0; + switch(pcm_fmt) { + case LC3_PCM_FORMAT_S16: sample_size = 2; break; + case LC3_PCM_FORMAT_S24_3LE: sample_size = 3; break; + case LC3_PCM_FORMAT_FLOAT: sample_size = 4; break; + default: throw std::invalid_argument("Unsupported PCM format"); + } + + int out_len = nchannels * frame_samples * sample_size; + std::vector out_buf(out_len); + + if (data_obj.is_none()) { + for (int ich = 0; ich < nchannels; ++ich) { + uint8_t* ch_pcm = out_buf.data() + ich * sample_size; + int ret = lc3_decode(decoders[ich], nullptr, 0, static_cast(pcm_fmt), + ch_pcm, nchannels); + if (ret < 0) { + throw std::runtime_error("lc3_decode failed"); + } + } + } else { + py::buffer data_buf(data_obj); + auto info = data_buf.request(); + uint8_t* data_ptr = static_cast(info.ptr); + int data_len = info.size * info.itemsize; + + int data_offset = 0; + for (int ich = 0; ich < nchannels; ++ich) { + int data_size = data_len / nchannels + (ich < (data_len % nchannels) ? 1 : 0); + uint8_t* ch_pcm = out_buf.data() + ich * sample_size; + + int ret = lc3_decode(decoders[ich], data_ptr + data_offset, data_size, + static_cast(pcm_fmt), ch_pcm, nchannels); + + if (ret < 0) { + throw std::runtime_error("lc3_decode failed"); + } + + data_offset += data_size; + } + } + + return py::bytes(reinterpret_cast(out_buf.data()), out_len); + } + + int get_frame_samples() const { return frame_samples; } +}; + +PYBIND11_MODULE(_lc3, m) { + m.doc() = "pybind11 wrapper for liblc3"; + + py::class_(m, "Encoder") + .def(py::init()) + .def("encode", &Encoder::encode) + .def("get_frame_samples", &Encoder::get_frame_samples); + + py::class_(m, "Decoder") + .def(py::init()) + .def("decode", &Decoder::decode) + .def("get_frame_samples", &Decoder::get_frame_samples); + + m.def("lc3_hr_frame_samples", &lc3_hr_frame_samples); + m.def("lc3_hr_frame_block_bytes", &lc3_hr_frame_block_bytes); + m.def("lc3_hr_resolve_bitrate", &lc3_hr_resolve_bitrate); + m.def("lc3_hr_delay_samples", &lc3_hr_delay_samples); +} diff --git a/python/meson.build b/python/meson.build index e63c768..48d2da1 100644 --- a/python/meson.build +++ b/python/meson.build @@ -1,3 +1,12 @@ py = import('python').find_installation() -py.install_sources('lc3.py') +pybind11_dep = dependency('pybind11') + +py.extension_module('_lc3', + 'lc3_pybind.cpp', + dependencies: [pybind11_dep, liblc3_dep], + install: true, + subdir: '' +) + +py.install_sources('lc3.py', 'py.typed', '_lc3.pyi') diff --git a/python/py.typed b/python/py.typed new file mode 100644 index 0000000..dcda36a --- /dev/null +++ b/python/py.typed @@ -0,0 +1,15 @@ +# +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# diff --git a/python/tests/basic_test.py b/python/tests/basic_test.py index de61088..8968d4a 100644 --- a/python/tests/basic_test.py +++ b/python/tests/basic_test.py @@ -1,4 +1,5 @@ import array + import lc3 import pytest diff --git a/python/tools/decoder.py b/python/tools/decoder.py index 7cc5127..f9e7d05 100755 --- a/python/tools/decoder.py +++ b/python/tools/decoder.py @@ -16,11 +16,12 @@ # import argparse -import lc3 import struct import sys import wave +import lc3 + parser = argparse.ArgumentParser(description='LC3 Decoder') parser.add_argument( diff --git a/python/tools/encoder.py b/python/tools/encoder.py index 3246c4c..0f8d739 100755 --- a/python/tools/encoder.py +++ b/python/tools/encoder.py @@ -16,11 +16,12 @@ # import argparse -import lc3 import struct import sys import wave +import lc3 + parser = argparse.ArgumentParser(description='LC3 Encoder') parser.add_argument( diff --git a/python/tools/specgram.py b/python/tools/specgram.py index 42781d4..f425e58 100755 --- a/python/tools/specgram.py +++ b/python/tools/specgram.py @@ -16,6 +16,7 @@ # import argparse + import lc3 import matplotlib import matplotlib.pyplot as plt