aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine SOULIER <103120622+asoulier@users.noreply.github.com>2024-01-30 14:09:32 -0800
committerGitHub <noreply@github.com>2024-01-30 14:09:32 -0800
commit8b1619706e4c9bd6cc95a1722585ace0adec76f1 (patch)
tree40253e57aa2e5a9d511006ef3560f37d96fb1dc6
parent10999c6c58f38f941ce192d6466405e0d7f8c19c (diff)
parent8e0bd81fe483c6c3f47d1f0d970904ff726d6179 (diff)
downloadliblc3-8b1619706e4c9bd6cc95a1722585ace0adec76f1.tar.gz
Merge pull request #41 from google/libpython
Python library wrapper
-rw-r--r--README.md19
-rw-r--r--python/LICENSE202
-rw-r--r--python/lc3.py393
-rw-r--r--python/pyproject.toml16
-rwxr-xr-xpython/tools/decoder.py96
-rwxr-xr-xpython/tools/encoder.py88
-rwxr-xr-xpython/tools/specgram.py92
-rw-r--r--[-rwxr-xr-x]test/decoder.py81
-rw-r--r--[-rwxr-xr-x]test/encoder.py88
9 files changed, 905 insertions, 170 deletions
diff --git a/README.md b/README.md
index d183393..9f4d8f0 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ The directory layout is as follows :
- include: Library interface
- src: Source files
- tools: Standalone encoder/decoder tools
+- python: Python wrapper
- test: Unit testing framework
- fuzz: Roundtrip fuzz testing harness
- build: Building outputs
@@ -62,7 +63,7 @@ Compiled library will be found in `bin` directory.
## Tools
-Tools can be all compiled, while involking `make` as follows :
+Tools can be all compiled, while invoking `make` as follows :
```sh
$ make tools
@@ -138,6 +139,7 @@ The conformance reports can be found [here](conformance/README.md)
The codec was [_here_](https://hydrogenaud.io/index.php/topic,122575.0.html)
subjectively evaluated in a blind listening test.
+
## Meson build system
Meson build system is also available to build and install lc3 codec in Linux
@@ -148,3 +150,18 @@ $ meson build
$ cd build && meson install
```
+## Python wrapper
+
+A python wrapper, installed as follows, is available in the `python` directory.
+
+```sh
+$ python3 -m pip install ./python
+```
+
+Decoding and encoding tools are provided in `python/tools`, like C tools,
+you can easly test encoding / decoding loop with :
+
+```sh
+$ python3 ./python/tools/encoder.py <in.wav> --bitrate <bitrate> | \
+ python3 ./python/tools/decoder.py > <out.wav>
+```
diff --git a/python/LICENSE b/python/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/python/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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/lc3.py b/python/lc3.py
new file mode 100644
index 0000000..4245b23
--- /dev/null
+++ b/python/lc3.py
@@ -0,0 +1,393 @@
+#
+# Copyright 2024 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.
+#
+
+import array
+import ctypes
+import enum
+
+from ctypes import c_bool, c_byte, c_int, c_uint, c_size_t, c_void_p
+from ctypes.util import find_library
+
+
+class _Base:
+
+ def __init__(self, frame_duration, samplerate, nchannels, **kwargs):
+
+ self.hrmode = False
+ self.dt_us = int(frame_duration * 1000)
+ self.sr_hz = int(samplerate)
+ self.sr_pcm_hz = self.sr_hz
+ self.nchannels = nchannels
+
+ libpath = None
+
+ for k in kwargs.keys():
+ if k == 'hrmode':
+ self.hrmode = bool(kwargs[k])
+ elif k == 'pcm_samplerate':
+ self.sr_pcm_hz = int(kwargs[k])
+ elif k == 'libpath':
+ libpath = kwargs[k]
+ else:
+ raise ValueError("Invalid keyword argument: " + k)
+
+ if self.dt_us not in [2500, 5000, 7500, 10000]:
+ raise ValueError(
+ "Invalid frame duration: %.1f ms" % frame_duration)
+
+ allowed_samplerate = [8000, 16000, 24000, 32000, 48000] \
+ if not self.hrmode else [48000, 96000]
+
+ if self.sr_hz not in allowed_samplerate:
+ raise ValueError("Invalid sample rate: %d Hz" % samplerate)
+
+ if libpath is None:
+ libpath = find_library("lc3")
+ if not libpath:
+ raise Exception("LC3 library not found")
+
+ lib = ctypes.cdll.LoadLibrary(libpath)
+
+ try:
+ lib.lc3_hr_frame_samples \
+ and lib.lc3_hr_frame_block_bytes \
+ and lib.lc3_hr_resolve_bitrate \
+ and lib.lc3_hr_delay_samples
+
+ except AttributeError:
+
+ if self.hrmode:
+ raise Exception('High-Resolution interface not available')
+
+ lib.lc3_hr_frame_samples = \
+ lambda hrmode, dt_us, sr_hz: \
+ lib.lc3_frame_samples(dt_us, sr_hz)
+
+ lib.lc3_hr_frame_block_bytes = \
+ lambda hrmode, dt_us, sr_hz, nchannels, bitrate: \
+ nchannels * lib.lc3_frame_bytes(dt_us, bitrate // 2)
+
+ lib.lc3_hr_resolve_bitrate = \
+ lambda hrmode, dt_us, sr_hz, nbytes: \
+ lib.lc3_resolve_bitrate(dt_us, nbytes)
+
+ lib.lc3_hr_delay_samples = \
+ lambda hrmode, dt_us, sr_hz: \
+ lib.lc3_delay_samples(dt_us, sr_hz)
+
+ 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
+
+ libc = ctypes.cdll.LoadLibrary(find_library("c"))
+
+ 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]
+
+ def get_frame_samples(self):
+ """
+ Returns the number of PCM samples in an LC3 frame
+ """
+ ret = self.lib.lc3_hr_frame_samples(
+ self.hrmode, self.dt_us, self.sr_pcm_hz)
+ if ret < 0:
+ raise ValueError("Bad parameters")
+ return ret
+
+ def get_frame_bytes(self, bitrate):
+ """
+ Returns the size of LC3 frame blocks, from bitrate in bit per seconds.
+ 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.dt_us, self.sr_hz, self.nchannels, bitrate)
+ if ret < 0:
+ raise ValueError("Bad parameters")
+ return ret
+
+ def resolve_bitrate(self, nbytes):
+ """
+ Returns the bitrate in bits per seconds, from the size of LC3 frames.
+ """
+ ret = self.lib.lc3_hr_resolve_bitrate(
+ self.hrmode, self.dt_us, self.sr_hz, nbytes)
+ if ret < 0:
+ raise ValueError("Bad parameters")
+ return ret
+
+ def get_delay_samples(self):
+ """
+ Returns the algorithmic delay, as a number of samples.
+ """
+ ret = self.lib.lc3_hr_delay_samples(
+ self.hrmode, self.dt_us, self.sr_pcm_hz)
+ if ret < 0:
+ raise ValueError("Bad parameters")
+ return ret
+
+ @staticmethod
+ def _resolve_pcm_format(bitdepth):
+ PCM_FORMAT_S16 = 0
+ PCM_FORMAT_S24 = 1
+ PCM_FORMAT_S24_3LE = 2
+ PCM_FORMAT_FLOAT = 3
+
+ match bitdepth:
+ case 16: return (PCM_FORMAT_S16, ctypes.c_int16)
+ case 24: return (PCM_FORMAT_S24_3LE, 3 * ctypes.c_byte)
+ case None: return (PCM_FORMAT_FLOAT, ctypes.c_float)
+ case _: raise ValueError("Could not interpret PCM bitdepth")
+
+
+class Encoder(_Base):
+ """
+ LC3 Encoder wrapper
+
+ The `frame_duration` expressed in milliseconds is any of 2.5, 5.0, 7.5
+ or 10.0. The `samplerate`, in Hertz, is any of 8000, 16000, 24000, 32000
+ or 48000, unless High-Resolution mode is enabled. In High-Resolution mode,
+ the `samplerate` is 48000 or 96000.
+
+ By default, one channel is processed. When `nchannels` is greater than one,
+ the PCM input stream is read interleaved and consecutives LC3 frames are
+ output, for each channel.
+
+ Keyword arguments:
+ hrmode : Enable High-Resolution mode, default is `False`.
+ sr_pcm_hz : Input PCM samplerate, enable downsampling of input.
+ libpath : LC3 library path and name
+ """
+
+ class c_encoder_t(c_void_p):
+ pass
+
+ def __init__(self, frame_duration, samplerate, nchannels=1, **kwargs):
+
+ super().__init__(frame_duration, samplerate, nchannels, **kwargs)
+
+ lib = self.lib
+
+ try:
+ lib.lc3_hr_encoder_size \
+ and lib.lc3_hr_setup_encoder
+
+ except AttributeError:
+
+ assert not self.hrmode
+
+ lib.lc3_hr_encoder_size = \
+ lambda hrmode, dt_us, sr_hz: \
+ lib.lc3_encoder_size(dt_us, sr_hz)
+
+ lib.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)
+
+ 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(
+ self.hrmode, self.dt_us, self.sr_hz, self.sr_pcm_hz,
+ self.malloc(lib.lc3_hr_encoder_size(
+ self.hrmode, self.dt_us, self.sr_pcm_hz)))
+
+ self.__encoders = [new_encoder() for i in range(nchannels)]
+
+ def __del__(self):
+
+ try:
+ (self.free(encoder) for encoder in self.__encoders)
+ finally:
+ return
+
+ def encode(self, pcm, nbytes, bitdepth=None):
+ """
+ Encode LC3 frame(s), for each channel.
+
+ The `pcm` input is given in two ways. When no `bitdepth` is defined,
+ it's a vector of floating point values from -1 to 1, coding the sample
+ levels. When `bitdepth` is defined, `pcm` is interpreted as a byte-like
+ object, each sample coded on `bitdepth` bits (16 or 24).
+ The machine endianness, or little endian, is used for 16 or 24 bits
+ width, respectively.
+ In both cases, the `pcm` vector data is padded with zeros when
+ its length is less than the required input samples for the encoder.
+ Channels concatenation of encoded LC3 frames, of `nbytes`, is returned.
+ """
+
+ nchannels = self.nchannels
+ frame_samples = self.get_frame_samples()
+
+ (pcm_fmt, pcm_t) = self._resolve_pcm_format(bitdepth)
+ pcm_len = nchannels * frame_samples
+
+ if bitdepth is None:
+ pcm_buffer = array.array('f', pcm)
+
+ # Invert test to catch NaN
+ if not abs(sum(pcm)) / frame_samples < 2:
+ raise ValueError("Out of range PCM input")
+
+ padding = max(pcm_len - frame_samples, 0)
+ pcm_buffer.extend(array.array('f', [0] * padding))
+
+ else:
+ padding = max(pcm_len * ctypes.sizeof(pcm_t) - len(pcm), 0)
+ pcm_buffer = bytearray(pcm) + bytearray(padding)
+
+ data_buffer = (c_byte * nbytes)()
+ 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 = nbytes // nchannels + int(ich < nbytes % 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 ValueError("Bad parameters")
+
+ return bytes(data_buffer)
+
+
+class Decoder(_Base):
+ """
+ LC3 Decoder wrapper
+
+ The `frame_duration` expressed in milliseconds is any of 2.5, 5.0, 7.5
+ or 10.0. The `samplerate`, in Hertz, is any of 8000, 16000, 24000, 32000
+ or 48000, unless High-Resolution mode is enabled. In High-Resolution
+ mode, the `samplerate` is 48000 or 96000.
+
+ By default, one channel is processed. When `nchannels` is greater than one,
+ the PCM input stream is read interleaved and consecutives LC3 frames are
+ output, for each channel.
+
+ Keyword arguments:
+ hrmode : Enable High-Resolution mode, default is `False`.
+ sr_pcm_hz : Output PCM samplerate, enable upsampling of output.
+ libpath : LC3 library path and name
+ """
+
+ class c_decoder_t(c_void_p):
+ pass
+
+ def __init__(self, frame_duration, samplerate, nchannels=1, **kwargs):
+
+ super().__init__(frame_duration, samplerate, nchannels, **kwargs)
+
+ lib = self.lib
+
+ try:
+ lib.lc3_hr_decoder_size \
+ and lib.lc3_hr_setup_decoder
+
+ except AttributeError:
+
+ assert not self.hrmode
+
+ lib.lc3_hr_decoder_size = \
+ lambda hrmode, dt_us, sr_hz: \
+ lib.lc3_decoder_size(dt_us, sr_hz)
+
+ lib.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)
+
+ 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(
+ self.hrmode, self.dt_us, self.sr_hz, self.sr_pcm_hz,
+ self.malloc(lib.lc3_hr_decoder_size(
+ self.hrmode, self.dt_us, self.sr_pcm_hz)))
+
+ self.__decoders = [new_decoder() for i in range(nchannels)]
+
+ def __del__(self):
+
+ try:
+ (self.free(decoder) for decoder in self.__decoders)
+ finally:
+ return
+
+ def decode(self, data, bitdepth=None):
+ """
+ Decode an LC3 frame
+
+ The input `data` is the channels concatenation of LC3 frames in a
+ byte-like object. Interleaved PCM samples are returned according to
+ the `bitdepth` indication.
+ When no `bitdepth` is defined, it's a vector of floating point values
+ from -1 to 1, coding the sample levels. When `bitdepth` is defined,
+ it returns a byte array, each sample coded on `bitdepth` bits.
+ The machine endianness, or little endian, is used for 16 or 24 bits
+ width, respectively.
+ """
+
+ nchannels = self.nchannels
+ frame_samples = self.get_frame_samples()
+
+ (pcm_fmt, pcm_t) = self._resolve_pcm_format(bitdepth)
+ pcm_len = nchannels * self.get_frame_samples()
+ pcm_buffer = (pcm_t * pcm_len)()
+
+ 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)
+
+ data_size = len(data_buffer) // nchannels + \
+ int(ich < len(data_buffer) % nchannels)
+ data = (c_byte * data_size).from_buffer(data_buffer, data_offset)
+ data_offset += data_size
+
+ ret = self.lib.lc3_decode(
+ decoder, data, len(data), pcm_fmt, pcm, self.nchannels)
+ if ret < 0:
+ raise ValueError("Bad parameters")
+
+ return array.array('f', pcm_buffer) \
+ if bitdepth is None else bytes(pcm_buffer)
diff --git a/python/pyproject.toml b/python/pyproject.toml
new file mode 100644
index 0000000..c1bb77c
--- /dev/null
+++ b/python/pyproject.toml
@@ -0,0 +1,16 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "lc3"
+version = "0.0.1"
+license = "Apache-2.0"
+authors = [
+ { name="Antoine Soulier", email="asoulier@google.com" },
+]
+description = "LC3 Codec library wrapper"
+requires-python = ">=3.10"
+
+[project.urls]
+Homepage = "https://github.com/google/liblc3"
diff --git a/python/tools/decoder.py b/python/tools/decoder.py
new file mode 100755
index 0000000..c10c1e4
--- /dev/null
+++ b/python/tools/decoder.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+#
+# Copyright 2024 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.
+#
+
+import argparse
+import lc3
+import struct
+import sys
+import wave
+
+parser = argparse.ArgumentParser(description='LC3 Decoder')
+
+parser.add_argument(
+ 'lc3_file', nargs='?',
+ help='Input bitstream file, default is stdin',
+ type=argparse.FileType('rb'), default=sys.stdin.buffer)
+
+parser.add_argument(
+ 'wav_file', nargs='?',
+ help='Output wave file, default is stdout',
+ type=argparse.FileType('wb'), default=sys.stdout.buffer)
+
+parser.add_argument(
+ '--bitdepth',
+ help='Output bitdepth, default is 16 bits',
+ type=int, choices=[16, 24], default=16)
+
+parser.add_argument(
+ '--libpath', help='LC3 Library path')
+
+args = parser.parse_args()
+
+# --- LC3 File input ---
+
+f_lc3 = args.lc3_file
+
+header = struct.unpack('=HHHHHHHI', f_lc3.read(18))
+if header[0] != 0xcc1c:
+ raise ValueError('Invalid bitstream file')
+
+samplerate = header[2] * 100
+nchannels = header[4]
+frame_duration = header[5] / 100
+stream_length = header[7]
+
+# --- Setup output ---
+
+bitdepth = args.bitdepth
+pcm_size = nchannels * (bitdepth // 8)
+
+f_wav = args.wav_file
+wavfile = wave.open(f_wav)
+wavfile.setnchannels(nchannels)
+wavfile.setsampwidth(bitdepth // 8)
+wavfile.setframerate(samplerate)
+wavfile.setnframes(stream_length)
+
+# --- Setup decoder ---
+
+dec = lc3.Decoder(
+ frame_duration, samplerate, nchannels, libpath=args.libpath)
+frame_length = dec.get_frame_samples()
+encoded_length = stream_length + dec.get_delay_samples()
+
+# --- Decoding loop ---
+
+for i in range(0, encoded_length, frame_length):
+
+ lc3_frame_size = struct.unpack('=H', f_lc3.read(2))[0]
+ pcm = dec.decode(f_lc3.read(lc3_frame_size), bitdepth=bitdepth)
+
+ pcm = pcm[max(encoded_length - stream_length - i, 0) * pcm_size:
+ min(encoded_length - i, frame_length) * pcm_size]
+
+ wavfile.writeframesraw(pcm)
+
+# --- Cleanup ---
+
+wavfile.close()
+
+for f in (f_lc3, f_wav):
+ if f is not sys.stdout.buffer:
+ f.close()
diff --git a/python/tools/encoder.py b/python/tools/encoder.py
new file mode 100755
index 0000000..9f291ff
--- /dev/null
+++ b/python/tools/encoder.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+#
+# Copyright 2024 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.
+#
+
+import argparse
+import lc3
+import struct
+import sys
+import wave
+
+parser = argparse.ArgumentParser(description='LC3 Encoder')
+
+parser.add_argument(
+ 'wav_file', nargs='?',
+ help='Input wave file, default is stdin',
+ type=argparse.FileType('rb'), default=sys.stdin.buffer)
+
+parser.add_argument(
+ 'lc3_file', nargs='?',
+ help='Output bitstream file, default is stdout',
+ type=argparse.FileType('wb'), default=sys.stdout.buffer)
+
+parser.add_argument(
+ '--bitrate', help='Bitrate in bps', type=int, required=True)
+
+parser.add_argument(
+ '--frame_duration', help='Frame duration in ms', type=float, default=10)
+
+parser.add_argument(
+ '--libpath', help='LC3 Library path')
+
+args = parser.parse_args()
+
+# --- WAV File input ---
+
+f_wav = args.wav_file
+wavfile = wave.open(f_wav, 'rb')
+
+samplerate = wavfile.getframerate()
+nchannels = wavfile.getnchannels()
+bitdepth = wavfile.getsampwidth() * 8
+stream_length = wavfile.getnframes()
+
+# --- Setup encoder ---
+
+enc = lc3.Encoder(
+ args.frame_duration, samplerate, nchannels, libpath=args.libpath)
+frame_size = enc.get_frame_bytes(args.bitrate)
+frame_length = enc.get_frame_samples()
+bitrate = enc.resolve_bitrate(frame_size)
+
+# --- Setup output ---
+
+f_lc3 = args.lc3_file
+f_lc3.write(struct.pack(
+ '=HHHHHHHI', 0xcc1c, 18,
+ samplerate // 100, bitrate // 100, nchannels,
+ int(args.frame_duration * 100), 0, stream_length))
+
+# --- Encoding loop ---
+
+for i in range(0, stream_length, frame_length):
+
+ f_lc3.write(struct.pack('=H', frame_size))
+
+ pcm = wavfile.readframes(frame_length)
+ f_lc3.write(enc.encode(pcm, frame_size, bitdepth=bitdepth))
+
+# --- Cleanup ---
+
+wavfile.close()
+
+for f in (f_wav, f_lc3):
+ if f is not sys.stdout.buffer:
+ f.close()
diff --git a/python/tools/specgram.py b/python/tools/specgram.py
new file mode 100755
index 0000000..42781d4
--- /dev/null
+++ b/python/tools/specgram.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+#
+# Copyright 2024 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.
+#
+
+import argparse
+import lc3
+import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+import scipy.signal as signal
+
+matplotlib.use('QtAgg')
+
+parser = argparse.ArgumentParser(description='LC3 Encoder')
+
+parser.add_argument(
+ '--frame_duration', help='Frame duration (ms)', type=float, default=10)
+
+parser.add_argument(
+ '--sample_rate', help='Sampling frequency (Hz)', type=float, default=48000)
+
+parser.add_argument(
+ '--hrmode', help='Enable high-resolution mode', action='store_true')
+
+parser.add_argument(
+ '--bitrate', help='Bitrate (bps)', type=int, default=96000)
+
+parser.add_argument(
+ '--libpath', help='LC3 Library path')
+
+args = parser.parse_args()
+
+# --- Setup encoder + decoder ---
+
+fs = args.sample_rate
+
+enc = lc3.Encoder(
+ args.frame_duration, fs, hrmode=args.hrmode, libpath=args.libpath)
+dec = lc3.Decoder(
+ args.frame_duration, fs, hrmode=args.hrmode, libpath=args.libpath)
+
+frame_len = enc.get_frame_samples()
+delay_len = enc.get_delay_samples()
+
+# --- Generate 10 seconds chirp ---
+
+n = 10 * int(fs)
+t = np.arange(n - (n % frame_len)) / fs
+s = signal.chirp(t, f0=10, f1=fs/2, t1=t[-1], phi=-90, method='linear')
+
+# --- Encoding / decoding loop ---
+
+frame_size = enc.get_frame_bytes(args.bitrate)
+bitrate = enc.resolve_bitrate(frame_size)
+
+y = np.empty(len(s) + frame_len)
+
+for i in range(0, len(s), frame_len):
+ y[i:i+frame_len] = dec.decode(enc.encode(s[i:i+frame_len], frame_size))
+
+y[len(s):] = dec.decode(enc.encode(np.zeros(frame_len), frame_size))
+y = y[delay_len:len(s)+delay_len]
+
+# --- Plot spectrograms ---
+
+fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
+
+NFFT = 512
+for (ax, s) in [(ax1, s), (ax2, y)]:
+ ax.specgram(s, Fs=fs, NFFT=NFFT, pad_to=4*NFFT, noverlap=NFFT//2,
+ vmin=-160, vmax=0)
+
+ax1.set_title('Input signal')
+ax1.set_ylabel('Frequency (Hz)')
+
+ax2.set_title(('Processed signal (%.1f kbps)' % (bitrate/1000)))
+ax2.set_ylabel('Frequency (Hz)')
+
+plt.show()
diff --git a/test/decoder.py b/test/decoder.py
index 26cdccc..388fc62 100755..100644
--- a/test/decoder.py
+++ b/test/decoder.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python3
#
# Copyright 2022 Google LLC
#
@@ -27,8 +26,6 @@ import tables as T, appendix_c as C
import mdct, energy, bwdet, sns, tns, spec, ltpf
import bitstream
-### ------------------------------------------------------------------------ ###
-
class Decoder:
def __init__(self, dt_ms, sr_hz):
@@ -96,8 +93,6 @@ class Decoder:
return x
-### ------------------------------------------------------------------------ ###
-
def check_appendix_c(dt):
i0 = dt - T.DT_7M5
@@ -120,79 +115,3 @@ def check():
ok = ok and check_appendix_c(dt)
return ok
-
-### ------------------------------------------------------------------------ ###
-
-if __name__ == "__main__":
-
- parser = argparse.ArgumentParser(description='LC3 Decoder Test Framework')
- parser.add_argument('lc3_file',
- help='Input bitstream file', type=argparse.FileType('r'))
- parser.add_argument('--pyout',
- help='Python output file', type=argparse.FileType('w'))
- parser.add_argument('--cout',
- help='C output file', type=argparse.FileType('w'))
- args = parser.parse_args()
-
- ### File Header ###
-
- f_lc3 = open(args.lc3_file.name, 'rb')
-
- header = struct.unpack('=HHHHHHHI', f_lc3.read(18))
-
- if header[0] != 0xcc1c:
- raise ValueError('Invalid bitstream file')
-
- if header[4] != 1:
- raise ValueError('Unsupported number of channels')
-
- sr_hz = header[2] * 100
- bitrate = header[3] * 100
- nchannels = header[4]
- dt_ms = header[5] / 100
-
- f_lc3.seek(header[1])
-
- ### Setup ###
-
- dec = Decoder(dt_ms, sr_hz)
- dec_c = lc3.setup_decoder(int(dt_ms * 1000), sr_hz)
-
- pcm_c = np.empty(0).astype(np.int16)
- pcm_py = np.empty(0).astype(np.int16)
-
- ### Decoding loop ###
-
- nframes = 0
-
- while True:
-
- data = f_lc3.read(2)
- if len(data) != 2:
- break
-
- (frame_nbytes,) = struct.unpack('=H', data)
-
- print('Decoding frame %d' % nframes, end='\r')
-
- data = f_lc3.read(frame_nbytes)
-
- x = dec.run(data)
- pcm_py = np.append(pcm_py,
- np.clip(np.round(x), -32768, 32767).astype(np.int16))
-
- x_c = lc3.decode(dec_c, data)
- pcm_c = np.append(pcm_c, x_c)
-
- nframes += 1
-
- print('done ! %16s' % '')
-
- ### Terminate ###
-
- if args.pyout:
- wavfile.write(args.pyout.name, sr_hz, pcm_py)
- if args.cout:
- wavfile.write(args.cout.name, sr_hz, pcm_c)
-
-### ------------------------------------------------------------------------ ###
diff --git a/test/encoder.py b/test/encoder.py
index f687d4b..913420e 100755..100644
--- a/test/encoder.py
+++ b/test/encoder.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python3
#
# Copyright 2022 Google LLC
#
@@ -28,8 +27,6 @@ import attdet, ltpf
import mdct, energy, bwdet, sns, tns, spec
import bitstream
-### ------------------------------------------------------------------------ ###
-
class Encoder:
def __init__(self, dt_ms, sr_hz):
@@ -104,8 +101,6 @@ class Encoder:
return data
-### ------------------------------------------------------------------------ ###
-
def check_appendix_c(dt):
i0 = dt - T.DT_7M5
@@ -128,86 +123,3 @@ def check():
ok = ok and check_appendix_c(dt)
return ok
-
-### ------------------------------------------------------------------------ ###
-
-def dump(data):
- for i in range(0, len(data), 20):
- print(''.join('{:02x} '.format(x)
- for x in data[i:min(i+20, len(data))] ))
-
-if __name__ == "__main__":
-
- parser = argparse.ArgumentParser(description='LC3 Encoder Test Framework')
- parser.add_argument('wav_file',
- help='Input wave file', type=argparse.FileType('r'))
- parser.add_argument('--bitrate',
- help='Bitrate in bps', type=int, required=True)
- parser.add_argument('--dt',
- help='Frame duration in ms', type=float, default=10)
- parser.add_argument('--pyout',
- help='Python output file', type=argparse.FileType('w'))
- parser.add_argument('--cout',
- help='C output file', type=argparse.FileType('w'))
- args = parser.parse_args()
-
- if args.bitrate < 16000 or args.bitrate > 320000:
- raise ValueError('Invalid bitate %d bps' % args.bitrate)
-
- if args.dt not in (7.5, 10):
- raise ValueError('Invalid frame duration %.1f ms' % args.dt)
-
- (sr_hz, pcm) = wavfile.read(args.wav_file.name)
- if sr_hz not in (8000, 16000, 24000, 320000, 48000):
- raise ValueError('Unsupported input samplerate: %d' % sr_hz)
- if pcm.ndim != 1:
- raise ValueError('Only single channel wav file supported')
-
- ### Setup ###
-
- enc = Encoder(args.dt, sr_hz)
- enc_c = lc3.setup_encoder(int(args.dt * 1000), sr_hz)
-
- frame_samples = int((args.dt * sr_hz) / 1000)
- frame_nbytes = int((args.bitrate * args.dt) / (1000 * 8))
-
- ### File Header ###
-
- f_py = open(args.pyout.name, 'wb') if args.pyout else None
- f_c = open(args.cout.name , 'wb') if args.cout else None
-
- header = struct.pack('=HHHHHHHI', 0xcc1c, 18,
- sr_hz // 100, args.bitrate // 100, 1, int(args.dt * 100), 0, len(pcm))
-
- for f in (f_py, f_c):
- if f: f.write(header)
-
- ### Encoding loop ###
-
- if len(pcm) % frame_samples > 0:
- pcm = np.append(pcm, np.zeros(frame_samples - (len(pcm) % frame_samples)))
-
- for i in range(0, len(pcm), frame_samples):
-
- print('Encoding frame %d' % (i // frame_samples), end='\r')
-
- frame_pcm = pcm[i:i+frame_samples]
-
- data = enc.run(frame_pcm, frame_nbytes)
- data_c = lc3.encode(enc_c, frame_pcm, frame_nbytes)
-
- for f in (f_py, f_c):
- if f: f.write(struct.pack('=H', frame_nbytes))
-
- if f_py: f_py.write(data)
- if f_c: f_c.write(data_c)
-
- print('done ! %16s' % '')
-
- ### Terminate ###
-
- for f in (f_py, f_c):
- if f: f.close()
-
-
-### ------------------------------------------------------------------------ ###