diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 42d0078..987e6e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,9 +31,14 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions + pip install coverage - name: Run tests - run: tox + run: coverage run setup.py test + - name: Run coveralls + uses: AndreMiras/coveralls-python-action@v20201129 + with: + parallel: true + flag-name: unittest macos: runs-on: macos-latest @@ -58,3 +63,12 @@ jobs: submodules: recursive - name: Check install run: python setup.py install + + coverage: + needs: linux + runs-on: ubuntu-latest + steps: + - name: coveralls + uses: AndreMiras/coveralls-python-action@v20201129 + with: + parallel-finished: true diff --git a/.gitignore b/.gitignore index d8ac85b..f9992a0 100644 --- a/.gitignore +++ b/.gitignore @@ -132,6 +132,9 @@ dmypy.json # VSCode .vscode/ +# PyCharm +.idea/ + # macOS .DS_Store @@ -143,4 +146,4 @@ dmypy.json *.flac # Development -.archive \ No newline at end of file +.archive diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cfbbcbb..4361989 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ pyFLAC Changelog ---------------- +**v2.0.0** + +* Added `seek` and `tell` callbacks to `StreamEncoder` +* Renamed the write callbacks from `callback` to `write_callback` for `StreamEncoder` and `StreamDecoder` + **v1.0.0** * Added a `StreamEncoder` to compress raw audio data on-the-fly into a FLAC byte stream diff --git a/README.rst b/README.rst index 47b114d..da334d7 100644 --- a/README.rst +++ b/README.rst @@ -1,15 +1,23 @@ -pyFLAC -====== - -.. image:: https://github.com/Sonos-Inc/pyFLAC/actions/workflows/lint.yml/badge.svg - :target: https://github.com/Sonos-Inc/pyFLAC/actions/workflows/lint.yml -.. image:: https://github.com/Sonos-Inc/pyFLAC/actions/workflows/test.yml/badge.svg - :target: https://github.com/Sonos-Inc/pyFLAC/actions/workflows/test.yml -.. image:: https://github.com/Sonos-Inc/pyFLAC/actions/workflows/build.yml/badge.svg - :target: https://github.com/Sonos-Inc/pyFLAC/actions/workflows/build.yml -.. image:: https://img.shields.io/pypi/pyversions/pysoundio +.. image:: https://raw.githubusercontent.com/sonos/pyFLAC/develop/assets/logo-white-background.jpg + :target: https://pyflac.readthedocs.io + +.. image:: https://github.com/sonos/pyFLAC/actions/workflows/lint.yml/badge.svg + :target: https://github.com/sonos/pyFLAC/actions/workflows/lint.yml +.. image:: https://github.com/sonos/pyFLAC/actions/workflows/test.yml/badge.svg + :target: https://github.com/sonos/pyFLAC/actions/workflows/test.yml +.. image:: https://github.com/sonos/pyFLAC/actions/workflows/build.yml/badge.svg + :target: https://github.com/sonos/pyFLAC/actions/workflows/build.yml +.. image:: https://coveralls.io/repos/github/sonos/pyFLAC/badge.svg + :target: https://coveralls.io/github/sonos/pyFLAC +.. image:: https://readthedocs.org/projects/pyflac/badge + :target: https://pyflac.readthedocs.io/en/latest/ +.. image:: https://badge.fury.io/py/pyFLAC.svg + :target: https://badge.fury.io/py/pyFLAC +.. image:: https://img.shields.io/pypi/pyversions/pyFLAC :target: https://pypi.org/project/pyFLAC +------------------------------------------------------------------- + A simple Pythonic interface for `libFLAC `_. FLAC stands for Free Lossless Audio Codec, an audio format similar to MP3, but lossless, diff --git a/assets/icon-black.png b/assets/icon-black.png new file mode 100644 index 0000000..8efdb4d Binary files /dev/null and b/assets/icon-black.png differ diff --git a/assets/icon-white.png b/assets/icon-white.png new file mode 100644 index 0000000..8cccc9d Binary files /dev/null and b/assets/icon-white.png differ diff --git a/assets/logo-black.png b/assets/logo-black.png new file mode 100644 index 0000000..d7b6cd9 Binary files /dev/null and b/assets/logo-black.png differ diff --git a/assets/logo-white-background.jpg b/assets/logo-white-background.jpg new file mode 100644 index 0000000..0516904 Binary files /dev/null and b/assets/logo-white-background.jpg differ diff --git a/assets/logo-white-wide.jpg b/assets/logo-white-wide.jpg new file mode 100644 index 0000000..74c0259 Binary files /dev/null and b/assets/logo-white-wide.jpg differ diff --git a/assets/logo-white.png b/assets/logo-white.png new file mode 100644 index 0000000..4cc9fd3 Binary files /dev/null and b/assets/logo-white.png differ diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..9f48c12 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,3 @@ +.wy-nav-content { + max-width: 960px !important; +} \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 1f51ff4..be843be 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,19 +12,25 @@ # import os import sys + sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('..')) - # -- Project information ----------------------------------------------------- project = 'pyFLAC' copyright = '2021, Sonos, Inc' author = 'Joe Todd' - # -- General configuration --------------------------------------------------- +autodoc_mock_imports = [ + 'numpy', + 'soundfile', + 'pyflac._encoder', + 'pyflac._decoder' +] + # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.3' # for napoleon @@ -83,10 +89,14 @@ # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - 'collapse_navigation': False + 'collapse_navigation': False, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] + +html_css_files = [ + 'custom.css', +] \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 2543925..de060f6 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ +cffi Sphinx sphinx-rtd-theme \ No newline at end of file diff --git a/examples/passthrough.py b/examples/passthrough.py index 851ef7e..341f5d4 100755 --- a/examples/passthrough.py +++ b/examples/passthrough.py @@ -32,13 +32,13 @@ def __init__(self, args): self.data, self.sr = sf.read(args.input_file, dtype='int16', always_2d=True) self.encoder = pyflac.StreamEncoder( - callback=self.encoder_callback, + write_callback=self.encoder_callback, sample_rate=self.sr, blocksize=args.block_size, ) self.decoder = pyflac.StreamDecoder( - callback=self.decoder_callback + write_callback=self.decoder_callback ) def encode(self): diff --git a/examples/stream.py b/examples/stream.py index 526a289..03c61ae 100755 --- a/examples/stream.py +++ b/examples/stream.py @@ -35,7 +35,7 @@ def __init__(self, args, stream): self.num_channels = stream.channels self.sample_size = stream.samplesize - self.encoder = pyflac.StreamEncoder(callback=self.encoder_callback, + self.encoder = pyflac.StreamEncoder(write_callback=self.encoder_callback, sample_rate=int(stream.samplerate), compression_level=args.compression_level) diff --git a/pyflac/__init__.py b/pyflac/__init__.py index 01190a1..4c42cfd 100644 --- a/pyflac/__init__.py +++ b/pyflac/__init__.py @@ -10,7 +10,7 @@ # ------------------------------------------------------------------------------ __title__ = 'pyFLAC' -__version__ = '1.0.0-beta.2' +__version__ = '2.0.0' __all__ = [ 'StreamEncoder', 'FileEncoder', diff --git a/pyflac/builder/encoder.py b/pyflac/builder/encoder.py index b622b3a..0c894f0 100644 --- a/pyflac/builder/encoder.py +++ b/pyflac/builder/encoder.py @@ -284,6 +284,7 @@ FLAC__bool FLAC__stream_encoder_set_max_residual_partition_order(FLAC__StreamEncoder *encoder, uint32_t value); FLAC__bool FLAC__stream_encoder_set_rice_parameter_search_dist(FLAC__StreamEncoder *encoder, uint32_t value); FLAC__bool FLAC__stream_encoder_set_total_samples_estimate(FLAC__StreamEncoder *encoder, FLAC__uint64 value); +FLAC__bool FLAC__stream_encoder_set_streamable_subset(FLAC__StreamEncoder *encoder, FLAC__bool value); // GETTERS FLAC__StreamEncoderState FLAC__stream_encoder_get_state(const FLAC__StreamEncoder *encoder); diff --git a/pyflac/decoder.py b/pyflac/decoder.py index cb096a7..d58dabf 100644 --- a/pyflac/decoder.py +++ b/pyflac/decoder.py @@ -122,7 +122,7 @@ class StreamDecoder(_Decoder): the `callback`. Args: - callback (fn): Function to call when there is uncompressed + write_callback (fn): Function to call when there is uncompressed audio data ready, see the example below for more information. Examples: @@ -154,12 +154,12 @@ def callback(self, DecoderInitException: If initialisation of the decoder fails """ def __init__(self, - callback: Callable[[np.ndarray, int, int, int], None]): + write_callback: Callable[[np.ndarray, int, int, int], None]): super().__init__() self._done = False self._buffer = deque() - self.callback = callback + self.write_callback = write_callback rc = _lib.FLAC__stream_decoder_init_stream( self._decoder, @@ -250,7 +250,7 @@ def __init__(self, super().__init__() self.__output = None - self.callback = self._callback + self.write_callback = self._write_callback if output_file: self.__output_file = output_file else: @@ -291,7 +291,7 @@ def process(self) -> Tuple[np.ndarray, int]: self.__output.close() return sf.read(str(self.__output_file), always_2d=True) - def _callback(self, data: np.ndarray, sample_rate: int, num_channels: int, num_samples: int): + def _write_callback(self, data: np.ndarray, sample_rate: int, num_channels: int, num_samples: int): """ Internal callback to write the decoded data to a WAV file. """ @@ -418,7 +418,7 @@ def _write_callback(_decoder, np.frombuffer(cbuffer, dtype='int32').astype(np.int16) ) output = np.column_stack(channels) - decoder.callback( + decoder.write_callback( output, int(frame.header.sample_rate), int(frame.header.channels), diff --git a/pyflac/encoder.py b/pyflac/encoder.py index e53de0e..932ed37 100644 --- a/pyflac/encoder.py +++ b/pyflac/encoder.py @@ -209,6 +209,19 @@ def _compression_level(self) -> int: def _compression_level(self, value: int): _lib.FLAC__stream_encoder_set_compression_level(self._encoder, value) + @property + def _streamable_subset(self) -> bool: + """ + bool: Property to get/set the streamable subset setting. + If true, the encoder will comply with the Subset and will check the settings during + init. If false, the settings may take advantage of the full range that the format allows. + """ + return _lib.FLAC__stream_encoder_get_streamable_subset(self._encoder) + + @_streamable_subset.setter + def _streamable_subset(self, value: bool): + _lib.FLAC__stream_encoder_set_streamable_subset(self._encoder, value) + class StreamEncoder(_Encoder): """ @@ -216,18 +229,25 @@ class StreamEncoder(_Encoder): raw audio data. Raw audio data is passed in via the `process` method, and chunks - of compressed data is passed back to the user via the `callback`. + of compressed data is passed back to the user via the `write_callback`. Args: sample_rate (int): The raw audio sample rate (Hz) - callback (fn): Function to call when there is compressed + write_callback (fn): Function to call when there is compressed data ready, see the example below for more information. + seek_callback (fn): Optional function to call when the encoder + wants to seek within the output file. + tell_callback (fn): Optional function to call when the encoder + wants to find the current position within the output file. compression_level (int): The compression level parameter that varies from 0 (fastest) to 8 (slowest). The default setting is 5, see https://en.wikipedia.org/wiki/FLAC for more details. 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. + 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. verify (bool): If `True`, the encoder will verify its own encoded output by feeding it through an internal decoder and comparing the original signal against the decoded signal. @@ -236,17 +256,17 @@ class StreamEncoder(_Encoder): encoding process by the extra time required for decoding and comparison. Examples: - An example callback which adds the encoded data to a queue for + An example write callback which adds the encoded data to a queue for later processing. .. code-block:: python :linenos: - def callback(self, - buffer: bytes, - num_bytes: int, - num_samples: int, - current_frame: int): + def write_callback(self, + buffer: bytes, + num_bytes: int, + num_samples: int, + current_frame: int): if num_samples == 0: # If there are no samples in the encoded data, this is # a FLAC header. The header data will arrive in several @@ -262,26 +282,34 @@ def callback(self, """ def __init__(self, sample_rate: int, - callback: Callable[[bytes, int, int, int], None], + write_callback: Callable[[bytes, int, int, int], None], + seek_callback: Callable[[int], None] = None, + tell_callback: Callable[[], int] = None, + metadata_callback: Callable[[int], None] = None, compression_level: int = 5, blocksize: int = 0, + streamable_subset: bool = True, verify: bool = False): super().__init__() - self.callback = callback + self.write_callback = write_callback + self.seek_callback = seek_callback + self.tell_callback = tell_callback + self.metadata_callback = metadata_callback self._sample_rate = sample_rate self._blocksize = blocksize self._compression_level = compression_level + self._streamable_subset = streamable_subset self._verify = verify def _init(self): rc = _lib.FLAC__stream_encoder_init_stream( self._encoder, _lib._write_callback, - _ffi.NULL, - _ffi.NULL, - _ffi.NULL, + _lib._seek_callback if self.seek_callback else _ffi.NULL, + _lib._tell_callback if self.tell_callback else _ffi.NULL, + _lib._metadata_callback if self.metadata_callback else _ffi.NULL, self._encoder_handle ) if rc != _lib.FLAC__STREAM_ENCODER_INIT_STATUS_OK: @@ -305,7 +333,10 @@ 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. - verify (bool): If `True`, the encoder will verify its own + 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. + verify (bool): If `True`, the encoder will verify it's own encoded output by feeding it through an internal decoder and comparing the original signal against the decoded signal. If a mismatch occurs, the `process` method will raise a @@ -320,6 +351,7 @@ def __init__(self, output_file: Path = None, compression_level: int = 5, blocksize: int = 0, + streamable_subset: bool = True, verify: bool = False): super().__init__() @@ -333,6 +365,7 @@ def __init__(self, self._sample_rate = sample_rate self._blocksize = blocksize self._compression_level = compression_level + self._streamable_subset = streamable_subset self._verify = verify def _init(self): @@ -386,7 +419,7 @@ def _write_callback(_encoder, """ encoder = _ffi.from_handle(client_data) buffer = bytes(_ffi.buffer(byte_buffer, num_bytes)) - encoder.callback( + encoder.write_callback( buffer, num_bytes, num_samples, @@ -395,18 +428,22 @@ def _write_callback(_encoder, return _lib.FLAC__STREAM_ENCODER_WRITE_STATUS_OK -@_ffi.def_extern() +@_ffi.def_extern(error=_lib.FLAC__STREAM_ENCODER_SEEK_STATUS_ERROR) def _seek_callback(_encoder, absolute_byte_offset, client_data): - raise NotImplementedError + encoder = _ffi.from_handle(client_data) + encoder.seek_callback(absolute_byte_offset) + return _lib.FLAC__STREAM_ENCODER_SEEK_STATUS_OK -@_ffi.def_extern() +@_ffi.def_extern(error=_lib.FLAC__STREAM_ENCODER_TELL_STATUS_ERROR) def _tell_callback(_encoder, absolute_byte_offset, client_data): - raise NotImplementedError + encoder = _ffi.from_handle(client_data) + absolute_byte_offset[0] = encoder.tell_callback() + return _lib.FLAC__STREAM_ENCODER_TELL_STATUS_OK @_ffi.def_extern() @@ -420,7 +457,8 @@ def _metadata_callback(_encoder, with the correct statistics after encoding (like minimum/maximum frame size and total samples). """ - raise NotImplementedError + encoder = _ffi.from_handle(client_data) + encoder.metadata_callback(metadata) @_ffi.def_extern() diff --git a/setup.py b/setup.py index 042bfb4..31b9ff0 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def read(fname): author_email='joe.todd@sonos.com', license='Apache License 2.0', url='http://pyflac.readthedocs.io/en/latest/', - download_url='https://github.com/sonos/pyFLAC/archive/' + __version__ + '.tar.gz', + download_url='https://github.com/sonos/pyFLAC/archive/v' + __version__ + '.tar.gz', packages=find_packages(), include_package_data=True, setup_requires=['cffi>=1.4.0'], @@ -53,7 +53,7 @@ def read(fname): ], }, classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: Apache Software License', diff --git a/tests/test_decoder.py b/tests/test_decoder.py index 0be5cea..8c4fbe4 100644 --- a/tests/test_decoder.py +++ b/tests/test_decoder.py @@ -45,22 +45,22 @@ class TestStreamDecoder(unittest.TestCase): """ def setUp(self): self.decoder = None - self.callback_called = False + self.write_callback_called = False self.tests_path = pathlib.Path(__file__).parent.absolute() - def _callback(self, data, rate, channels, samples): + 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.callback_called = True + self.write_callback_called = True def test_process_invalid_data(self): """ Test that processing invalid data raises an exception """ test_data = bytearray(os.urandom(100000)) with self.assertRaises(DecoderProcessException): - self.decoder = StreamDecoder(callback=self._callback) + self.decoder = StreamDecoder(write_callback=self._write_callback) self.decoder.process(test_data) self.decoder.finish() @@ -70,12 +70,12 @@ def test_process(self): with open(test_path, 'rb') as flac: test_data = flac.read() - self.decoder = StreamDecoder(callback=self._callback) + self.decoder = StreamDecoder(write_callback=self._write_callback) time.sleep(0.05) self.decoder.process(test_data) self.decoder.finish() - self.assertTrue(self.callback_called) + self.assertTrue(self.write_callback_called) def test_process_blocks(self): """ Test that FLAC data can be decoded in blocks """ @@ -85,7 +85,7 @@ def test_process_blocks(self): test_data = flac.read() data_length = len(test_data) - self.decoder = StreamDecoder(callback=self._callback) + self.decoder = StreamDecoder(write_callback=self._write_callback) for i in range(0, data_length - blocksize, blocksize): self.decoder.process(test_data[i:i + blocksize]) diff --git a/tests/test_encoder.py b/tests/test_encoder.py index 8f5dd71..33140a6 100644 --- a/tests/test_encoder.py +++ b/tests/test_encoder.py @@ -65,6 +65,12 @@ def test_blocksize(self): self.encoder._blocksize = test_blocksize self.assertEqual(self.encoder._blocksize, test_blocksize) + def test_streamable_subset(self): + """ Test that the streamable_subset setter returns the same value from libFLAC """ + test_streamable_subset = False + self.encoder._streamable_subset = test_streamable_subset + self.assertEqual(self.encoder._streamable_subset, test_streamable_subset) + def test_compression_level(self): """ Test that the compression level setter returns the same value from libFLAC """ test_compression_level = 8 @@ -86,12 +92,15 @@ class TestStreamEncoder(unittest.TestCase): Test suite for the stream encoder class """ def setUp(self): - self.callback_called = False + self.write_callback_called = False + self.seek_callback_called = False + self.tell_callback_called = False + self.metadata_callback_called = False self.encoder = None self.default_kwargs = { 'sample_rate': DEFAULT_SAMPLE_RATE, 'blocksize': DEFAULT_BLOCKSIZE, - 'callback': self._callback, + 'write_callback': self._write_callback, 'verify': True } @@ -99,7 +108,7 @@ def tearDown(self): if self.encoder: self.encoder.finish() - def _callback(self, + def _write_callback(self, buffer: bytes, num_bytes: int, num_samples: int, @@ -108,40 +117,90 @@ def _callback(self, assert isinstance(num_bytes, int) assert isinstance(num_samples, int) assert isinstance(current_frame, int) - self.callback_called = True + self.write_callback_called = True + + def _seek_callback(self, offset: int): + assert isinstance(offset, int) + self.seek_callback_called = True + + def _tell_callback(self): + self.tell_callback_called = True + return 0 + + def _metadata_callback(self, metadata): + self.metadata_callback_called = True def test_invalid_sample_rate(self): - """ Test than an exception is raised if given an invalid sample rate """ - self.encoder = StreamEncoder(sample_rate=1000000, callback=self._callback) - with self.assertRaises(EncoderInitException) as err: + self.default_kwargs['sample_rate'] = 1000000 + self.encoder = StreamEncoder(**self.default_kwargs) + with self.assertRaisesRegex(EncoderInitException, 'FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_SAMPLE_RATE'): self.encoder._init() - self.assertEqual(str(err), 'FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_SAMPLE_RATE') def test_invalid_blocksize(self): """ Test than an exception is raised if given an invalid block size """ - self.encoder = StreamEncoder( - sample_rate=DEFAULT_SAMPLE_RATE, - blocksize=1000000, - callback=self._callback - ) - with self.assertRaises(EncoderInitException) as err: + self.default_kwargs['blocksize'] = 1000000 + self.encoder = StreamEncoder(**self.default_kwargs) + with self.assertRaisesRegex(EncoderInitException, 'FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BLOCK_SIZE'): self.encoder._init() - self.assertEqual(str(err), 'FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BLOCK_SIZE') + + def test_blocksize_streamable_subset(self): + """ Test that an exception is raised if blocksize is outside the streamable subset and streamable is unset """ + self.default_kwargs['blocksize'] = 65535 + self.encoder = StreamEncoder(**self.default_kwargs) + with self.assertRaisesRegex(EncoderInitException, 'FLAC__STREAM_ENCODER_INIT_STATUS_NOT_STREAMABLE'): + self.encoder._init() + + def test_blocksize_lax(self): + """ Test that an exception is not raised on large blocks when in lax mode """ + self.default_kwargs['blocksize'] = 65535 + self.default_kwargs['streamable_subset'] = False + self.encoder = StreamEncoder(**self.default_kwargs) + self.encoder._init() def test_process_mono(self): """ Test that an array of int16 mono samples can be processed """ self.encoder = StreamEncoder(**self.default_kwargs) test_samples = np.random.rand(DEFAULT_BLOCKSIZE, 1).astype('int16') self.encoder.process(test_samples) - self.assertTrue(self.callback_called) + self.encoder.finish() + self.assertTrue(self.write_callback_called) def test_process_stereo(self): """ Test that an array of int16 stereo samples can be processed """ self.encoder = StreamEncoder(**self.default_kwargs) test_samples = np.random.rand(DEFAULT_BLOCKSIZE, 2).astype('int16') self.encoder.process(test_samples) - self.assertTrue(self.callback_called) + self.encoder.finish() + self.assertTrue(self.write_callback_called) + def test_seek_tell(self): + """ Test that seek and tell callbacks are used """ + self.default_kwargs['seek_callback'] = self._seek_callback + self.default_kwargs['tell_callback'] = self._tell_callback + self.encoder = StreamEncoder(**self.default_kwargs) + test_samples = np.random.rand(DEFAULT_BLOCKSIZE, 1).astype('int16') + self.encoder.process(test_samples) + self.encoder.finish() + self.assertTrue(self.write_callback_called) + self.assertTrue(self.seek_callback_called) + self.assertTrue(self.tell_callback_called) + + def test_seek_only(self): + """ Supplying only one of seek or tell is not allowed """ + self.default_kwargs['seek_callback'] = self._seek_callback + self.encoder = StreamEncoder(**self.default_kwargs) + with self.assertRaisesRegex(EncoderInitException, 'FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_CALLBACKS'): + self.encoder._init() + + def test_metadata(self): + """ Test that the metadata callback is called """ + self.default_kwargs['metadata_callback'] = self._metadata_callback + self.encoder = StreamEncoder(**self.default_kwargs) + test_samples = np.random.rand(DEFAULT_BLOCKSIZE, 1).astype('int16') + self.encoder.process(test_samples) + self.encoder.finish() + self.assertTrue(self.write_callback_called) + self.assertTrue(self.metadata_callback_called) class TestFileEncoder(unittest.TestCase): """ @@ -161,9 +220,23 @@ def test_invalid_blocksize(self): """ Test than an exception is raised if given an invalid block size """ self.default_kwargs['blocksize'] = 1000000 self.encoder = FileEncoder(**self.default_kwargs) - with self.assertRaises(EncoderInitException): + with self.assertRaisesRegex(EncoderInitException, 'FLAC__STREAM_ENCODER_INIT_STATUS_INVALID_BLOCK_SIZE'): + self.encoder._init() + + def test_blocksize_streamable_subset(self): + """ Test that an exception is raised if blocksize is outside the streamable subset """ + self.default_kwargs['blocksize'] = 65535 + self.encoder = FileEncoder(**self.default_kwargs) + with self.assertRaisesRegex(EncoderInitException, 'FLAC__STREAM_ENCODER_INIT_STATUS_NOT_STREAMABLE'): self.encoder._init() + def test_blocksize_lax(self): + """ Test that an exception is not raised on large blocks when in lax mode """ + self.default_kwargs['blocksize'] = 65535 + self.default_kwargs['streamable_subset'] = False + self.encoder = FileEncoder(**self.default_kwargs) + self.encoder._init() + def test_state(self): """ Test that the initial state is ok """ self.encoder = FileEncoder(**self.default_kwargs)