Spaces:
Runtime error
Runtime error
import pyogg | |
import os.path | |
import warnings | |
from abc import abstractmethod | |
from ctypes import c_void_p, POINTER, c_int, pointer, cast, c_char, c_char_p, CFUNCTYPE, c_ubyte | |
from ctypes import memmove, create_string_buffer, byref | |
from pyglet.media import StreamingSource | |
from pyglet.media.codecs import AudioFormat, AudioData, MediaDecoder, StaticSource | |
from pyglet.util import debug_print, DecodeException | |
_debug = debug_print('Debug PyOgg codec') | |
if _debug: | |
if not pyogg.PYOGG_OGG_AVAIL and not pyogg.PYOGG_VORBIS_AVAIL and not pyogg.PYOGG_VORBIS_FILE_AVAIL: | |
warnings.warn("PyOgg determined the ogg/vorbis libraries were not available.") | |
if not pyogg.PYOGG_FLAC_AVAIL: | |
warnings.warn("PyOgg determined the flac library was not available.") | |
if not pyogg.PYOGG_OPUS_AVAIL and not pyogg.PYOGG_OPUS_FILE_AVAIL: | |
warnings.warn("PyOgg determined the opus libraries were not available.") | |
if not ( | |
pyogg.PYOGG_OGG_AVAIL and not pyogg.PYOGG_VORBIS_AVAIL and not pyogg.PYOGG_VORBIS_FILE_AVAIL) and ( | |
not pyogg.PYOGG_OPUS_AVAIL and not pyogg.PYOGG_OPUS_FILE_AVAIL) and not pyogg.PYOGG_FLAC_AVAIL: | |
raise ImportError("PyOgg determined no supported libraries were found") | |
# Some monkey patching PyOgg for FLAC. | |
if pyogg.PYOGG_FLAC_AVAIL: | |
# Original in PyOgg: FLAC__StreamDecoderEofCallback = CFUNCTYPE(FLAC__bool, POINTER(FLAC__StreamDecoder), c_void_p) | |
# FLAC__bool is not valid for this return type (at least for ctypes). Needs to be an int or an error occurs. | |
FLAC__StreamDecoderEofCallback = CFUNCTYPE(c_int, POINTER(pyogg.flac.FLAC__StreamDecoder), c_void_p) | |
# Override explicits with c_void_p, so we can support non-seeking FLAC's (CFUNCTYPE does not accept None). | |
pyogg.flac.libflac.FLAC__stream_decoder_init_stream.restype = pyogg.flac.FLAC__StreamDecoderInitStatus | |
pyogg.flac.libflac.FLAC__stream_decoder_init_stream.argtypes = [POINTER(pyogg.flac.FLAC__StreamDecoder), | |
pyogg.flac.FLAC__StreamDecoderReadCallback, | |
c_void_p, # Seek | |
c_void_p, # Tell | |
c_void_p, # Length | |
c_void_p, # EOF | |
pyogg.flac.FLAC__StreamDecoderWriteCallback, | |
pyogg.flac.FLAC__StreamDecoderMetadataCallback, | |
pyogg.flac.FLAC__StreamDecoderErrorCallback, | |
c_void_p] | |
def metadata_callback(self, decoder, metadata, client_data): | |
self.bits_per_sample = metadata.contents.data.stream_info.bits_per_sample # missing from pyogg | |
self.total_samples = metadata.contents.data.stream_info.total_samples | |
self.channels = metadata.contents.data.stream_info.channels | |
self.frequency = metadata.contents.data.stream_info.sample_rate | |
# Monkey patch metadata callback to include bits per sample as FLAC may rarely deviate from 16 bit. | |
pyogg.FlacFileStream.metadata_callback = metadata_callback | |
class MemoryVorbisObject: | |
def __init__(self, file): | |
self.file = file | |
def read_func_cb(ptr, byte_size, size_to_read, datasource): | |
data_size = size_to_read * byte_size | |
data = self.file.read(data_size) | |
read_size = len(data) | |
memmove(ptr, data, read_size) | |
return read_size | |
def seek_func_cb(datasource, offset, whence): | |
pos = self.file.seek(offset, whence) | |
return pos | |
def close_func_cb(datasource): | |
return 0 | |
def tell_func_cb(datasource): | |
return self.file.tell() | |
self.read_func = pyogg.vorbis.read_func(read_func_cb) | |
self.seek_func = pyogg.vorbis.seek_func(seek_func_cb) | |
self.close_func = pyogg.vorbis.close_func(close_func_cb) | |
self.tell_func = pyogg.vorbis.tell_func(tell_func_cb) | |
self.callbacks = pyogg.vorbis.ov_callbacks(self.read_func, self.seek_func, self.close_func, self.tell_func) | |
class UnclosedVorbisFileStream(pyogg.VorbisFileStream): | |
def __del__(self): | |
if self.exists: | |
pyogg.vorbis.ov_clear(byref(self.vf)) | |
self.exists = False | |
def clean_up(self): | |
"""PyOgg calls clean_up on end of data. We may want to loop a sound or replay. Prevent this. | |
Rely on GC (__del__) to clean up objects instead. | |
""" | |
return | |
class UnclosedOpusFileStream(pyogg.OpusFileStream): | |
def __del__(self): | |
self.ptr.contents.value = self.ptr_init | |
del self.ptr | |
if self.of: | |
pyogg.opus.op_free(self.of) | |
def clean_up(self): | |
pass | |
class MemoryOpusObject: | |
def __init__(self, filename, file): | |
self.file = file | |
self.filename = filename | |
def read_func_cb(stream, buffer, size): | |
data = self.file.read(size) | |
read_size = len(data) | |
memmove(buffer, data, read_size) | |
return read_size | |
def seek_func_cb(stream, offset, whence): | |
self.file.seek(offset, whence) | |
return 0 | |
def tell_func_cb(stream): | |
pos = self.file.tell() | |
return pos | |
def close_func_cb(stream): | |
return 0 | |
self.read_func = pyogg.opus.op_read_func(read_func_cb) | |
self.seek_func = pyogg.opus.op_seek_func(seek_func_cb) | |
self.tell_func = pyogg.opus.op_tell_func(tell_func_cb) | |
self.close_func = pyogg.opus.op_close_func(close_func_cb) | |
self.callbacks = pyogg.opus.OpusFileCallbacks(self.read_func, self.seek_func, self.tell_func, self.close_func) | |
class MemoryOpusFileStream(UnclosedOpusFileStream): | |
def __init__(self, filename, file): | |
self.file = file | |
self.memory_object = MemoryOpusObject(filename, file) | |
self._dummy_fileobj = c_void_p() | |
error = c_int() | |
self.read_buffer = create_string_buffer(pyogg.PYOGG_STREAM_BUFFER_SIZE) | |
self.ptr_buffer = cast(self.read_buffer, POINTER(c_ubyte)) | |
self.of = pyogg.opus.op_open_callbacks( | |
self._dummy_fileobj, | |
byref(self.memory_object.callbacks), | |
self.ptr_buffer, | |
0, # Start length | |
byref(error) | |
) | |
if error.value != 0: | |
raise DecodeException( | |
"file-like object: {} couldn't be processed. Error code : {}".format(filename, error.value)) | |
self.channels = pyogg.opus.op_channel_count(self.of, -1) | |
self.pcm_size = pyogg.opus.op_pcm_total(self.of, -1) | |
self.frequency = 48000 | |
self.bfarr_t = pyogg.opus.opus_int16 * (pyogg.PYOGG_STREAM_BUFFER_SIZE * self.channels * 2) | |
self.buffer = cast(pointer(self.bfarr_t()), pyogg.opus.opus_int16_p) | |
self.ptr = cast(pointer(self.buffer), POINTER(c_void_p)) | |
self.ptr_init = self.ptr.contents.value | |
class MemoryVorbisFileStream(UnclosedVorbisFileStream): | |
def __init__(self, path, file): | |
buff = create_string_buffer(pyogg.PYOGG_STREAM_BUFFER_SIZE) | |
self.vf = pyogg.vorbis.OggVorbis_File() | |
self.memory_object = MemoryVorbisObject(file) | |
error = pyogg.vorbis.libvorbisfile.ov_open_callbacks(buff, self.vf, None, 0, self.memory_object.callbacks) | |
if error != 0: | |
raise DecodeException("file couldn't be opened or doesn't exist. Error code : {}".format(error)) | |
info = pyogg.vorbis.ov_info(byref(self.vf), -1) | |
self.channels = info.contents.channels | |
self.frequency = info.contents.rate | |
array = (c_char * (pyogg.PYOGG_STREAM_BUFFER_SIZE * self.channels))() | |
self.buffer_ = cast(pointer(array), c_char_p) | |
self.bitstream = c_int() | |
self.bitstream_pointer = pointer(self.bitstream) | |
self.exists = True | |
class UnclosedFLACFileStream(pyogg.FlacFileStream): | |
def __init__(self, *args, **kw): | |
super().__init__(*args, **kw) | |
self.seekable = True | |
def __del__(self): | |
if self.decoder: | |
pyogg.flac.FLAC__stream_decoder_finish(self.decoder) | |
class MemoryFLACFileStream(UnclosedFLACFileStream): | |
def __init__(self, path, file): | |
self.file = file | |
self.file_size = 0 | |
if getattr(self.file, 'seek', None) and getattr(self.file, 'tell', None): | |
self.seekable = True | |
self.file.seek(0, 2) | |
self.file_size = self.file.tell() | |
self.file.seek(0) | |
else: | |
warnings.warn(f"Warning: {file} file object is not seekable.") | |
self.seekable = False | |
self.decoder = pyogg.flac.FLAC__stream_decoder_new() | |
self.client_data = c_void_p() | |
self.channels = None | |
self.frequency = None | |
self.total_samples = None | |
self.buffer = None | |
self.bytes_written = None | |
self.write_callback_ = pyogg.flac.FLAC__StreamDecoderWriteCallback(self.write_callback) | |
self.metadata_callback_ = pyogg.flac.FLAC__StreamDecoderMetadataCallback(self.metadata_callback) | |
self.error_callback_ = pyogg.flac.FLAC__StreamDecoderErrorCallback(self.error_callback) | |
self.read_callback_ = pyogg.flac.FLAC__StreamDecoderReadCallback(self.read_callback) | |
if self.seekable: | |
self.seek_callback_ = pyogg.flac.FLAC__StreamDecoderSeekCallback(self.seek_callback) | |
self.tell_callback_ = pyogg.flac.FLAC__StreamDecoderTellCallback(self.tell_callback) | |
self.length_callback_ = pyogg.flac.FLAC__StreamDecoderLengthCallback(self.length_callback) | |
self.eof_callback_ = FLAC__StreamDecoderEofCallback(self.eof_callback) | |
else: | |
self.seek_callback_ = None | |
self.tell_callback_ = None | |
self.length_callback_ = None | |
self.eof_callback_ = None | |
init_status = pyogg.flac.libflac.FLAC__stream_decoder_init_stream( | |
self.decoder, | |
self.read_callback_, | |
self.seek_callback_, | |
self.tell_callback_, | |
self.length_callback_, | |
self.eof_callback_, | |
self.write_callback_, | |
self.metadata_callback_, | |
self.error_callback_, | |
self.client_data | |
) | |
if init_status: # error | |
raise DecodeException("An error occurred when trying to open '{}': {}".format( | |
path, pyogg.flac.FLAC__StreamDecoderInitStatusEnum[init_status])) | |
metadata_status = pyogg.flac.FLAC__stream_decoder_process_until_end_of_metadata(self.decoder) | |
if not metadata_status: # error | |
raise DecodeException("An error occured when trying to decode the metadata of {}".format(path)) | |
def read_callback(self, decoder, buffer, size, data): | |
chunk = size.contents.value | |
data = self.file.read(chunk) | |
read_size = len(data) | |
memmove(buffer, data, read_size) | |
size.contents.value = read_size | |
if read_size > 0: | |
return 0 # FLAC__STREAM_DECODER_READ_STATUS_CONTINUE | |
elif read_size == 0: | |
return 1 # FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM | |
else: | |
return 2 # FLAC__STREAM_DECODER_READ_STATUS_ABORT | |
def seek_callback(self, decoder, offset, data): | |
pos = self.file.seek(offset, 0) | |
if pos < 0: | |
return 1 # FLAC__STREAM_DECODER_SEEK_STATUS_ERROR | |
else: | |
return 0 # FLAC__STREAM_DECODER_SEEK_STATUS_OK | |
def tell_callback(self, decoder, offset, data): | |
"""Decoder wants to know the current position of the file stream.""" | |
pos = self.file.tell() | |
if pos < 0: | |
return 1 # FLAC__STREAM_DECODER_TELL_STATUS_ERROR | |
else: | |
offset.contents.value = pos | |
return 0 # FLAC__STREAM_DECODER_TELL_STATUS_OK | |
def length_callback(self, decoder, length, data): | |
"""Decoder wants to know the total length of the stream.""" | |
if self.file_size == 0: | |
return 1 # FLAC__STREAM_DECODER_LENGTH_STATUS_ERROR | |
else: | |
length.contents.value = self.file_size | |
return 0 # FLAC__STREAM_DECODER_LENGTH_STATUS_OK | |
def eof_callback(self, decoder, data): | |
return self.file.tell() >= self.file_size | |
class PyOggSource(StreamingSource): | |
def __init__(self, filename, file): | |
self.filename = filename | |
self.file = file | |
self._stream = None | |
self.sample_size = 16 | |
self._load_source() | |
self.audio_format = AudioFormat(channels=self._stream.channels, sample_size=self.sample_size, | |
sample_rate=self._stream.frequency) | |
def _load_source(self): | |
pass | |
def get_audio_data(self, num_bytes, compensation_time=0.0): | |
"""Data returns as c_short_array instead of LP_c_char or c_ubyte, cast each buffer.""" | |
data = self._stream.get_buffer() # Returns buffer, length or None | |
if data is not None: | |
buff, length = data | |
buff_char_p = cast(buff, POINTER(c_char)) | |
return AudioData(buff_char_p[:length], length, 1000, 1000, []) | |
return None | |
def __del__(self): | |
if self._stream: | |
del self._stream | |
class PyOggFLACSource(PyOggSource): | |
def _load_source(self): | |
if self.file: | |
self._stream = MemoryFLACFileStream(self.filename, self.file) | |
else: | |
self._stream = UnclosedFLACFileStream(self.filename) | |
self.sample_size = self._stream.bits_per_sample | |
self._duration = self._stream.total_samples / self._stream.frequency | |
# Unknown amount of samples. May occur in some sources. | |
if self._stream.total_samples == 0: | |
if _debug: | |
warnings.warn(f"Unknown amount of samples found in {self.filename}. Seeking may be limited.") | |
self._duration_per_frame = 0 | |
else: | |
self._duration_per_frame = self._duration / self._stream.total_samples | |
def seek(self, timestamp): | |
if self._stream.seekable: | |
# Convert sample to seconds. | |
if self._duration_per_frame: | |
timestamp = max(0.0, min(timestamp, self._duration)) | |
position = int(timestamp / self._duration_per_frame) | |
else: # If we have no duration, we cannot reliably seek. However, 0.0 is still required to play and loop. | |
position = 0 | |
seek_succeeded = pyogg.flac.FLAC__stream_decoder_seek_absolute(self._stream.decoder, position) | |
if seek_succeeded is False: | |
warnings.warn(f"Failed to seek FLAC file: {self.filename}") | |
else: | |
warnings.warn(f"Stream is not seekable for FLAC file: {self.filename}.") | |
class PyOggVorbisSource(PyOggSource): | |
def _load_source(self): | |
if self.file: | |
self._stream = MemoryVorbisFileStream(self.filename, self.file) | |
else: | |
self._stream = UnclosedVorbisFileStream(self.filename) | |
self._duration = pyogg.vorbis.libvorbisfile.ov_time_total(byref(self._stream.vf), -1) | |
def get_audio_data(self, num_bytes, compensation_time=0.0): | |
data = self._stream.get_buffer() # Returns buffer, length or None | |
if data is not None: | |
return AudioData(*data, 1000, 1000, []) | |
return None | |
def seek(self, timestamp): | |
seek_succeeded = pyogg.vorbis.ov_time_seek(self._stream.vf, timestamp) | |
if seek_succeeded != 0: | |
if _debug: | |
warnings.warn(f"Failed to seek file {self.filename} - {seek_succeeded}") | |
class PyOggOpusSource(PyOggSource): | |
def _load_source(self): | |
if self.file: | |
self._stream = MemoryOpusFileStream(self.filename, self.file) | |
else: | |
self._stream = UnclosedOpusFileStream(self.filename) | |
self._duration = self._stream.pcm_size / self._stream.frequency | |
self._duration_per_frame = self._duration / self._stream.pcm_size | |
def seek(self, timestamp): | |
timestamp = max(0.0, min(timestamp, self._duration)) | |
position = int(timestamp / self._duration_per_frame) | |
error = pyogg.opus.op_pcm_seek(self._stream.of, position) | |
if error: | |
warnings.warn(f"Opus stream could not seek properly {error}.") | |
class PyOggDecoder(MediaDecoder): | |
vorbis_exts = ('.ogg',) if pyogg.PYOGG_OGG_AVAIL and pyogg.PYOGG_VORBIS_AVAIL and pyogg.PYOGG_VORBIS_FILE_AVAIL else () | |
flac_exts = ('.flac',) if pyogg.PYOGG_FLAC_AVAIL else () | |
opus_exts = ('.opus',) if pyogg.PYOGG_OPUS_AVAIL and pyogg.PYOGG_OPUS_FILE_AVAIL else () | |
exts = vorbis_exts + flac_exts + opus_exts | |
def get_file_extensions(self): | |
return PyOggDecoder.exts | |
def decode(self, filename, file, streaming=True): | |
name, ext = os.path.splitext(filename) | |
if ext in PyOggDecoder.vorbis_exts: | |
source = PyOggVorbisSource | |
elif ext in PyOggDecoder.flac_exts: | |
source = PyOggFLACSource | |
elif ext in PyOggDecoder.opus_exts: | |
source = PyOggOpusSource | |
else: | |
raise DecodeException("Decoder could not find a suitable source to use with this filetype.") | |
if streaming: | |
return source(filename, file) | |
else: | |
return StaticSource(source(filename, file)) | |
def get_decoders(): | |
return [PyOggDecoder()] | |
def get_encoders(): | |
return [] | |