Skip to content

Commit

Permalink
Merge pull request #8 from ali1234/things
Browse files Browse the repository at this point in the history
Implement callbacks and streamable subset option
  • Loading branch information
joetoddsonos authored Jun 9, 2021
2 parents 19b3322 + c5c32f7 commit 779a6ac
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 56 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ dmypy.json
# VSCode
.vscode/

# PyCharm
.idea/

# macOS
.DS_Store

Expand All @@ -143,4 +146,4 @@ dmypy.json
*.flac

# Development
.archive
.archive
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions examples/passthrough.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion examples/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion pyflac/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# ------------------------------------------------------------------------------

__title__ = 'pyFLAC'
__version__ = '1.0.1'
__version__ = '2.0.0'
__all__ = [
'StreamEncoder',
'FileEncoder',
Expand Down
1 change: 1 addition & 0 deletions pyflac/builder/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 6 additions & 6 deletions pyflac/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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),
Expand Down
78 changes: 58 additions & 20 deletions pyflac/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,25 +209,45 @@ 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):
"""
The pyFLAC stream encoder is used for real-time compression of
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.
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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__()

Expand All @@ -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):
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand All @@ -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()
Expand Down
14 changes: 7 additions & 7 deletions tests/test_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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 """
Expand All @@ -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])

Expand Down
Loading

0 comments on commit 779a6ac

Please sign in to comment.