Difficult-Rocket/libs/pyglet/media/codecs/pyogg.py

475 lines
17 KiB
Python
Raw Normal View History

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
_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 pyogg.PyOggError(
"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 pyogg.PyOggError("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 pyogg.PyOggError("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 pyogg.PyOggError("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)
@abstractmethod
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, file, filename, 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 Exception("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 []