From cc12ef911458565b62bf72eda05512de3a0d6016 Mon Sep 17 00:00:00 2001 From: Joe Todd Date: Fri, 29 Mar 2024 14:47:58 +0000 Subject: [PATCH 1/8] Fix bug in the shutdown behaviour of the decoder --- CHANGELOG.rst | 4 ++++ pyflac/decoder.py | 18 ++++++++++-------- pyproject.toml | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eca7004..f9b7cf5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ pyFLAC Changelog ---------------- +**v2.3.0** + +* Fixed bug in the shutdown behaviour of the decoder (see #22 and #23). + **v2.2.0** * Updated FLAC library to v1.4.3. diff --git a/pyflac/decoder.py b/pyflac/decoder.py index d848981..0f865ec 100644 --- a/pyflac/decoder.py +++ b/pyflac/decoder.py @@ -225,8 +225,8 @@ def finish(self): # Instruct the decoder to finish up and wait until it is done # -------------------------------------------------------------- self._done = True - super().finish() self._thread.join(timeout=3) + super().finish() if self._error: raise DecoderProcessException(self._error) @@ -314,6 +314,14 @@ def _read_callback(_decoder, If an exception is raised here, the abort status is returned. """ decoder = _ffi.from_handle(client_data) + + while len(decoder._buffer) == 0 and not (decoder._error or decoder._done): + # ---------------------------------------------------------- + # Wait until there is something in the buffer, or an error + # occurs, or the end of the stream is reached. + # ---------------------------------------------------------- + time.sleep(0.01) + if decoder._error: # ---------------------------------------------------------- # If an error has been issued via the error callback, then @@ -329,18 +337,12 @@ def _read_callback(_decoder, num_bytes[0] = 0 return _lib.FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM - maximum_bytes = int(num_bytes[0]) - while len(decoder._buffer) == 0: - # ---------------------------------------------------------- - # Wait until there is something in the buffer - # ---------------------------------------------------------- - time.sleep(0.01) - # -------------------------------------------------------------- # Ensure only the maximum bytes or less is taken from # the thread safe queue. # -------------------------------------------------------------- data = bytes() + maximum_bytes = int(num_bytes[0]) if len(decoder._buffer[0]) <= maximum_bytes: data = decoder._buffer.popleft() maximum_bytes -= len(data) diff --git a/pyproject.toml b/pyproject.toml index ae42b9e..afac4d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta" [project] name = "pyFLAC" -version = "2.2.0" +version = "2.3.0" description = "A Python wrapper for libFLAC" readme = "README.rst" authors = [{ name = "Joe Todd", email = "joe.todd@sonos.com" }] From 2cb68a87c9788df1d8fe7b62048e6ed45000184a Mon Sep 17 00:00:00 2001 From: Joe Todd Date: Sat, 30 Mar 2024 13:15:03 +0000 Subject: [PATCH 2/8] Add comment to finish about lingering processing threads --- pyflac/decoder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyflac/decoder.py b/pyflac/decoder.py index 0f865ec..7f791f0 100644 --- a/pyflac/decoder.py +++ b/pyflac/decoder.py @@ -93,7 +93,8 @@ def finish(self): Flushes the decoding buffer, releases resources, resets the decoder settings to their defaults, and returns the decoder state to `DecoderState.UNINITIALIZED`. - A well behaved program should always call this at the end. + A well behaved program should always call this at the end, otherwise the processing + thread will be left running, awaiting more data. """ _lib.FLAC__stream_decoder_finish(self._decoder) From a3945c3a1e2c4a1683364c96a1c0ccc96c3e2967 Mon Sep 17 00:00:00 2001 From: Joe Todd Date: Sun, 31 Mar 2024 11:44:29 +0100 Subject: [PATCH 3/8] Update unit test to check for hanging threads --- tests/test_decoder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_decoder.py b/tests/test_decoder.py index acfdc88..9017f7c 100644 --- a/tests/test_decoder.py +++ b/tests/test_decoder.py @@ -76,6 +76,7 @@ def test_process(self): self.decoder.process(test_data) self.decoder.finish() self.assertTrue(self.write_callback_called) + self.assertFalse(self.decoder._thread.is_alive()) def test_process_blocks(self): """ Test that FLAC data can be decoded in blocks """ From 949cf630bd4be5dc9495a3fecfdc3ef2b347bd6b Mon Sep 17 00:00:00 2001 From: Joe Todd Date: Sun, 31 Mar 2024 12:21:09 +0100 Subject: [PATCH 4/8] Automatically detect bit depth in FileEncoder, and raise error --- CHANGELOG.rst | 4 +++- pyflac/__main__.py | 2 -- pyflac/decoder.py | 2 +- pyflac/encoder.py | 16 ++++++++++------ pyproject.toml | 4 ++-- tests/test_decoder.py | 2 +- tests/test_encoder.py | 9 +-------- 7 files changed, 18 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f9b7cf5..634b67a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,9 +1,11 @@ pyFLAC Changelog ---------------- -**v2.3.0** +**v3.0.0** * Fixed bug in the shutdown behaviour of the decoder (see #22 and #23). +* Automatically detect bit depth of input data in the `FileEncoder`, and + raise an error if not 16-bit or 32-bit PCM (see #24). **v2.2.0** diff --git a/pyflac/__main__.py b/pyflac/__main__.py index 2a14dd7..fdbe233 100644 --- a/pyflac/__main__.py +++ b/pyflac/__main__.py @@ -27,7 +27,6 @@ def get_args(): parser.add_argument('-c', '--compression-level', type=int, choices=range(0, 9), default=5, help='0 is the fastest compression, 5 is the default, 8 is the highest compression') parser.add_argument('-b', '--block-size', type=int, default=0, help='The block size') - parser.add_argument('-d', '--dtype', default='int16', help='The encoded data type (int16 or int32)') parser.add_argument('-v', '--verify', action='store_false', default=True, help='Verify the compressed data') args = parser.parse_args() return args @@ -45,7 +44,6 @@ def main(): input_file=args.input_file, output_file=args.output_file, blocksize=args.block_size, - dtype=args.dtype, compression_level=args.compression_level, verify=args.verify ) diff --git a/pyflac/decoder.py b/pyflac/decoder.py index 7f791f0..bc8b80a 100644 --- a/pyflac/decoder.py +++ b/pyflac/decoder.py @@ -4,7 +4,7 @@ # # pyFLAC decoder # -# Copyright (c) 2020-2021, Sonos, Inc. +# Copyright (c) 2020-2024, Sonos, Inc. # All rights reserved. # # ------------------------------------------------------------------------------ diff --git a/pyflac/encoder.py b/pyflac/encoder.py index fde1797..5261f75 100644 --- a/pyflac/encoder.py +++ b/pyflac/encoder.py @@ -4,7 +4,7 @@ # # pyFLAC encoder # -# Copyright (c) 2020-2021, Sonos, Inc. +# Copyright (c) 2020-2024, Sonos, Inc. # All rights reserved. # # ------------------------------------------------------------------------------ @@ -335,6 +335,8 @@ class FileEncoder(_Encoder): The pyFLAC file encoder reads the raw audio data from the WAV file and writes the encoded audio data to a FLAC file. + Note that the input WAV file must be either PCM_16 or PCM_32. + Args: input_file (pathlib.Path): Path to the input WAV file output_file (pathlib.Path): Path to the output FLAC file, a temporary @@ -345,8 +347,6 @@ class FileEncoder(_Encoder): blocksize (int): The size of the block to be returned in the callback. The default is 0 which allows libFLAC to determine the best block size. - dtype (str): The data type to use in the FLAC encoder, either int16 or int32, - defaults to int16. streamable_subset (bool): Whether to use the streamable subset for encoding. If true the encoder will check settings for compatibility. If false, the settings may take advantage of the full range that the format allows. @@ -365,13 +365,17 @@ def __init__(self, output_file: Path = None, compression_level: int = 5, blocksize: int = 0, - dtype: str = 'int16', streamable_subset: bool = True, verify: bool = False): super().__init__() - if dtype not in ('int16', 'int32'): - raise ValueError('FLAC encoding data type must be either int16 or int32') + info = sf.info(str(input_file)) + if info.subtype == 'PCM_16': + dtype = 'int16' + elif info.subtype == 'PCM_32': + dtype = 'int32' + else: + raise ValueError(f'WAV input data type must be either PCM_16 or PCM_32: Got {info.subtype}') self.__raw_audio, sample_rate = sf.read(str(input_file), dtype=dtype) if output_file: diff --git a/pyproject.toml b/pyproject.toml index afac4d2..ece5d91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ # # pyFLAC # -# Copyright (c) 2020-2023, Sonos, Inc. +# Copyright (c) 2020-2024, Sonos, Inc. # All rights reserved. # # ------------------------------------------------------------------------------ @@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta" [project] name = "pyFLAC" -version = "2.3.0" +version = "3.0.0" description = "A Python wrapper for libFLAC" readme = "README.rst" authors = [{ name = "Joe Todd", email = "joe.todd@sonos.com" }] diff --git a/tests/test_decoder.py b/tests/test_decoder.py index 9017f7c..7998efc 100644 --- a/tests/test_decoder.py +++ b/tests/test_decoder.py @@ -4,7 +4,7 @@ # # pyFLAC decoder test suite # -# Copyright (c) 2020-2021, Sonos, Inc. +# Copyright (c) 2020-2024, Sonos, Inc. # All rights reserved. # # ------------------------------------------------------------------------------ diff --git a/tests/test_encoder.py b/tests/test_encoder.py index f84dca7..ac876e0 100644 --- a/tests/test_encoder.py +++ b/tests/test_encoder.py @@ -4,7 +4,7 @@ # # pyFLAC encoder test suite # -# Copyright (c) 2020-2021, Sonos, Inc. +# Copyright (c) 2020-2024, Sonos, Inc. # All rights reserved. # # ------------------------------------------------------------------------------ @@ -237,12 +237,6 @@ def test_invalid_blocksize(self): with self.assertRaisesRegex(EncoderInitException, 'FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BLOCK_SIZE'): self.encoder._init() - def test_invalid_dtype(self): - """ Test than an exception is raised if given an invalid dtype """ - self.default_kwargs['dtype'] = 'int24' - with self.assertRaisesRegex(ValueError, 'FLAC encoding data type must be either int16 or int32'): - self.encoder = FileEncoder(**self.default_kwargs) - def test_blocksize_streamable_subset(self): """ Test that an exception is raised if blocksize is outside the streamable subset """ self.default_kwargs['blocksize'] = 65535 @@ -291,7 +285,6 @@ def test_process_32_bit_file(self): test_path = pathlib.Path(__file__).parent.absolute() / 'data/32bit.wav' self.default_kwargs['input_file'] = test_path self.default_kwargs['output_file'] = pathlib.Path(self.temp_file.name) - self.default_kwargs['dtype'] = 'int32' self.encoder = FileEncoder(**self.default_kwargs) self.encoder.process() From 3952eac016562f1f18ccee9bcaa9655e3c9622fa Mon Sep 17 00:00:00 2001 From: Joe Todd Date: Wed, 10 Apr 2024 08:20:11 +0100 Subject: [PATCH 5/8] Use threading Events instead of sleeps, and lock access to buffer --- examples/passthrough.py | 11 ++++++++++- pyflac/decoder.py | 31 ++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/examples/passthrough.py b/examples/passthrough.py index eb329ae..b9d1983 100755 --- a/examples/passthrough.py +++ b/examples/passthrough.py @@ -29,7 +29,16 @@ def __init__(self, args): self.idx = 0 self.total_bytes = 0 self.queue = queue.SimpleQueue() - self.data, self.sr = sf.read(args.input_file, dtype='int16', always_2d=True) + + info = sf.info(str(args.input_file)) + if info.subtype == 'PCM_16': + dtype = 'int16' + elif info.subtype == 'PCM_32': + dtype = 'int32' + else: + raise ValueError(f'WAV input data type must be either PCM_16 or PCM_32: Got {info.subtype}') + + self.data, self.sr = sf.read(args.input_file, dtype=dtype, always_2d=True) self.encoder = pyflac.StreamEncoder( write_callback=self.encoder_callback, diff --git a/pyflac/decoder.py b/pyflac/decoder.py index bc8b80a..625b957 100644 --- a/pyflac/decoder.py +++ b/pyflac/decoder.py @@ -160,6 +160,8 @@ def __init__(self, self._done = False self._buffer = deque() + self._event = threading.Event() + self._lock = threading.Lock() self.write_callback = write_callback rc = _lib.FLAC__stream_decoder_init_stream( @@ -201,7 +203,10 @@ def process(self, data: bytes): Args: data (bytes): Bytes of FLAC data """ + self._lock.acquire() self._buffer.append(data) + self._lock.release() + self._event.set() def finish(self): """ @@ -226,6 +231,7 @@ def finish(self): # Instruct the decoder to finish up and wait until it is done # -------------------------------------------------------------- self._done = True + self._event.set() self._thread.join(timeout=3) super().finish() if self._error: @@ -316,13 +322,11 @@ def _read_callback(_decoder, """ decoder = _ffi.from_handle(client_data) - while len(decoder._buffer) == 0 and not (decoder._error or decoder._done): - # ---------------------------------------------------------- - # Wait until there is something in the buffer, or an error - # occurs, or the end of the stream is reached. - # ---------------------------------------------------------- - time.sleep(0.01) - + # ---------------------------------------------------------- + # Wait until there is something in the buffer, or an error + # occurs, or the end of the stream is reached. + # ---------------------------------------------------------- + decoder._event.wait() if decoder._error: # ---------------------------------------------------------- # If an error has been issued via the error callback, then @@ -345,16 +349,28 @@ def _read_callback(_decoder, data = bytes() maximum_bytes = int(num_bytes[0]) if len(decoder._buffer[0]) <= maximum_bytes: + decoder._lock.acquire() data = decoder._buffer.popleft() + decoder._lock.release() maximum_bytes -= len(data) if len(decoder._buffer) > 0 and len(decoder._buffer[0]) > maximum_bytes: + decoder._lock.acquire() data += decoder._buffer[0][0:maximum_bytes] decoder._buffer[0] = decoder._buffer[0][maximum_bytes:] + decoder._lock.release() actual_bytes = len(data) num_bytes[0] = actual_bytes _ffi.memmove(byte_buffer, data, actual_bytes) + + # -------------------------------------------------------------- + # If there is no more data to process from the buffer, then + # clear the event, the thread will await more data to process. + # -------------------------------------------------------------- + if len(decoder._buffer) == 0 or (len(decoder._buffer) > 0 and len(decoder._buffer[0]) == 0): + decoder._event.clear() + return _lib.FLAC__STREAM_DECODER_READ_STATUS_CONTINUE @@ -452,3 +468,4 @@ def _error_callback(_decoder, _lib.FLAC__StreamDecoderErrorStatusString[status]).decode() decoder.logger.error(f'Error in libFLAC decoder: {message}') decoder._error = message + decoder._event.set() From 06987118f57cd0a30ec0b9c29328fb2f1ae579d9 Mon Sep 17 00:00:00 2001 From: Joe Todd Date: Wed, 10 Apr 2024 17:44:21 +0100 Subject: [PATCH 6/8] Add oneshot decoder --- CHANGELOG.rst | 4 +- pyflac/__init__.py | 6 ++- pyflac/decoder.py | 96 +++++++++++++++++++++++++++++++++++++++---- tests/test_decoder.py | 27 ++++++++++++ 4 files changed, 121 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 634b67a..cd9b68a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,9 +3,11 @@ pyFLAC Changelog **v3.0.0** -* Fixed bug in the shutdown behaviour of the decoder (see #22 and #23). +* Fixed bug in the shutdown behaviour of the `StreamDecoder` (see #22 and #23). * Automatically detect bit depth of input data in the `FileEncoder`, and raise an error if not 16-bit or 32-bit PCM (see #24). +* Added a new `OneShotDecoder` to decode a buffer of FLAC data in a single + blocking operation, without the use of threads. Courtesy of @GOAE. **v2.2.0** diff --git a/pyflac/__init__.py b/pyflac/__init__.py index fb72601..dd02e70 100644 --- a/pyflac/__init__.py +++ b/pyflac/__init__.py @@ -4,13 +4,13 @@ # # pyFLAC # -# Copyright (c) 2020-2021, Sonos, Inc. +# Copyright (c) 2020-2024, Sonos, Inc. # All rights reserved. # # ------------------------------------------------------------------------------ __title__ = 'pyFLAC' -__version__ = '2.2.0' +__version__ = '3.0.0' __all__ = [ 'StreamEncoder', 'FileEncoder', @@ -19,6 +19,7 @@ 'EncoderProcessException', 'StreamDecoder', 'FileDecoder', + 'OneShotDecoder', 'DecoderState', 'DecoderInitException', 'DecoderProcessException' @@ -55,6 +56,7 @@ from .decoder import ( StreamDecoder, FileDecoder, + OneShotDecoder, DecoderState, DecoderInitException, DecoderProcessException diff --git a/pyflac/decoder.py b/pyflac/decoder.py index 625b957..ae6e572 100644 --- a/pyflac/decoder.py +++ b/pyflac/decoder.py @@ -122,6 +122,9 @@ class StreamDecoder(_Decoder): blocks of raw uncompressed audio is passed back to the user via the `callback`. + The `finish` method must be called at the end of the decoding process, + otherwise the processing thread will be left running. + Args: write_callback (fn): Function to call when there is uncompressed audio data ready, see the example below for more information. @@ -232,7 +235,7 @@ def finish(self): # -------------------------------------------------------------- self._done = True self._event.set() - self._thread.join(timeout=3) + self._thread.join() super().finish() if self._error: raise DecoderProcessException(self._error) @@ -310,6 +313,84 @@ def _write_callback(self, data: np.ndarray, sample_rate: int, num_channels: int, self.__output.write(data) +class OneShotDecoder(_Decoder): + """ + A pyFLAC one-shot decoder converts a buffer of FLAC encoded + bytes back to raw audio data. Unlike the `StreamDecoder` class, + the one-shot decoder operates on a single block of data, and + runs in a blocking manner, as opposed to in a background thread. + + The compressed data is passed in via the constructor, and + blocks of raw uncompressed audio is passed back to the user via + the `callback`. + + Args: + write_callback (fn): Function to call when there is uncompressed + audio data ready, see the example below for more information. + buffer (bytes): The FLAC encoded audio data + + Examples: + An example callback which writes the audio data to file + using SoundFile. + + .. code-block:: python + :linenos: + + import soundfile as sf + + def callback(self, + audio: np.ndarray, + sample_rate: int, + num_channels: int, + num_samples: int): + + # ------------------------------------------------------ + # Note: num_samples is the number of samples per channel + # ------------------------------------------------------ + if self.output is None: + self.output = sf.SoundFile( + 'output.wav', mode='w', channels=num_channels, + samplerate=sample_rate + ) + self.output.write(audio) + + Raises: + DecoderInitException: If initialisation of the decoder fails + """ + def __init__(self, + write_callback: Callable[[np.ndarray, int, int, int], None], + buffer: bytes): + super().__init__() + self._done = False + self._buffer = deque() + self._buffer.append(buffer) + self._event = threading.Event() + self._event.set() + self._lock = threading.Lock() + self.write_callback = write_callback + + rc = _lib.FLAC__stream_decoder_init_stream( + self._decoder, + _lib._read_callback, + _ffi.NULL, + _ffi.NULL, + _ffi.NULL, + _ffi.NULL, + _lib._write_callback, + _ffi.NULL, + _lib._error_callback, + self._decoder_handle + ) + if rc != _lib.FLAC__STREAM_DECODER_INIT_STATUS_OK: + raise DecoderInitException(rc) + + while len(self._buffer) > 0: + _lib.FLAC__stream_decoder_process_single(self._decoder) + + self._done = True + super().finish() + + @_ffi.def_extern(error=_lib.FLAC__STREAM_DECODER_READ_STATUS_ABORT) def _read_callback(_decoder, byte_buffer, @@ -348,21 +429,14 @@ def _read_callback(_decoder, # -------------------------------------------------------------- data = bytes() maximum_bytes = int(num_bytes[0]) + decoder._lock.acquire() if len(decoder._buffer[0]) <= maximum_bytes: - decoder._lock.acquire() data = decoder._buffer.popleft() - decoder._lock.release() maximum_bytes -= len(data) if len(decoder._buffer) > 0 and len(decoder._buffer[0]) > maximum_bytes: - decoder._lock.acquire() data += decoder._buffer[0][0:maximum_bytes] decoder._buffer[0] = decoder._buffer[0][maximum_bytes:] - decoder._lock.release() - - actual_bytes = len(data) - num_bytes[0] = actual_bytes - _ffi.memmove(byte_buffer, data, actual_bytes) # -------------------------------------------------------------- # If there is no more data to process from the buffer, then @@ -370,7 +444,11 @@ def _read_callback(_decoder, # -------------------------------------------------------------- if len(decoder._buffer) == 0 or (len(decoder._buffer) > 0 and len(decoder._buffer[0]) == 0): decoder._event.clear() + decoder._lock.release() + actual_bytes = len(data) + num_bytes[0] = actual_bytes + _ffi.memmove(byte_buffer, data, actual_bytes) return _lib.FLAC__STREAM_DECODER_READ_STATUS_CONTINUE diff --git a/tests/test_decoder.py b/tests/test_decoder.py index 7998efc..61b2d43 100644 --- a/tests/test_decoder.py +++ b/tests/test_decoder.py @@ -20,6 +20,7 @@ from pyflac import ( FileDecoder, StreamDecoder, + OneShotDecoder, DecoderState, DecoderInitException, DecoderProcessException @@ -150,5 +151,31 @@ def test_process_32_bit_file(self): self.assertIsNotNone(self.decoder.process()) +class TestOneShotDecoder(unittest.TestCase): + """ + Test suite for the one-shot decoder class. + """ + def setUp(self): + self.decoder = None + self.write_callback_called = False + self.tests_path = pathlib.Path(__file__).parent.absolute() + + def _write_callback(self, data, rate, channels, samples): + assert isinstance(data, np.ndarray) + assert isinstance(rate, int) + assert isinstance(channels, int) + assert isinstance(samples, int) + self.write_callback_called = True + + def test_process(self): + """ Test that FLAC data can be decoded """ + test_path = self.tests_path / 'data/stereo.flac' + with open(test_path, 'rb') as flac: + test_data = flac.read() + + self.decoder = OneShotDecoder(write_callback=self._write_callback, buffer=test_data) + self.assertTrue(self.write_callback_called) + + if __name__ == '__main__': unittest.main(failfast=True) From 78a2a64cb6a4e8c03e20e887b621d8e57002be20 Mon Sep 17 00:00:00 2001 From: Joe Todd Date: Wed, 10 Apr 2024 17:51:32 +0100 Subject: [PATCH 7/8] Update docs --- docs/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 2108372..0cec563 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -68,6 +68,8 @@ data directly from a file or process in real-time. :undoc-members: :inherited-members: +.. autoclass:: pyflac.OneShotDecoder + State ----- From 034410759d848ec968effe78f96f97745225bb4c Mon Sep 17 00:00:00 2001 From: Joe Todd Date: Thu, 11 Apr 2024 08:51:16 +0100 Subject: [PATCH 8/8] Add official support for Python 3.12 --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 14 +++++++------- pyproject.toml | 1 + tox.ini | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 38920b6..1813b15 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: flake8: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Lint the Python code uses: TrueBrain/actions-flake8@master with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a5d095b..7c22ea9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,22 +15,22 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install dependencies run: | sudo apt-get update -y sudo apt-get install libsndfile1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install coverage + pip install coverage setuptools - name: Run tests run: coverage run setup.py test - name: Run coveralls @@ -42,9 +42,9 @@ jobs: macos: runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.11 - name: Install dependencies @@ -58,7 +58,7 @@ jobs: windows: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - name: Check install diff --git a/pyproject.toml b/pyproject.toml index ece5d91..23a63f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Multimedia :: Sound/Audio", ] dependencies = ["cffi>=1.4.0", "numpy>=1.22", "SoundFile>=0.11"] diff --git a/tox.ini b/tox.ini index 7d87a70..9dfabc9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38, py39, py310, py311 +envlist = py38, py39, py310, py311, py312 [testenv] deps = pytest==7.4.0