mirror of
https://github.com/liberatedsystems/openCom-Companion.git
synced 2024-11-21 21:10:36 +01:00
Added pyogg
This commit is contained in:
parent
dcf722d85f
commit
17c4febc96
108
sbapp/pyogg/__init__.py
Normal file
108
sbapp/pyogg/__init__.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import ctypes
|
||||||
|
|
||||||
|
from .pyogg_error import PyOggError
|
||||||
|
from .ogg import PYOGG_OGG_AVAIL
|
||||||
|
from .vorbis import PYOGG_VORBIS_AVAIL, PYOGG_VORBIS_FILE_AVAIL, PYOGG_VORBIS_ENC_AVAIL
|
||||||
|
from .opus import PYOGG_OPUS_AVAIL, PYOGG_OPUS_FILE_AVAIL, PYOGG_OPUS_ENC_AVAIL
|
||||||
|
from .flac import PYOGG_FLAC_AVAIL
|
||||||
|
|
||||||
|
|
||||||
|
#: PyOgg version number. Versions should comply with PEP440.
|
||||||
|
__version__ = '0.7'
|
||||||
|
|
||||||
|
|
||||||
|
if (PYOGG_OGG_AVAIL and PYOGG_VORBIS_AVAIL and PYOGG_VORBIS_FILE_AVAIL):
|
||||||
|
# VorbisFile
|
||||||
|
from .vorbis_file import VorbisFile
|
||||||
|
# VorbisFileStream
|
||||||
|
from .vorbis_file_stream import VorbisFileStream
|
||||||
|
|
||||||
|
else:
|
||||||
|
class VorbisFile: # type: ignore
|
||||||
|
def __init__(*args, **kw):
|
||||||
|
if not PYOGG_OGG_AVAIL:
|
||||||
|
raise PyOggError("The Ogg library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
raise PyOggError("The Vorbis libraries weren't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
|
||||||
|
class VorbisFileStream: # type: ignore
|
||||||
|
def __init__(*args, **kw):
|
||||||
|
if not PYOGG_OGG_AVAIL:
|
||||||
|
raise PyOggError("The Ogg library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
raise PyOggError("The Vorbis libraries weren't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (PYOGG_OGG_AVAIL and PYOGG_OPUS_AVAIL and PYOGG_OPUS_FILE_AVAIL):
|
||||||
|
# OpusFile
|
||||||
|
from .opus_file import OpusFile
|
||||||
|
# OpusFileStream
|
||||||
|
from .opus_file_stream import OpusFileStream
|
||||||
|
|
||||||
|
else:
|
||||||
|
class OpusFile: # type: ignore
|
||||||
|
def __init__(*args, **kw):
|
||||||
|
if not PYOGG_OGG_AVAIL:
|
||||||
|
raise PyOggError("The Ogg library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
if not PYOGG_OPUS_AVAIL:
|
||||||
|
raise PyOggError("The Opus library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
if not PYOGG_OPUS_FILE_AVAIL:
|
||||||
|
raise PyOggError("The OpusFile library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
raise PyOggError("Unknown initialisation error")
|
||||||
|
|
||||||
|
class OpusFileStream: # type: ignore
|
||||||
|
def __init__(*args, **kw):
|
||||||
|
if not PYOGG_OGG_AVAIL:
|
||||||
|
raise PyOggError("The Ogg library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
if not PYOGG_OPUS_AVAIL:
|
||||||
|
raise PyOggError("The Opus library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
if not PYOGG_OPUS_FILE_AVAIL:
|
||||||
|
raise PyOggError("The OpusFile library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
raise PyOggError("Unknown initialisation error")
|
||||||
|
|
||||||
|
|
||||||
|
if PYOGG_OPUS_AVAIL:
|
||||||
|
# OpusEncoder
|
||||||
|
from .opus_encoder import OpusEncoder
|
||||||
|
# OpusBufferedEncoder
|
||||||
|
from .opus_buffered_encoder import OpusBufferedEncoder
|
||||||
|
# OpusDecoder
|
||||||
|
from .opus_decoder import OpusDecoder
|
||||||
|
|
||||||
|
else:
|
||||||
|
class OpusEncoder: # type: ignore
|
||||||
|
def __init__(*args, **kw):
|
||||||
|
raise PyOggError("The Opus library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
|
||||||
|
class OpusBufferedEncoder: # type: ignore
|
||||||
|
def __init__(*args, **kw):
|
||||||
|
raise PyOggError("The Opus library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
|
||||||
|
class OpusDecoder: # type: ignore
|
||||||
|
def __init__(*args, **kw):
|
||||||
|
raise PyOggError("The Opus library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
|
||||||
|
if (PYOGG_OGG_AVAIL and PYOGG_OPUS_AVAIL):
|
||||||
|
# OggOpusWriter
|
||||||
|
from .ogg_opus_writer import OggOpusWriter
|
||||||
|
|
||||||
|
else:
|
||||||
|
class OggOpusWriter: # type: ignore
|
||||||
|
def __init__(*args, **kw):
|
||||||
|
if not PYOGG_OGG_AVAIL:
|
||||||
|
raise PyOggError("The Ogg library wasn't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
raise PyOggError("The Opus library was't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
|
||||||
|
|
||||||
|
if PYOGG_FLAC_AVAIL:
|
||||||
|
# FlacFile
|
||||||
|
from .flac_file import FlacFile
|
||||||
|
# FlacFileStream
|
||||||
|
from .flac_file_stream import FlacFileStream
|
||||||
|
else:
|
||||||
|
class FlacFile: # type: ignore
|
||||||
|
def __init__(*args, **kw):
|
||||||
|
raise PyOggError("The FLAC libraries weren't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
||||||
|
|
||||||
|
class FlacFileStream: # type: ignore
|
||||||
|
def __init__(*args, **kw):
|
||||||
|
raise PyOggError("The FLAC libraries weren't found or couldn't be loaded (maybe you're trying to use 64bit libraries with 32bit Python?)")
|
59
sbapp/pyogg/audio_file.py
Normal file
59
sbapp/pyogg/audio_file.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from .pyogg_error import PyOggError
|
||||||
|
|
||||||
|
class AudioFile:
|
||||||
|
"""Abstract base class for audio files.
|
||||||
|
|
||||||
|
This class is a base class for audio files (such as Vorbis, Opus,
|
||||||
|
and FLAC). It should not be instatiated directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
raise PyOggError("AudioFile is an Abstract Base Class "+
|
||||||
|
"and should not be instantiated")
|
||||||
|
|
||||||
|
def as_array(self):
|
||||||
|
"""Returns the buffer as a NumPy array.
|
||||||
|
|
||||||
|
The shape of the returned array is in units of (number of
|
||||||
|
samples per channel, number of channels).
|
||||||
|
|
||||||
|
The data type is either 8-bit or 16-bit signed integers,
|
||||||
|
depending on bytes_per_sample.
|
||||||
|
|
||||||
|
The buffer is not copied, but rather the NumPy array
|
||||||
|
shares the memory with the buffer.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Assumes that self.buffer is a one-dimensional array of
|
||||||
|
# bytes and that channels are interleaved.
|
||||||
|
|
||||||
|
import numpy # type: ignore
|
||||||
|
|
||||||
|
assert self.buffer is not None
|
||||||
|
assert self.channels is not None
|
||||||
|
|
||||||
|
# The following code assumes that the bytes in the buffer
|
||||||
|
# represent 8-bit or 16-bit signed ints. Ensure the number of
|
||||||
|
# bytes per sample matches that assumption.
|
||||||
|
assert self.bytes_per_sample == 1 or self.bytes_per_sample == 2
|
||||||
|
|
||||||
|
# Create a dictionary mapping bytes per sample to numpy data
|
||||||
|
# types
|
||||||
|
dtype = {
|
||||||
|
1: numpy.int8,
|
||||||
|
2: numpy.int16
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert the ctypes buffer to a NumPy array
|
||||||
|
array = numpy.frombuffer(
|
||||||
|
self.buffer,
|
||||||
|
dtype=dtype[self.bytes_per_sample]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape the array
|
||||||
|
return array.reshape(
|
||||||
|
(len(self.buffer)
|
||||||
|
// self.bytes_per_sample
|
||||||
|
// self.channels,
|
||||||
|
self.channels)
|
||||||
|
)
|
2061
sbapp/pyogg/flac.py
Normal file
2061
sbapp/pyogg/flac.py
Normal file
File diff suppressed because it is too large
Load Diff
114
sbapp/pyogg/flac_file.py
Normal file
114
sbapp/pyogg/flac_file.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import ctypes
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from . import flac
|
||||||
|
from .audio_file import AudioFile
|
||||||
|
from .pyogg_error import PyOggError
|
||||||
|
|
||||||
|
def _to_char_p(string):
|
||||||
|
try:
|
||||||
|
return ctypes.c_char_p(string.encode("utf-8"))
|
||||||
|
except:
|
||||||
|
return ctypes.c_char_p(string)
|
||||||
|
|
||||||
|
def _resize_array(array, new_size):
|
||||||
|
return (array._type_*new_size).from_address(ctypes.addressof(array))
|
||||||
|
|
||||||
|
|
||||||
|
class FlacFile(AudioFile):
|
||||||
|
def write_callback(self, decoder, frame, buffer, client_data):
|
||||||
|
multi_channel_buf = _resize_array(buffer.contents, self.channels)
|
||||||
|
arr_size = frame.contents.header.blocksize
|
||||||
|
if frame.contents.header.channels >= 2:
|
||||||
|
arrays = []
|
||||||
|
for i in range(frame.contents.header.channels):
|
||||||
|
arr = ctypes.cast(multi_channel_buf[i], ctypes.POINTER(flac.FLAC__int32*arr_size)).contents
|
||||||
|
arrays.append(arr[:])
|
||||||
|
|
||||||
|
arr = list(chain.from_iterable(zip(*arrays)))
|
||||||
|
|
||||||
|
self.buffer[self.buffer_pos : self.buffer_pos + len(arr)] = arr[:]
|
||||||
|
self.buffer_pos += len(arr)
|
||||||
|
|
||||||
|
else:
|
||||||
|
arr = ctypes.cast(multi_channel_buf[0], ctypes.POINTER(flac.FLAC__int32*arr_size)).contents
|
||||||
|
self.buffer[self.buffer_pos : self.buffer_pos + arr_size] = arr[:]
|
||||||
|
self.buffer_pos += arr_size
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def metadata_callback(self,decoder, metadata, client_data):
|
||||||
|
if not self.buffer:
|
||||||
|
self.total_samples = metadata.contents.data.stream_info.total_samples
|
||||||
|
self.channels = metadata.contents.data.stream_info.channels
|
||||||
|
Buffer = flac.FLAC__int16*(self.total_samples * self.channels)
|
||||||
|
self.buffer = Buffer()
|
||||||
|
self.frequency = metadata.contents.data.stream_info.sample_rate
|
||||||
|
|
||||||
|
def error_callback(self,decoder, status, client_data):
|
||||||
|
raise PyOggError("An error occured during the process of decoding. Status enum: {}".format(flac.FLAC__StreamDecoderErrorStatusEnum[status]))
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
self.decoder = flac.FLAC__stream_decoder_new()
|
||||||
|
|
||||||
|
self.client_data = ctypes.c_void_p()
|
||||||
|
|
||||||
|
#: Number of channels in audio file.
|
||||||
|
self.channels = None
|
||||||
|
|
||||||
|
#: Number of samples per second (per channel). For
|
||||||
|
# example, 44100.
|
||||||
|
self.frequency = None
|
||||||
|
|
||||||
|
self.total_samples = None
|
||||||
|
|
||||||
|
#: Raw PCM data from audio file.
|
||||||
|
self.buffer = None
|
||||||
|
|
||||||
|
self.buffer_pos = 0
|
||||||
|
|
||||||
|
write_callback_ = flac.FLAC__StreamDecoderWriteCallback(self.write_callback)
|
||||||
|
|
||||||
|
metadata_callback_ = flac.FLAC__StreamDecoderMetadataCallback(self.metadata_callback)
|
||||||
|
|
||||||
|
error_callback_ = flac.FLAC__StreamDecoderErrorCallback(self.error_callback)
|
||||||
|
|
||||||
|
init_status = flac.FLAC__stream_decoder_init_file(
|
||||||
|
self.decoder,
|
||||||
|
_to_char_p(path), # This will have an issue with Unicode filenames
|
||||||
|
write_callback_,
|
||||||
|
metadata_callback_,
|
||||||
|
error_callback_,
|
||||||
|
self.client_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if init_status: # error
|
||||||
|
error = flac.FLAC__StreamDecoderInitStatusEnum[init_status]
|
||||||
|
raise PyOggError(
|
||||||
|
"An error occured when trying to open '{}': {}".format(path, error)
|
||||||
|
)
|
||||||
|
|
||||||
|
metadata_status = (flac.FLAC__stream_decoder_process_until_end_of_metadata(self.decoder))
|
||||||
|
if not metadata_status: # error
|
||||||
|
raise PyOggError("An error occured when trying to decode the metadata of {}".format(path))
|
||||||
|
|
||||||
|
stream_status = (flac.FLAC__stream_decoder_process_until_end_of_stream(self.decoder))
|
||||||
|
if not stream_status: # error
|
||||||
|
raise PyOggError("An error occured when trying to decode the audio stream of {}".format(path))
|
||||||
|
|
||||||
|
flac.FLAC__stream_decoder_finish(self.decoder)
|
||||||
|
|
||||||
|
#: Length of buffer
|
||||||
|
self.buffer_length = len(self.buffer)
|
||||||
|
|
||||||
|
self.bytes_per_sample = ctypes.sizeof(flac.FLAC__int16) # See definition of Buffer in metadata_callback()
|
||||||
|
|
||||||
|
# Cast buffer to one-dimensional array of chars
|
||||||
|
CharBuffer = (
|
||||||
|
ctypes.c_byte *
|
||||||
|
(self.bytes_per_sample * len(self.buffer))
|
||||||
|
)
|
||||||
|
self.buffer = CharBuffer.from_buffer(self.buffer)
|
||||||
|
|
||||||
|
# FLAC audio is always signed. See
|
||||||
|
# https://xiph.org/flac/api/group__flac__stream__decoder.html#gaf98a4f9e2cac5747da6018c3dfc8dde1
|
||||||
|
self.signed = True
|
141
sbapp/pyogg/flac_file_stream.py
Normal file
141
sbapp/pyogg/flac_file_stream.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import ctypes
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from . import flac
|
||||||
|
from .pyogg_error import PyOggError
|
||||||
|
|
||||||
|
def _to_char_p(string):
|
||||||
|
try:
|
||||||
|
return ctypes.c_char_p(string.encode("utf-8"))
|
||||||
|
except:
|
||||||
|
return ctypes.c_char_p(string)
|
||||||
|
|
||||||
|
def _resize_array(array, new_size):
|
||||||
|
return (array._type_*new_size).from_address(ctypes.addressof(array))
|
||||||
|
|
||||||
|
|
||||||
|
class FlacFileStream:
|
||||||
|
def write_callback(self,decoder, frame, buffer, client_data):
|
||||||
|
multi_channel_buf = _resize_array(buffer.contents, self.channels)
|
||||||
|
arr_size = frame.contents.header.blocksize
|
||||||
|
if frame.contents.header.channels >= 2:
|
||||||
|
arrays = []
|
||||||
|
for i in range(frame.contents.header.channels):
|
||||||
|
arr = ctypes.cast(multi_channel_buf[i], ctypes.POINTER(flac.FLAC__int32*arr_size)).contents
|
||||||
|
arrays.append(arr[:])
|
||||||
|
|
||||||
|
arr = list(chain.from_iterable(zip(*arrays)))
|
||||||
|
|
||||||
|
self.buffer = (flac.FLAC__int16*len(arr))(*arr)
|
||||||
|
self.bytes_written = len(arr) * 2
|
||||||
|
|
||||||
|
else:
|
||||||
|
arr = ctypes.cast(multi_channel_buf[0], ctypes.POINTER(flac.FLAC__int32*arr_size)).contents
|
||||||
|
self.buffer = (flac.FLAC__int16*len(arr))(*arr[:])
|
||||||
|
self.bytes_written = arr_size * 2
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def metadata_callback(self,decoder, metadata, client_data):
|
||||||
|
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
|
||||||
|
|
||||||
|
def error_callback(self,decoder, status, client_data):
|
||||||
|
raise PyOggError("An error occured during the process of decoding. Status enum: {}".format(flac.FLAC__StreamDecoderErrorStatusEnum[status]))
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
self.decoder = flac.FLAC__stream_decoder_new()
|
||||||
|
|
||||||
|
self.client_data = ctypes.c_void_p()
|
||||||
|
|
||||||
|
#: Number of channels in audio file.
|
||||||
|
self.channels = None
|
||||||
|
|
||||||
|
#: Number of samples per second (per channel). For
|
||||||
|
# example, 44100.
|
||||||
|
self.frequency = None
|
||||||
|
|
||||||
|
self.total_samples = None
|
||||||
|
|
||||||
|
self.buffer = None
|
||||||
|
|
||||||
|
self.bytes_written = None
|
||||||
|
|
||||||
|
self.write_callback_ = flac.FLAC__StreamDecoderWriteCallback(self.write_callback)
|
||||||
|
|
||||||
|
self.metadata_callback_ = flac.FLAC__StreamDecoderMetadataCallback(self.metadata_callback)
|
||||||
|
|
||||||
|
self.error_callback_ = flac.FLAC__StreamDecoderErrorCallback(self.error_callback)
|
||||||
|
|
||||||
|
init_status = flac.FLAC__stream_decoder_init_file(self.decoder,
|
||||||
|
_to_char_p(path),
|
||||||
|
self.write_callback_,
|
||||||
|
self.metadata_callback_,
|
||||||
|
self.error_callback_,
|
||||||
|
self.client_data)
|
||||||
|
|
||||||
|
if init_status: # error
|
||||||
|
raise PyOggError("An error occured when trying to open '{}': {}".format(path, flac.FLAC__StreamDecoderInitStatusEnum[init_status]))
|
||||||
|
|
||||||
|
metadata_status = (flac.FLAC__stream_decoder_process_until_end_of_metadata(self.decoder))
|
||||||
|
if not metadata_status: # error
|
||||||
|
raise PyOggError("An error occured when trying to decode the metadata of {}".format(path))
|
||||||
|
|
||||||
|
#: Bytes per sample
|
||||||
|
self.bytes_per_sample = 2
|
||||||
|
|
||||||
|
def get_buffer(self):
|
||||||
|
"""Returns the buffer.
|
||||||
|
|
||||||
|
Returns buffer (a bytes object) or None if all data has
|
||||||
|
been read from the file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Attempt to read a single frame of audio
|
||||||
|
stream_status = (flac.FLAC__stream_decoder_process_single(self.decoder))
|
||||||
|
if not stream_status: # error
|
||||||
|
raise PyOggError("An error occured when trying to decode the audio stream of {}".format(path))
|
||||||
|
|
||||||
|
# Check if we encountered the end of the stream
|
||||||
|
if (flac.FLAC__stream_decoder_get_state(self.decoder) == 4): # end of stream
|
||||||
|
return None
|
||||||
|
|
||||||
|
buffer_as_bytes = bytes(self.buffer)
|
||||||
|
return buffer_as_bytes
|
||||||
|
|
||||||
|
def clean_up(self):
|
||||||
|
flac.FLAC__stream_decoder_finish(self.decoder)
|
||||||
|
|
||||||
|
def get_buffer_as_array(self):
|
||||||
|
"""Provides the buffer as a NumPy array.
|
||||||
|
|
||||||
|
Note that the underlying data type is 16-bit signed
|
||||||
|
integers.
|
||||||
|
|
||||||
|
Does not copy the underlying data, so the returned array
|
||||||
|
should either be processed or copied before the next call
|
||||||
|
to get_buffer() or get_buffer_as_array().
|
||||||
|
|
||||||
|
"""
|
||||||
|
import numpy # type: ignore
|
||||||
|
|
||||||
|
# Read the next samples from the stream
|
||||||
|
buf = self.get_buffer()
|
||||||
|
|
||||||
|
# Check if we've come to the end of the stream
|
||||||
|
if buf is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Convert the bytes buffer to a NumPy array
|
||||||
|
array = numpy.frombuffer(
|
||||||
|
buf,
|
||||||
|
dtype=numpy.int16
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape the array
|
||||||
|
return array.reshape(
|
||||||
|
(len(buf)
|
||||||
|
// self.bytes_per_sample
|
||||||
|
// self.channels,
|
||||||
|
self.channels)
|
||||||
|
)
|
147
sbapp/pyogg/library_loader.py
Normal file
147
sbapp/pyogg/library_loader.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import platform
|
||||||
|
from typing import (
|
||||||
|
Optional,
|
||||||
|
Dict,
|
||||||
|
List
|
||||||
|
)
|
||||||
|
|
||||||
|
_here = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
class ExternalLibraryError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
architecture = platform.architecture()[0]
|
||||||
|
|
||||||
|
_windows_styles = ["{}", "lib{}", "lib{}_dynamic", "{}_dynamic"]
|
||||||
|
|
||||||
|
_other_styles = ["{}", "lib{}"]
|
||||||
|
|
||||||
|
if architecture == "32bit":
|
||||||
|
for arch_style in ["32bit", "32" "86", "win32", "x86", "_x86", "_32", "_win32", "_32bit"]:
|
||||||
|
for style in ["{}", "lib{}"]:
|
||||||
|
_windows_styles.append(style.format("{}"+arch_style))
|
||||||
|
|
||||||
|
elif architecture == "64bit":
|
||||||
|
for arch_style in ["64bit", "64" "86_64", "amd64", "win_amd64", "x86_64", "_x86_64", "_64", "_amd64", "_64bit"]:
|
||||||
|
for style in ["{}", "lib{}"]:
|
||||||
|
_windows_styles.append(style.format("{}"+arch_style))
|
||||||
|
|
||||||
|
|
||||||
|
run_tests = lambda lib, tests: [f(lib) for f in tests]
|
||||||
|
|
||||||
|
# Get the appropriate directory for the shared libraries depending
|
||||||
|
# on the current platform and architecture
|
||||||
|
platform_ = platform.system()
|
||||||
|
lib_dir = None
|
||||||
|
if platform_ == "Darwin":
|
||||||
|
lib_dir = "libs/macos"
|
||||||
|
elif platform_ == "Windows":
|
||||||
|
if architecture == "32bit":
|
||||||
|
lib_dir = "libs/win32"
|
||||||
|
elif architecture == "64bit":
|
||||||
|
lib_dir = "libs/win_amd64"
|
||||||
|
|
||||||
|
|
||||||
|
class Library:
|
||||||
|
@staticmethod
|
||||||
|
def load(names: Dict[str, str], paths: Optional[List[str]] = None, tests = []) -> Optional[ctypes.CDLL]:
|
||||||
|
lib = InternalLibrary.load(names, tests)
|
||||||
|
if lib is None:
|
||||||
|
lib = ExternalLibrary.load(names["external"], paths, tests)
|
||||||
|
return lib
|
||||||
|
|
||||||
|
|
||||||
|
class InternalLibrary:
|
||||||
|
@staticmethod
|
||||||
|
def load(names: Dict[str, str], tests) -> Optional[ctypes.CDLL]:
|
||||||
|
# If we do not have a library directory, give up immediately
|
||||||
|
if lib_dir is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get the appropriate library filename given the platform
|
||||||
|
try:
|
||||||
|
name = names[platform_]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Attempt to load the library from here
|
||||||
|
path = _here + "/" + lib_dir + "/" + name
|
||||||
|
try:
|
||||||
|
lib = ctypes.CDLL(path)
|
||||||
|
except OSError as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check that the library passes the tests
|
||||||
|
if tests and all(run_tests(lib, tests)):
|
||||||
|
return lib
|
||||||
|
|
||||||
|
# Library failed tests
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Cache of libraries that have already been loaded
|
||||||
|
_loaded_libraries: Dict[str, ctypes.CDLL] = {}
|
||||||
|
|
||||||
|
class ExternalLibrary:
|
||||||
|
@staticmethod
|
||||||
|
def load(name, paths = None, tests = []):
|
||||||
|
if name in _loaded_libraries:
|
||||||
|
return _loaded_libraries[name]
|
||||||
|
if sys.platform == "win32":
|
||||||
|
lib = ExternalLibrary.load_windows(name, paths, tests)
|
||||||
|
_loaded_libraries[name] = lib
|
||||||
|
return lib
|
||||||
|
else:
|
||||||
|
lib = ExternalLibrary.load_other(name, paths, tests)
|
||||||
|
_loaded_libraries[name] = lib
|
||||||
|
return lib
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_other(name, paths = None, tests = []):
|
||||||
|
os.environ["PATH"] += ";" + ";".join((os.getcwd(), _here))
|
||||||
|
if paths: os.environ["PATH"] += ";" + ";".join(paths)
|
||||||
|
|
||||||
|
for style in _other_styles:
|
||||||
|
candidate = style.format(name)
|
||||||
|
library = ctypes.util.find_library(candidate)
|
||||||
|
if library:
|
||||||
|
try:
|
||||||
|
lib = ctypes.CDLL(library)
|
||||||
|
if tests and all(run_tests(lib, tests)):
|
||||||
|
return lib
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_windows(name, paths = None, tests = []):
|
||||||
|
os.environ["PATH"] += ";" + ";".join((os.getcwd(), _here))
|
||||||
|
if paths: os.environ["PATH"] += ";" + ";".join(paths)
|
||||||
|
|
||||||
|
not_supported = [] # libraries that were found, but are not supported
|
||||||
|
for style in _windows_styles:
|
||||||
|
candidate = style.format(name)
|
||||||
|
library = ctypes.util.find_library(candidate)
|
||||||
|
if library:
|
||||||
|
try:
|
||||||
|
lib = ctypes.CDLL(library)
|
||||||
|
if tests and all(run_tests(lib, tests)):
|
||||||
|
return lib
|
||||||
|
not_supported.append(library)
|
||||||
|
except WindowsError:
|
||||||
|
pass
|
||||||
|
except OSError:
|
||||||
|
not_supported.append(library)
|
||||||
|
|
||||||
|
|
||||||
|
if not_supported:
|
||||||
|
raise ExternalLibraryError("library '{}' couldn't be loaded, because the following candidates were not supported:".format(name)
|
||||||
|
+ ("\n{}" * len(not_supported)).format(*not_supported))
|
||||||
|
|
||||||
|
raise ExternalLibraryError("library '{}' couldn't be loaded".format(name))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
672
sbapp/pyogg/ogg.py
Normal file
672
sbapp/pyogg/ogg.py
Normal file
@ -0,0 +1,672 @@
|
|||||||
|
############################################################
|
||||||
|
# Ogg license: #
|
||||||
|
############################################################
|
||||||
|
"""
|
||||||
|
Copyright (c) 2002, Xiph.org Foundation
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
- Neither the name of the Xiph.org Foundation nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
|
||||||
|
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
from ctypes import c_int, c_int8, c_int16, c_int32, c_int64, c_uint, c_uint8, c_uint16, c_uint32, c_uint64, c_float, c_long, c_ulong, c_char, c_char_p, c_ubyte, c_longlong, c_ulonglong, c_size_t, c_void_p, c_double, POINTER, pointer, cast
|
||||||
|
import ctypes.util
|
||||||
|
import sys
|
||||||
|
from traceback import print_exc as _print_exc
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .library_loader import Library, ExternalLibrary, ExternalLibraryError
|
||||||
|
|
||||||
|
|
||||||
|
def get_raw_libname(name):
|
||||||
|
name = os.path.splitext(name)[0].lower()
|
||||||
|
for x in "0123456789._- ":name=name.replace(x,"")
|
||||||
|
return name
|
||||||
|
|
||||||
|
# Define a function to convert strings to char-pointers. In Python 3
|
||||||
|
# all strings are Unicode, while in Python 2 they were ASCII-encoded.
|
||||||
|
# FIXME: Does PyOgg even support Python 2?
|
||||||
|
if sys.version_info.major > 2:
|
||||||
|
to_char_p = lambda s: s.encode('utf-8')
|
||||||
|
else:
|
||||||
|
to_char_p = lambda s: s
|
||||||
|
|
||||||
|
__here = os.getcwd()
|
||||||
|
|
||||||
|
libogg = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
names = {
|
||||||
|
"Windows": "ogg.dll",
|
||||||
|
"Darwin": "libogg.0.dylib",
|
||||||
|
"external": "ogg"
|
||||||
|
}
|
||||||
|
libogg = Library.load(names, tests = [lambda lib: hasattr(lib, "oggpack_writeinit")])
|
||||||
|
except ExternalLibraryError:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
_print_exc()
|
||||||
|
|
||||||
|
if libogg is not None:
|
||||||
|
PYOGG_OGG_AVAIL = True
|
||||||
|
else:
|
||||||
|
PYOGG_OGG_AVAIL = False
|
||||||
|
|
||||||
|
if PYOGG_OGG_AVAIL:
|
||||||
|
# Sanity check also satisfies mypy type checking
|
||||||
|
assert libogg is not None
|
||||||
|
|
||||||
|
# ctypes
|
||||||
|
c_ubyte_p = POINTER(c_ubyte)
|
||||||
|
c_uchar = c_ubyte
|
||||||
|
c_uchar_p = c_ubyte_p
|
||||||
|
c_float_p = POINTER(c_float)
|
||||||
|
c_float_p_p = POINTER(c_float_p)
|
||||||
|
c_float_p_p_p = POINTER(c_float_p_p)
|
||||||
|
c_char_p_p = POINTER(c_char_p)
|
||||||
|
c_int_p = POINTER(c_int)
|
||||||
|
c_long_p = POINTER(c_long)
|
||||||
|
|
||||||
|
# os_types
|
||||||
|
ogg_int16_t = c_int16
|
||||||
|
ogg_uint16_t = c_uint16
|
||||||
|
ogg_int32_t = c_int32
|
||||||
|
ogg_uint32_t = c_uint32
|
||||||
|
ogg_int64_t = c_int64
|
||||||
|
ogg_uint64_t = c_uint64
|
||||||
|
ogg_int64_t_p = POINTER(ogg_int64_t)
|
||||||
|
|
||||||
|
# ogg
|
||||||
|
class ogg_iovec_t(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct ogg_iovec_t;
|
||||||
|
"""
|
||||||
|
_fields_ = [("iov_base", c_void_p),
|
||||||
|
("iov_len", c_size_t)]
|
||||||
|
|
||||||
|
class oggpack_buffer(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct oggpack_buffer;
|
||||||
|
"""
|
||||||
|
_fields_ = [("endbyte", c_long),
|
||||||
|
("endbit", c_int),
|
||||||
|
("buffer", c_uchar_p),
|
||||||
|
("ptr", c_uchar_p),
|
||||||
|
("storage", c_long)]
|
||||||
|
|
||||||
|
class ogg_page(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct ogg_page;
|
||||||
|
"""
|
||||||
|
_fields_ = [("header", c_uchar_p),
|
||||||
|
("header_len", c_long),
|
||||||
|
("body", c_uchar_p),
|
||||||
|
("body_len", c_long)]
|
||||||
|
|
||||||
|
class ogg_stream_state(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct ogg_stream_state;
|
||||||
|
"""
|
||||||
|
_fields_ = [("body_data", c_uchar_p),
|
||||||
|
("body_storage", c_long),
|
||||||
|
("body_fill", c_long),
|
||||||
|
("body_returned", c_long),
|
||||||
|
|
||||||
|
("lacing_vals", c_int),
|
||||||
|
("granule_vals", ogg_int64_t),
|
||||||
|
|
||||||
|
("lacing_storage", c_long),
|
||||||
|
("lacing_fill", c_long),
|
||||||
|
("lacing_packet", c_long),
|
||||||
|
("lacing_returned", c_long),
|
||||||
|
|
||||||
|
("header", c_uchar*282),
|
||||||
|
("header_fill", c_int),
|
||||||
|
|
||||||
|
("e_o_s", c_int),
|
||||||
|
("b_o_s", c_int),
|
||||||
|
|
||||||
|
("serialno", c_long),
|
||||||
|
("pageno", c_long),
|
||||||
|
("packetno", ogg_int64_t),
|
||||||
|
("granulepos", ogg_int64_t)]
|
||||||
|
|
||||||
|
class ogg_packet(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct ogg_packet;
|
||||||
|
"""
|
||||||
|
_fields_ = [("packet", c_uchar_p),
|
||||||
|
("bytes", c_long),
|
||||||
|
("b_o_s", c_long),
|
||||||
|
("e_o_s", c_long),
|
||||||
|
|
||||||
|
("granulepos", ogg_int64_t),
|
||||||
|
|
||||||
|
("packetno", ogg_int64_t)]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
bos = ""
|
||||||
|
if self.b_o_s:
|
||||||
|
bos = "beginning of stream, "
|
||||||
|
eos = ""
|
||||||
|
if self.e_o_s:
|
||||||
|
eos = "end of stream, "
|
||||||
|
|
||||||
|
# Converting the data will cause a seg-fault if the memory isn't valid
|
||||||
|
data = bytes(self.packet[0:self.bytes])
|
||||||
|
value = (
|
||||||
|
f"Ogg Packet <{hex(id(self))}>: " +
|
||||||
|
f"number {self.packetno}, " +
|
||||||
|
f"granule position {self.granulepos}, " +
|
||||||
|
bos + eos +
|
||||||
|
f"{self.bytes} bytes"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
class ogg_sync_state(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct ogg_sync_state;
|
||||||
|
"""
|
||||||
|
_fields_ = [("data", c_uchar_p),
|
||||||
|
("storage", c_int),
|
||||||
|
("fill", c_int),
|
||||||
|
("returned", c_int),
|
||||||
|
|
||||||
|
("unsynched", c_int),
|
||||||
|
("headerbytes", c_int),
|
||||||
|
("bodybytes", c_int)]
|
||||||
|
|
||||||
|
b_p = POINTER(oggpack_buffer)
|
||||||
|
oy_p = POINTER(ogg_sync_state)
|
||||||
|
op_p = POINTER(ogg_packet)
|
||||||
|
og_p = POINTER(ogg_page)
|
||||||
|
os_p = POINTER(ogg_stream_state)
|
||||||
|
iov_p = POINTER(ogg_iovec_t)
|
||||||
|
|
||||||
|
libogg.oggpack_writeinit.restype = None
|
||||||
|
libogg.oggpack_writeinit.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpack_writeinit(b):
|
||||||
|
libogg.oggpack_writeinit(b)
|
||||||
|
|
||||||
|
try:
|
||||||
|
libogg.oggpack_writecheck.restype = c_int
|
||||||
|
libogg.oggpack_writecheck.argtypes = [b_p]
|
||||||
|
def oggpack_writecheck(b):
|
||||||
|
libogg.oggpack_writecheck(b)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
libogg.oggpack_writetrunc.restype = None
|
||||||
|
libogg.oggpack_writetrunc.argtypes = [b_p, c_long]
|
||||||
|
|
||||||
|
def oggpack_writetrunc(b, bits):
|
||||||
|
libogg.oggpack_writetrunc(b, bits)
|
||||||
|
|
||||||
|
libogg.oggpack_writealign.restype = None
|
||||||
|
libogg.oggpack_writealign.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpack_writealign(b):
|
||||||
|
libogg.oggpack_writealign(b)
|
||||||
|
|
||||||
|
libogg.oggpack_writecopy.restype = None
|
||||||
|
libogg.oggpack_writecopy.argtypes = [b_p, c_void_p, c_long]
|
||||||
|
|
||||||
|
def oggpack_writecopy(b, source, bits):
|
||||||
|
libogg.oggpack_writecopy(b, source, bits)
|
||||||
|
|
||||||
|
libogg.oggpack_reset.restype = None
|
||||||
|
libogg.oggpack_reset.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpack_reset(b):
|
||||||
|
libogg.oggpack_reset(b)
|
||||||
|
|
||||||
|
libogg.oggpack_writeclear.restype = None
|
||||||
|
libogg.oggpack_writeclear.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpack_writeclear(b):
|
||||||
|
libogg.oggpack_writeclear(b)
|
||||||
|
|
||||||
|
libogg.oggpack_readinit.restype = None
|
||||||
|
libogg.oggpack_readinit.argtypes = [b_p, c_uchar_p, c_int]
|
||||||
|
|
||||||
|
def oggpack_readinit(b, buf, bytes):
|
||||||
|
libogg.oggpack_readinit(b, buf, bytes)
|
||||||
|
|
||||||
|
libogg.oggpack_write.restype = None
|
||||||
|
libogg.oggpack_write.argtypes = [b_p, c_ulong, c_int]
|
||||||
|
|
||||||
|
def oggpack_write(b, value, bits):
|
||||||
|
libogg.oggpack_write(b, value, bits)
|
||||||
|
|
||||||
|
libogg.oggpack_look.restype = c_long
|
||||||
|
libogg.oggpack_look.argtypes = [b_p, c_int]
|
||||||
|
|
||||||
|
def oggpack_look(b, bits):
|
||||||
|
return libogg.oggpack_look(b, bits)
|
||||||
|
|
||||||
|
libogg.oggpack_look1.restype = c_long
|
||||||
|
libogg.oggpack_look1.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpack_look1(b):
|
||||||
|
return libogg.oggpack_look1(b)
|
||||||
|
|
||||||
|
libogg.oggpack_adv.restype = None
|
||||||
|
libogg.oggpack_adv.argtypes = [b_p, c_int]
|
||||||
|
|
||||||
|
def oggpack_adv(b, bits):
|
||||||
|
libogg.oggpack_adv(b, bits)
|
||||||
|
|
||||||
|
libogg.oggpack_adv1.restype = None
|
||||||
|
libogg.oggpack_adv1.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpack_adv1(b):
|
||||||
|
libogg.oggpack_adv1(b)
|
||||||
|
|
||||||
|
libogg.oggpack_read.restype = c_long
|
||||||
|
libogg.oggpack_read.argtypes = [b_p, c_int]
|
||||||
|
|
||||||
|
def oggpack_read(b, bits):
|
||||||
|
return libogg.oggpack_read(b, bits)
|
||||||
|
|
||||||
|
libogg.oggpack_read1.restype = c_long
|
||||||
|
libogg.oggpack_read1.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpack_read1(b):
|
||||||
|
return libogg.oggpack_read1(b)
|
||||||
|
|
||||||
|
libogg.oggpack_bytes.restype = c_long
|
||||||
|
libogg.oggpack_bytes.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpack_bytes(b):
|
||||||
|
return libogg.oggpack_bytes(b)
|
||||||
|
|
||||||
|
libogg.oggpack_bits.restype = c_long
|
||||||
|
libogg.oggpack_bits.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpack_bits(b):
|
||||||
|
return libogg.oggpack_bits(b)
|
||||||
|
|
||||||
|
libogg.oggpack_get_buffer.restype = c_uchar_p
|
||||||
|
libogg.oggpack_get_buffer.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpack_get_buffer(b):
|
||||||
|
return libogg.oggpack_get_buffer(b)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libogg.oggpackB_writeinit.restype = None
|
||||||
|
libogg.oggpackB_writeinit.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpackB_writeinit(b):
|
||||||
|
libogg.oggpackB_writeinit(b)
|
||||||
|
|
||||||
|
try:
|
||||||
|
libogg.oggpackB_writecheck.restype = c_int
|
||||||
|
libogg.oggpackB_writecheck.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpackB_writecheck(b):
|
||||||
|
return libogg.oggpackB_writecheck(b)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
libogg.oggpackB_writetrunc.restype = None
|
||||||
|
libogg.oggpackB_writetrunc.argtypes = [b_p, c_long]
|
||||||
|
|
||||||
|
def oggpackB_writetrunc(b, bits):
|
||||||
|
libogg.oggpackB_writetrunc(b, bits)
|
||||||
|
|
||||||
|
libogg.oggpackB_writealign.restype = None
|
||||||
|
libogg.oggpackB_writealign.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpackB_writealign(b):
|
||||||
|
libogg.oggpackB_writealign(b)
|
||||||
|
|
||||||
|
libogg.oggpackB_writecopy.restype = None
|
||||||
|
libogg.oggpackB_writecopy.argtypes = [b_p, c_void_p, c_long]
|
||||||
|
|
||||||
|
def oggpackB_writecopy(b, source, bits):
|
||||||
|
libogg.oggpackB_writecopy(b, source, bits)
|
||||||
|
|
||||||
|
libogg.oggpackB_reset.restype = None
|
||||||
|
libogg.oggpackB_reset.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpackB_reset(b):
|
||||||
|
libogg.oggpackB_reset(b)
|
||||||
|
|
||||||
|
libogg.oggpackB_reset.restype = None
|
||||||
|
libogg.oggpackB_writeclear.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpackB_reset(b):
|
||||||
|
libogg.oggpackB_reset(b)
|
||||||
|
|
||||||
|
libogg.oggpackB_readinit.restype = None
|
||||||
|
libogg.oggpackB_readinit.argtypes = [b_p, c_uchar_p, c_int]
|
||||||
|
|
||||||
|
def oggpackB_readinit(b, buf, bytes):
|
||||||
|
libogg.oggpackB_readinit(b, buf, bytes)
|
||||||
|
|
||||||
|
libogg.oggpackB_write.restype = None
|
||||||
|
libogg.oggpackB_write.argtypes = [b_p, c_ulong, c_int]
|
||||||
|
|
||||||
|
def oggpackB_write(b, value, bits):
|
||||||
|
libogg.oggpackB_write(b, value, bits)
|
||||||
|
|
||||||
|
libogg.oggpackB_look.restype = c_long
|
||||||
|
libogg.oggpackB_look.argtypes = [b_p, c_int]
|
||||||
|
|
||||||
|
def oggpackB_look(b, bits):
|
||||||
|
return libogg.oggpackB_look(b, bits)
|
||||||
|
|
||||||
|
libogg.oggpackB_look1.restype = c_long
|
||||||
|
libogg.oggpackB_look1.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpackB_look1(b):
|
||||||
|
return libogg.oggpackB_look1(b)
|
||||||
|
|
||||||
|
libogg.oggpackB_adv.restype = None
|
||||||
|
libogg.oggpackB_adv.argtypes = [b_p, c_int]
|
||||||
|
|
||||||
|
def oggpackB_adv(b, bits):
|
||||||
|
libogg.oggpackB_adv(b, bits)
|
||||||
|
|
||||||
|
libogg.oggpackB_adv1.restype = None
|
||||||
|
libogg.oggpackB_adv1.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpackB_adv1(b):
|
||||||
|
libogg.oggpackB_adv1(b)
|
||||||
|
|
||||||
|
libogg.oggpackB_read.restype = c_long
|
||||||
|
libogg.oggpackB_read.argtypes = [b_p, c_int]
|
||||||
|
|
||||||
|
def oggpackB_read(b, bits):
|
||||||
|
return libogg.oggpackB_read(b, bits)
|
||||||
|
|
||||||
|
libogg.oggpackB_read1.restype = c_long
|
||||||
|
libogg.oggpackB_read1.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpackB_read1(b):
|
||||||
|
return libogg.oggpackB_read1(b)
|
||||||
|
|
||||||
|
libogg.oggpackB_bytes.restype = c_long
|
||||||
|
libogg.oggpackB_bytes.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpackB_bytes(b):
|
||||||
|
return libogg.oggpackB_bytes(b)
|
||||||
|
|
||||||
|
libogg.oggpackB_bits.restype = c_long
|
||||||
|
libogg.oggpackB_bits.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpackB_bits(b):
|
||||||
|
return libogg.oggpackB_bits(b)
|
||||||
|
|
||||||
|
libogg.oggpackB_get_buffer.restype = c_uchar_p
|
||||||
|
libogg.oggpackB_get_buffer.argtypes = [b_p]
|
||||||
|
|
||||||
|
def oggpackB_get_buffer(b):
|
||||||
|
return libogg.oggpackB_get_buffer(b)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libogg.ogg_stream_packetin.restype = c_int
|
||||||
|
libogg.ogg_stream_packetin.argtypes = [os_p, op_p]
|
||||||
|
|
||||||
|
def ogg_stream_packetin(os, op):
|
||||||
|
return libogg.ogg_stream_packetin(os, op)
|
||||||
|
|
||||||
|
try:
|
||||||
|
libogg.ogg_stream_iovecin.restype = c_int
|
||||||
|
libogg.ogg_stream_iovecin.argtypes = [os_p, iov_p, c_int, c_long, ogg_int64_t]
|
||||||
|
|
||||||
|
def ogg_stream_iovecin(os, iov, count, e_o_s, granulepos):
|
||||||
|
return libogg.ogg_stream_iovecin(os, iov, count, e_o_s, granulepos)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
libogg.ogg_stream_pageout.restype = c_int
|
||||||
|
libogg.ogg_stream_pageout.argtypes = [os_p, og_p]
|
||||||
|
|
||||||
|
def ogg_stream_pageout(os, og):
|
||||||
|
return libogg.ogg_stream_pageout(os, og)
|
||||||
|
|
||||||
|
try:
|
||||||
|
libogg.ogg_stream_pageout_fill.restype = c_int
|
||||||
|
libogg.ogg_stream_pageout_fill.argtypes = [os_p, og_p, c_int]
|
||||||
|
def ogg_stream_pageout_fill(os, og, nfill):
|
||||||
|
return libogg.ogg_stream_pageout_fill(os, og, nfill)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
libogg.ogg_stream_flush.restype = c_int
|
||||||
|
libogg.ogg_stream_flush.argtypes = [os_p, og_p]
|
||||||
|
|
||||||
|
def ogg_stream_flush(os, og):
|
||||||
|
return libogg.ogg_stream_flush(os, og)
|
||||||
|
|
||||||
|
try:
|
||||||
|
libogg.ogg_stream_flush_fill.restype = c_int
|
||||||
|
libogg.ogg_stream_flush_fill.argtypes = [os_p, og_p, c_int]
|
||||||
|
def ogg_stream_flush_fill(os, og, nfill):
|
||||||
|
return libogg.ogg_stream_flush_fill(os, og, nfill)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libogg.ogg_sync_init.restype = c_int
|
||||||
|
libogg.ogg_sync_init.argtypes = [oy_p]
|
||||||
|
|
||||||
|
def ogg_sync_init(oy):
|
||||||
|
return libogg.ogg_sync_init(oy)
|
||||||
|
|
||||||
|
libogg.ogg_sync_clear.restype = c_int
|
||||||
|
libogg.ogg_sync_clear.argtypes = [oy_p]
|
||||||
|
|
||||||
|
def ogg_sync_clear(oy):
|
||||||
|
return libogg.ogg_sync_clear(oy)
|
||||||
|
|
||||||
|
libogg.ogg_sync_reset.restype = c_int
|
||||||
|
libogg.ogg_sync_reset.argtypes = [oy_p]
|
||||||
|
|
||||||
|
def ogg_sync_reset(oy):
|
||||||
|
return libogg.ogg_sync_reset(oy)
|
||||||
|
|
||||||
|
libogg.ogg_sync_destroy.restype = c_int
|
||||||
|
libogg.ogg_sync_destroy.argtypes = [oy_p]
|
||||||
|
|
||||||
|
def ogg_sync_destroy(oy):
|
||||||
|
return libogg.ogg_sync_destroy(oy)
|
||||||
|
|
||||||
|
try:
|
||||||
|
libogg.ogg_sync_check.restype = c_int
|
||||||
|
libogg.ogg_sync_check.argtypes = [oy_p]
|
||||||
|
def ogg_sync_check(oy):
|
||||||
|
return libogg.ogg_sync_check(oy)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libogg.ogg_sync_buffer.restype = c_char_p
|
||||||
|
libogg.ogg_sync_buffer.argtypes = [oy_p, c_long]
|
||||||
|
|
||||||
|
def ogg_sync_buffer(oy, size):
|
||||||
|
return libogg.ogg_sync_buffer(oy, size)
|
||||||
|
|
||||||
|
libogg.ogg_sync_wrote.restype = c_int
|
||||||
|
libogg.ogg_sync_wrote.argtypes = [oy_p, c_long]
|
||||||
|
|
||||||
|
def ogg_sync_wrote(oy, bytes):
|
||||||
|
return libogg.ogg_sync_wrote(oy, bytes)
|
||||||
|
|
||||||
|
libogg.ogg_sync_pageseek.restype = c_int
|
||||||
|
libogg.ogg_sync_pageseek.argtypes = [oy_p, og_p]
|
||||||
|
|
||||||
|
def ogg_sync_pageseek(oy, og):
|
||||||
|
return libogg.ogg_sync_pageseek(oy, og)
|
||||||
|
|
||||||
|
libogg.ogg_sync_pageout.restype = c_long
|
||||||
|
libogg.ogg_sync_pageout.argtypes = [oy_p, og_p]
|
||||||
|
|
||||||
|
def ogg_sync_pageout(oy, og):
|
||||||
|
return libogg.ogg_sync_pageout(oy, og)
|
||||||
|
|
||||||
|
libogg.ogg_stream_pagein.restype = c_int
|
||||||
|
libogg.ogg_stream_pagein.argtypes = [os_p, og_p]
|
||||||
|
|
||||||
|
def ogg_stream_pagein(os, og):
|
||||||
|
return libogg.ogg_stream_pagein(oy, og)
|
||||||
|
|
||||||
|
libogg.ogg_stream_packetout.restype = c_int
|
||||||
|
libogg.ogg_stream_packetout.argtypes = [os_p, op_p]
|
||||||
|
|
||||||
|
def ogg_stream_packetout(os, op):
|
||||||
|
return libogg.ogg_stream_packetout(oy, op)
|
||||||
|
|
||||||
|
libogg.ogg_stream_packetpeek.restype = c_int
|
||||||
|
libogg.ogg_stream_packetpeek.argtypes = [os_p, op_p]
|
||||||
|
|
||||||
|
def ogg_stream_packetpeek(os, op):
|
||||||
|
return libogg.ogg_stream_packetpeek(os, op)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libogg.ogg_stream_init.restype = c_int
|
||||||
|
libogg.ogg_stream_init.argtypes = [os_p, c_int]
|
||||||
|
|
||||||
|
def ogg_stream_init(os, serialno):
|
||||||
|
return libogg.ogg_stream_init(os, serialno)
|
||||||
|
|
||||||
|
libogg.ogg_stream_clear.restype = c_int
|
||||||
|
libogg.ogg_stream_clear.argtypes = [os_p]
|
||||||
|
|
||||||
|
def ogg_stream_clear(os):
|
||||||
|
return libogg.ogg_stream_clear(os)
|
||||||
|
|
||||||
|
libogg.ogg_stream_reset.restype = c_int
|
||||||
|
libogg.ogg_stream_reset.argtypes = [os_p]
|
||||||
|
|
||||||
|
def ogg_stream_reset(os):
|
||||||
|
return libogg.ogg_stream_reset(os)
|
||||||
|
|
||||||
|
libogg.ogg_stream_reset_serialno.restype = c_int
|
||||||
|
libogg.ogg_stream_reset_serialno.argtypes = [os_p, c_int]
|
||||||
|
|
||||||
|
def ogg_stream_reset_serialno(os, serialno):
|
||||||
|
return libogg.ogg_stream_reset_serialno(os, serialno)
|
||||||
|
|
||||||
|
libogg.ogg_stream_destroy.restype = c_int
|
||||||
|
libogg.ogg_stream_destroy.argtypes = [os_p]
|
||||||
|
|
||||||
|
def ogg_stream_destroy(os):
|
||||||
|
return libogg.ogg_stream_destroy(os)
|
||||||
|
|
||||||
|
try:
|
||||||
|
libogg.ogg_stream_check.restype = c_int
|
||||||
|
libogg.ogg_stream_check.argtypes = [os_p]
|
||||||
|
def ogg_stream_check(os):
|
||||||
|
return libogg.ogg_stream_check(os)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
libogg.ogg_stream_eos.restype = c_int
|
||||||
|
libogg.ogg_stream_eos.argtypes = [os_p]
|
||||||
|
|
||||||
|
def ogg_stream_eos(os):
|
||||||
|
return libogg.ogg_stream_eos(os)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libogg.ogg_page_checksum_set.restype = None
|
||||||
|
libogg.ogg_page_checksum_set.argtypes = [og_p]
|
||||||
|
|
||||||
|
def ogg_page_checksum_set(og):
|
||||||
|
libogg.ogg_page_checksum_set(og)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libogg.ogg_page_version.restype = c_int
|
||||||
|
libogg.ogg_page_version.argtypes = [og_p]
|
||||||
|
|
||||||
|
def ogg_page_version(og):
|
||||||
|
return libogg.ogg_page_version(og)
|
||||||
|
|
||||||
|
libogg.ogg_page_continued.restype = c_int
|
||||||
|
libogg.ogg_page_continued.argtypes = [og_p]
|
||||||
|
|
||||||
|
def ogg_page_continued(og):
|
||||||
|
return libogg.ogg_page_continued(og)
|
||||||
|
|
||||||
|
libogg.ogg_page_bos.restype = c_int
|
||||||
|
libogg.ogg_page_bos.argtypes = [og_p]
|
||||||
|
|
||||||
|
def ogg_page_bos(og):
|
||||||
|
return libogg.ogg_page_bos(og)
|
||||||
|
|
||||||
|
libogg.ogg_page_eos.restype = c_int
|
||||||
|
libogg.ogg_page_eos.argtypes = [og_p]
|
||||||
|
|
||||||
|
def ogg_page_eos(og):
|
||||||
|
return libogg.ogg_page_eos(og)
|
||||||
|
|
||||||
|
libogg.ogg_page_granulepos.restype = ogg_int64_t
|
||||||
|
libogg.ogg_page_granulepos.argtypes = [og_p]
|
||||||
|
|
||||||
|
def ogg_page_granulepos(og):
|
||||||
|
return libogg.ogg_page_granulepos(og)
|
||||||
|
|
||||||
|
libogg.ogg_page_serialno.restype = c_int
|
||||||
|
libogg.ogg_page_serialno.argtypes = [og_p]
|
||||||
|
|
||||||
|
def ogg_page_serialno(og):
|
||||||
|
return libogg.ogg_page_serialno(og)
|
||||||
|
|
||||||
|
libogg.ogg_page_pageno.restype = c_long
|
||||||
|
libogg.ogg_page_pageno.argtypes = [og_p]
|
||||||
|
|
||||||
|
def ogg_page_pageno(og):
|
||||||
|
return libogg.ogg_page_pageno(og)
|
||||||
|
|
||||||
|
libogg.ogg_page_packets.restype = c_int
|
||||||
|
libogg.ogg_page_packets.argtypes = [og_p]
|
||||||
|
|
||||||
|
def ogg_page_packets(og):
|
||||||
|
return libogg.ogg_page_packets(og)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libogg.ogg_packet_clear.restype = None
|
||||||
|
libogg.ogg_packet_clear.argtypes = [op_p]
|
||||||
|
|
||||||
|
def ogg_packet_clear(op):
|
||||||
|
libogg.ogg_packet_clear(op)
|
421
sbapp/pyogg/ogg_opus_writer.py
Normal file
421
sbapp/pyogg/ogg_opus_writer.py
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
import builtins
|
||||||
|
import copy
|
||||||
|
import ctypes
|
||||||
|
import random
|
||||||
|
import struct
|
||||||
|
from typing import (
|
||||||
|
Optional,
|
||||||
|
Union,
|
||||||
|
BinaryIO
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import ogg
|
||||||
|
from . import opus
|
||||||
|
from .opus_buffered_encoder import OpusBufferedEncoder
|
||||||
|
#from .opus_encoder import OpusEncoder
|
||||||
|
from .pyogg_error import PyOggError
|
||||||
|
|
||||||
|
class OggOpusWriter():
|
||||||
|
"""Encodes PCM data into an OggOpus file."""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
f: Union[BinaryIO, str],
|
||||||
|
encoder: OpusBufferedEncoder,
|
||||||
|
custom_pre_skip: Optional[int] = None) -> None:
|
||||||
|
"""Construct an OggOpusWriter.
|
||||||
|
|
||||||
|
f may be either a string giving the path to the file, or
|
||||||
|
an already-opened file handle.
|
||||||
|
|
||||||
|
If f is an already-opened file handle, then it is the
|
||||||
|
user's responsibility to close the file when they are
|
||||||
|
finished with it. The file should be opened for writing
|
||||||
|
in binary (not text) mode.
|
||||||
|
|
||||||
|
The encoder should be a
|
||||||
|
OpusBufferedEncoder and should be fully configured before the
|
||||||
|
first call to the `write()` method.
|
||||||
|
|
||||||
|
The Opus encoder requires an amount of "warm up" and when
|
||||||
|
stored in an Ogg container that warm up can be skipped. When
|
||||||
|
`custom_pre_skip` is None, the required amount of warm up
|
||||||
|
silence is automatically calculated and inserted. If a custom
|
||||||
|
(non-silent) pre-skip is desired, then `custom_pre_skip`
|
||||||
|
should be specified as the number of samples (per channel).
|
||||||
|
It is then the user's responsibility to pass the non-silent
|
||||||
|
pre-skip samples to `encode()`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Store the Opus encoder
|
||||||
|
self._encoder = encoder
|
||||||
|
|
||||||
|
# Store the custom pre skip
|
||||||
|
self._custom_pre_skip = custom_pre_skip
|
||||||
|
|
||||||
|
# Create a new stream state with a random serial number
|
||||||
|
self._stream_state = self._create_stream_state()
|
||||||
|
|
||||||
|
# Create a packet (reused for each pass)
|
||||||
|
self._ogg_packet = ogg.ogg_packet()
|
||||||
|
self._packet_valid = False
|
||||||
|
|
||||||
|
# Create a page (reused for each pass)
|
||||||
|
self._ogg_page = ogg.ogg_page()
|
||||||
|
|
||||||
|
# Counter for the number of packets written into Ogg stream
|
||||||
|
self._count_packets = 0
|
||||||
|
|
||||||
|
# Counter for the number of samples encoded into Opus
|
||||||
|
# packets
|
||||||
|
self._count_samples = 0
|
||||||
|
|
||||||
|
# Flag to indicate if the headers have been written
|
||||||
|
self._headers_written = False
|
||||||
|
|
||||||
|
# Flag to indicate that the stream has been finished (the
|
||||||
|
# EOS bit was set in a final packet)
|
||||||
|
self._finished = False
|
||||||
|
|
||||||
|
# Reference to the current encoded packet (written only
|
||||||
|
# when we know if it the last)
|
||||||
|
self._current_encoded_packet: Optional[bytes] = None
|
||||||
|
|
||||||
|
# Open file if required. Given this may raise an exception,
|
||||||
|
# it should be the last step of initialisation.
|
||||||
|
self._i_opened_the_file = False
|
||||||
|
if isinstance(f, str):
|
||||||
|
self._file = builtins.open(f, 'wb')
|
||||||
|
self._i_opened_the_file = True
|
||||||
|
else:
|
||||||
|
# Assume it's already opened file
|
||||||
|
self._file = f
|
||||||
|
|
||||||
|
def __del__(self) -> None:
|
||||||
|
if not self._finished:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
#
|
||||||
|
# User visible methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def write(self, pcm: memoryview) -> None:
|
||||||
|
"""Encode the PCM and write out the Ogg Opus stream.
|
||||||
|
|
||||||
|
Encoders the PCM using the provided encoder.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Check that the stream hasn't already been finished
|
||||||
|
if self._finished:
|
||||||
|
raise PyOggError(
|
||||||
|
"Stream has already ended. Perhaps close() was "+
|
||||||
|
"called too early?")
|
||||||
|
|
||||||
|
# If we haven't already written out the headers, do so
|
||||||
|
# now. Then, write a frame of silence to warm up the
|
||||||
|
# encoder.
|
||||||
|
if not self._headers_written:
|
||||||
|
pre_skip = self._write_headers(self._custom_pre_skip)
|
||||||
|
if self._custom_pre_skip is None:
|
||||||
|
self._write_silence(pre_skip)
|
||||||
|
|
||||||
|
# Call the internal method to encode the bytes
|
||||||
|
self._write_to_oggopus(pcm)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_to_oggopus(self, pcm: memoryview, flush: bool = False) -> None:
|
||||||
|
assert self._encoder is not None
|
||||||
|
|
||||||
|
def handle_encoded_packet(encoded_packet: memoryview,
|
||||||
|
samples: int,
|
||||||
|
end_of_stream: bool) -> None:
|
||||||
|
# Cast memoryview to ctypes Array
|
||||||
|
Buffer = ctypes.c_ubyte * len(encoded_packet)
|
||||||
|
encoded_packet_ctypes = Buffer.from_buffer(encoded_packet)
|
||||||
|
|
||||||
|
# Obtain a pointer to the encoded packet
|
||||||
|
encoded_packet_ptr = ctypes.cast(
|
||||||
|
encoded_packet_ctypes,
|
||||||
|
ctypes.POINTER(ctypes.c_ubyte)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Increase the count of the number of samples written
|
||||||
|
self._count_samples += samples
|
||||||
|
|
||||||
|
# Place data into the packet
|
||||||
|
self._ogg_packet.packet = encoded_packet_ptr
|
||||||
|
self._ogg_packet.bytes = len(encoded_packet)
|
||||||
|
self._ogg_packet.b_o_s = 0
|
||||||
|
self._ogg_packet.e_o_s = end_of_stream
|
||||||
|
self._ogg_packet.granulepos = self._count_samples
|
||||||
|
self._ogg_packet.packetno = self._count_packets
|
||||||
|
|
||||||
|
# Increase the counter of the number of packets
|
||||||
|
# in the stream
|
||||||
|
self._count_packets += 1
|
||||||
|
|
||||||
|
# Write the packet into the stream
|
||||||
|
self._write_packet()
|
||||||
|
|
||||||
|
|
||||||
|
# Encode the PCM data into an Opus packet
|
||||||
|
self._encoder.buffered_encode(
|
||||||
|
pcm,
|
||||||
|
flush=flush,
|
||||||
|
callback=handle_encoded_packet
|
||||||
|
)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
# Check we haven't already closed this stream
|
||||||
|
if self._finished:
|
||||||
|
# We're attempting to close an already closed stream,
|
||||||
|
# do nothing more.
|
||||||
|
return
|
||||||
|
|
||||||
|
# Flush the underlying buffered encoder
|
||||||
|
self._write_to_oggopus(memoryview(bytearray(b"")), flush=True)
|
||||||
|
|
||||||
|
# The current packet must be the end of the stream, update
|
||||||
|
# the packet's details
|
||||||
|
self._ogg_packet.e_o_s = 1
|
||||||
|
|
||||||
|
# Write the packet to the stream
|
||||||
|
if self._packet_valid:
|
||||||
|
self._write_packet()
|
||||||
|
|
||||||
|
# Flush the stream of any unwritten pages
|
||||||
|
self._flush()
|
||||||
|
|
||||||
|
# Mark the stream as finished
|
||||||
|
self._finished = True
|
||||||
|
|
||||||
|
# Close the file if we opened it
|
||||||
|
if self._i_opened_the_file:
|
||||||
|
self._file.close()
|
||||||
|
self._i_opened_the_file = False
|
||||||
|
|
||||||
|
# Clean up the Ogg-related memory
|
||||||
|
ogg.ogg_stream_clear(self._stream_state)
|
||||||
|
|
||||||
|
# Clean up the reference to the encoded packet (as it must
|
||||||
|
# now have been written)
|
||||||
|
del self._current_encoded_packet
|
||||||
|
|
||||||
|
#
|
||||||
|
# Internal methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def _create_random_serial_no(self) -> ctypes.c_int:
|
||||||
|
sizeof_c_int = ctypes.sizeof(ctypes.c_int)
|
||||||
|
min_int = -2**(sizeof_c_int*8-1)
|
||||||
|
max_int = 2**(sizeof_c_int*8-1)-1
|
||||||
|
serial_no = ctypes.c_int(random.randint(min_int, max_int))
|
||||||
|
|
||||||
|
return serial_no
|
||||||
|
|
||||||
|
def _create_stream_state(self) -> ogg.ogg_stream_state:
|
||||||
|
# Create a random serial number
|
||||||
|
serial_no = self._create_random_serial_no()
|
||||||
|
|
||||||
|
# Create an ogg_stream_state
|
||||||
|
ogg_stream_state = ogg.ogg_stream_state()
|
||||||
|
|
||||||
|
# Initialise the stream state
|
||||||
|
ogg.ogg_stream_init(
|
||||||
|
ctypes.pointer(ogg_stream_state),
|
||||||
|
serial_no
|
||||||
|
)
|
||||||
|
|
||||||
|
return ogg_stream_state
|
||||||
|
|
||||||
|
def _make_identification_header(self, pre_skip: int, input_sampling_rate: int = 0) -> bytes:
|
||||||
|
"""Make the OggOpus identification header.
|
||||||
|
|
||||||
|
An input_sampling rate may be set to zero to mean 'unspecified'.
|
||||||
|
|
||||||
|
Only channel mapping family 0 is currently supported.
|
||||||
|
This allows mono and stereo signals.
|
||||||
|
|
||||||
|
See https://tools.ietf.org/html/rfc7845#page-12 for more
|
||||||
|
details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
signature = b"OpusHead"
|
||||||
|
version = 1
|
||||||
|
output_channels = self._encoder._channels
|
||||||
|
output_gain = 0
|
||||||
|
channel_mapping_family = 0
|
||||||
|
data = struct.pack(
|
||||||
|
"<BBHIHB",
|
||||||
|
version,
|
||||||
|
output_channels,
|
||||||
|
pre_skip,
|
||||||
|
input_sampling_rate,
|
||||||
|
output_gain,
|
||||||
|
channel_mapping_family
|
||||||
|
)
|
||||||
|
|
||||||
|
return signature+data
|
||||||
|
|
||||||
|
def _write_identification_header_packet(self, custom_pre_skip: int) -> int:
|
||||||
|
""" Returns pre-skip. """
|
||||||
|
if custom_pre_skip is not None:
|
||||||
|
# Use the user-specified amount of pre-skip
|
||||||
|
pre_skip = custom_pre_skip
|
||||||
|
else:
|
||||||
|
# Obtain the algorithmic delay of the Opus encoder. See
|
||||||
|
# https://tools.ietf.org/html/rfc7845#page-27
|
||||||
|
delay_samples = self._encoder.get_algorithmic_delay()
|
||||||
|
|
||||||
|
# Extra samples are recommended. See
|
||||||
|
# https://tools.ietf.org/html/rfc7845#page-27
|
||||||
|
extra_samples = 120
|
||||||
|
|
||||||
|
# We will just fill a whole frame with silence. Calculate
|
||||||
|
# the minimum frame length, which we'll use as the
|
||||||
|
# pre-skip.
|
||||||
|
frame_durations = [2.5, 5, 10, 20, 40, 60] # milliseconds
|
||||||
|
frame_lengths = [
|
||||||
|
x * self._encoder._samples_per_second // 1000
|
||||||
|
for x in frame_durations
|
||||||
|
]
|
||||||
|
for frame_length in frame_lengths:
|
||||||
|
if frame_length > delay_samples + extra_samples:
|
||||||
|
pre_skip = frame_length
|
||||||
|
break
|
||||||
|
|
||||||
|
# Create the identification header
|
||||||
|
id_header = self._make_identification_header(
|
||||||
|
pre_skip = pre_skip
|
||||||
|
)
|
||||||
|
|
||||||
|
# Specify the packet containing the identification header
|
||||||
|
self._ogg_packet.packet = ctypes.cast(id_header, ogg.c_uchar_p) # type: ignore
|
||||||
|
self._ogg_packet.bytes = len(id_header)
|
||||||
|
self._ogg_packet.b_o_s = 1
|
||||||
|
self._ogg_packet.e_o_s = 0
|
||||||
|
self._ogg_packet.granulepos = 0
|
||||||
|
self._ogg_packet.packetno = self._count_packets
|
||||||
|
self._count_packets += 1
|
||||||
|
|
||||||
|
# Write the identification header
|
||||||
|
result = ogg.ogg_stream_packetin(
|
||||||
|
self._stream_state,
|
||||||
|
self._ogg_packet
|
||||||
|
)
|
||||||
|
|
||||||
|
if result != 0:
|
||||||
|
raise PyOggError(
|
||||||
|
"Failed to write Opus identification header"
|
||||||
|
)
|
||||||
|
|
||||||
|
return pre_skip
|
||||||
|
|
||||||
|
def _make_comment_header(self):
|
||||||
|
"""Make the OggOpus comment header.
|
||||||
|
|
||||||
|
See https://tools.ietf.org/html/rfc7845#page-22 for more
|
||||||
|
details.
|
||||||
|
|
||||||
|
"""
|
||||||
|
signature = b"OpusTags"
|
||||||
|
vendor_string = b"ENCODER=PyOgg"
|
||||||
|
vendor_string_length = struct.pack("<I",len(vendor_string))
|
||||||
|
user_comments_length = struct.pack("<I",0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
signature
|
||||||
|
+ vendor_string_length
|
||||||
|
+ vendor_string
|
||||||
|
+ user_comments_length
|
||||||
|
)
|
||||||
|
|
||||||
|
def _write_comment_header_packet(self):
|
||||||
|
# Specify the comment header
|
||||||
|
comment_header = self._make_comment_header()
|
||||||
|
|
||||||
|
# Specify the packet containing the identification header
|
||||||
|
self._ogg_packet.packet = ctypes.cast(comment_header, ogg.c_uchar_p)
|
||||||
|
self._ogg_packet.bytes = len(comment_header)
|
||||||
|
self._ogg_packet.b_o_s = 0
|
||||||
|
self._ogg_packet.e_o_s = 0
|
||||||
|
self._ogg_packet.granulepos = 0
|
||||||
|
self._ogg_packet.packetno = self._count_packets
|
||||||
|
self._count_packets += 1
|
||||||
|
|
||||||
|
# Write the header
|
||||||
|
result = ogg.ogg_stream_packetin(
|
||||||
|
self._stream_state,
|
||||||
|
self._ogg_packet
|
||||||
|
)
|
||||||
|
|
||||||
|
if result != 0:
|
||||||
|
raise PyOggError(
|
||||||
|
"Failed to write Opus comment header"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _write_page(self):
|
||||||
|
""" Write page to file """
|
||||||
|
# Cast pointer to ctypes array, which can then be passed to
|
||||||
|
# write without issues.
|
||||||
|
HeaderBufferPtr = ctypes.POINTER(ctypes.c_ubyte * self._ogg_page.header_len)
|
||||||
|
header = HeaderBufferPtr(self._ogg_page.header.contents)[0]
|
||||||
|
self._file.write(header)
|
||||||
|
|
||||||
|
BodyBufferPtr = ctypes.POINTER(ctypes.c_ubyte * self._ogg_page.body_len)
|
||||||
|
body = BodyBufferPtr(self._ogg_page.body.contents)[0]
|
||||||
|
self._file.write(body)
|
||||||
|
|
||||||
|
def _flush(self):
|
||||||
|
""" Flush all pages to the file. """
|
||||||
|
while ogg.ogg_stream_flush(
|
||||||
|
ctypes.pointer(self._stream_state),
|
||||||
|
ctypes.pointer(self._ogg_page)) != 0:
|
||||||
|
self._write_page()
|
||||||
|
|
||||||
|
def _write_headers(self, custom_pre_skip):
|
||||||
|
""" Write the two Opus header packets."""
|
||||||
|
pre_skip = self._write_identification_header_packet(
|
||||||
|
custom_pre_skip
|
||||||
|
)
|
||||||
|
self._write_comment_header_packet()
|
||||||
|
|
||||||
|
# Store that the headers have been written
|
||||||
|
self._headers_written = True
|
||||||
|
|
||||||
|
# Write out pages to file to ensure that the headers are
|
||||||
|
# the only packets to appear on the first page. If this
|
||||||
|
# is not done, the file cannot be read by the library
|
||||||
|
# opusfile.
|
||||||
|
self._flush()
|
||||||
|
|
||||||
|
return pre_skip
|
||||||
|
|
||||||
|
def _write_packet(self):
|
||||||
|
# Place the packet into the stream
|
||||||
|
result = ogg.ogg_stream_packetin(
|
||||||
|
self._stream_state,
|
||||||
|
self._ogg_packet
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
if result != 0:
|
||||||
|
raise PyOggError(
|
||||||
|
"Error while placing packet in Ogg stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write out pages to file
|
||||||
|
while ogg.ogg_stream_pageout(
|
||||||
|
ctypes.pointer(self._stream_state),
|
||||||
|
ctypes.pointer(self._ogg_page)) != 0:
|
||||||
|
self._write_page()
|
||||||
|
|
||||||
|
def _write_silence(self, samples):
|
||||||
|
""" Write a frame of silence. """
|
||||||
|
silence_length = (
|
||||||
|
samples
|
||||||
|
* self._encoder._channels
|
||||||
|
* ctypes.sizeof(opus.opus_int16)
|
||||||
|
)
|
||||||
|
silence_pcm = \
|
||||||
|
memoryview(bytearray(b"\x00" * silence_length))
|
||||||
|
self._write_to_oggopus(silence_pcm)
|
1377
sbapp/pyogg/opus.py
Normal file
1377
sbapp/pyogg/opus.py
Normal file
File diff suppressed because it is too large
Load Diff
407
sbapp/pyogg/opus_buffered_encoder.py
Normal file
407
sbapp/pyogg/opus_buffered_encoder.py
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
import copy
|
||||||
|
import ctypes
|
||||||
|
from typing import Optional, ByteString, List, Tuple, Callable
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from . import opus
|
||||||
|
from .opus_encoder import OpusEncoder
|
||||||
|
from .pyogg_error import PyOggError
|
||||||
|
|
||||||
|
class OpusBufferedEncoder(OpusEncoder):
|
||||||
|
# TODO: This could be made more efficient. We don't need a
|
||||||
|
# deque. Instead, we need only sufficient PCM storage for one
|
||||||
|
# whole packet. We know the size of the packet thanks to
|
||||||
|
# set_frame_size().
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._frame_size_ms: Optional[float] = None
|
||||||
|
self._frame_size_bytes: Optional[int] = None
|
||||||
|
|
||||||
|
# Buffer contains the bytes required for the next
|
||||||
|
# frame.
|
||||||
|
self._buffer: Optional[ctypes.Array] = None
|
||||||
|
|
||||||
|
# Location of the next free byte in the buffer
|
||||||
|
self._buffer_index = 0
|
||||||
|
|
||||||
|
|
||||||
|
def set_frame_size(self, frame_size: float) -> None:
|
||||||
|
""" Set the desired frame duration (in milliseconds).
|
||||||
|
|
||||||
|
Valid options are 2.5, 5, 10, 20, 40, or 60ms.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Ensure the frame size is valid. Compare frame size in
|
||||||
|
# units of 0.1ms to avoid floating point comparison
|
||||||
|
if int(frame_size*10) not in [25, 50, 100, 200, 400, 600]:
|
||||||
|
raise PyOggError(
|
||||||
|
"Frame size ({:f}) not one of ".format(frame_size)+
|
||||||
|
"the acceptable values"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._frame_size_ms = frame_size
|
||||||
|
|
||||||
|
self._calc_frame_size()
|
||||||
|
|
||||||
|
|
||||||
|
def set_sampling_frequency(self, samples_per_second: int) -> None:
|
||||||
|
super().set_sampling_frequency(samples_per_second)
|
||||||
|
self._calc_frame_size()
|
||||||
|
|
||||||
|
|
||||||
|
def buffered_encode(self,
|
||||||
|
pcm_bytes: memoryview,
|
||||||
|
flush: bool = False,
|
||||||
|
callback: Callable[[memoryview,int,bool],None] = None
|
||||||
|
) -> List[Tuple[memoryview, int, bool]]:
|
||||||
|
"""Gets encoded packets and their number of samples.
|
||||||
|
|
||||||
|
This method returns a list, where each item in the list is
|
||||||
|
a tuple. The first item in the tuple is an Opus-encoded
|
||||||
|
frame stored as a bytes-object. The second item in the
|
||||||
|
tuple is the number of samples encoded (excluding
|
||||||
|
silence).
|
||||||
|
|
||||||
|
If `callback` is supplied then this method will instead
|
||||||
|
return an empty list but call the callback for every
|
||||||
|
Opus-encoded frame that would have been returned as a
|
||||||
|
list. This option has the desireable property of
|
||||||
|
eliminating the copying of the encoded packets, which is
|
||||||
|
required in order to form a list. The callback should
|
||||||
|
take two arguments, the encoded frame (a Python bytes
|
||||||
|
object) and the number of samples encoded per channel (an
|
||||||
|
int). The user must either process or copy the data as
|
||||||
|
the data may be overwritten once the callback terminates.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# If there's no work to do return immediately
|
||||||
|
if len(pcm_bytes) == 0 and flush == False:
|
||||||
|
return [] # no work to do
|
||||||
|
|
||||||
|
# Sanity checks
|
||||||
|
if self._frame_size_ms is None:
|
||||||
|
raise PyOggError("Frame size must be set before encoding")
|
||||||
|
assert self._frame_size_bytes is not None
|
||||||
|
assert self._channels is not None
|
||||||
|
assert self._buffer is not None
|
||||||
|
assert self._buffer_index is not None
|
||||||
|
|
||||||
|
# Local variable initialisation
|
||||||
|
results = []
|
||||||
|
pcm_index = 0
|
||||||
|
pcm_len = len(pcm_bytes)
|
||||||
|
|
||||||
|
# 'Cast' memoryview of PCM to ctypes Array
|
||||||
|
Buffer = ctypes.c_ubyte * len(pcm_bytes)
|
||||||
|
try:
|
||||||
|
pcm_ctypes = Buffer.from_buffer(pcm_bytes)
|
||||||
|
except TypeError:
|
||||||
|
warnings.warn(
|
||||||
|
"Because PCM was read-only, an extra memory "+
|
||||||
|
"copy was required; consider storing PCM in "+
|
||||||
|
"writable memory (for example, bytearray "+
|
||||||
|
"rather than bytes)."
|
||||||
|
)
|
||||||
|
pcm_ctypes = Buffer.from_buffer(pcm_bytes)
|
||||||
|
|
||||||
|
# Either store the encoded packet to return at the end of the
|
||||||
|
# method or immediately call the callback with the encoded
|
||||||
|
# packet.
|
||||||
|
def store_or_callback(encoded_packet: memoryview,
|
||||||
|
samples: int,
|
||||||
|
end_of_stream: bool = False) -> None:
|
||||||
|
if callback is None:
|
||||||
|
# Store the result
|
||||||
|
results.append((
|
||||||
|
encoded_packet,
|
||||||
|
samples,
|
||||||
|
end_of_stream
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
# Call the callback
|
||||||
|
callback(
|
||||||
|
encoded_packet,
|
||||||
|
samples,
|
||||||
|
end_of_stream
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fill the remainder of the buffer with silence and encode it.
|
||||||
|
# The associated number of samples are only that of actual
|
||||||
|
# data, not the added silence.
|
||||||
|
def flush_buffer() -> None:
|
||||||
|
# Sanity checks to satisfy mypy
|
||||||
|
assert self._buffer_index is not None
|
||||||
|
assert self._channels is not None
|
||||||
|
assert self._buffer is not None
|
||||||
|
|
||||||
|
# If the buffer is already empty, we have no work to do
|
||||||
|
if self._buffer_index == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Store the number of samples currently in the buffer
|
||||||
|
samples = (
|
||||||
|
self._buffer_index
|
||||||
|
// self._channels
|
||||||
|
// ctypes.sizeof(opus.opus_int16)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fill the buffer with silence
|
||||||
|
ctypes.memset(
|
||||||
|
# destination
|
||||||
|
ctypes.byref(self._buffer, self._buffer_index),
|
||||||
|
# value
|
||||||
|
0,
|
||||||
|
# count
|
||||||
|
len(self._buffer) - self._buffer_index
|
||||||
|
)
|
||||||
|
|
||||||
|
# Encode the PCM
|
||||||
|
# As at 2020-11-05, mypy is unaware that ctype Arrays
|
||||||
|
# support the buffer protocol.
|
||||||
|
encoded_packet = self.encode(memoryview(self._buffer)) # type: ignore
|
||||||
|
|
||||||
|
# Either store the encoded packet or call the
|
||||||
|
# callback
|
||||||
|
store_or_callback(encoded_packet, samples, True)
|
||||||
|
|
||||||
|
|
||||||
|
# Copy the data remaining from the provided PCM into the
|
||||||
|
# buffer. Flush if required.
|
||||||
|
def copy_insufficient_data() -> None:
|
||||||
|
# Sanity checks to satisfy mypy
|
||||||
|
assert self._buffer is not None
|
||||||
|
|
||||||
|
# Calculate remaining data
|
||||||
|
remaining_data = len(pcm_bytes) - pcm_index
|
||||||
|
|
||||||
|
# Copy the data into the buffer.
|
||||||
|
ctypes.memmove(
|
||||||
|
# destination
|
||||||
|
ctypes.byref(self._buffer, self._buffer_index),
|
||||||
|
# source
|
||||||
|
ctypes.byref(pcm_ctypes, pcm_index),
|
||||||
|
# count
|
||||||
|
remaining_data
|
||||||
|
)
|
||||||
|
|
||||||
|
self._buffer_index += remaining_data
|
||||||
|
|
||||||
|
# If we've been asked to flush the buffer then do so
|
||||||
|
if flush:
|
||||||
|
flush_buffer()
|
||||||
|
|
||||||
|
# Loop through the provided PCM and the current buffer,
|
||||||
|
# encoding as we have full packets.
|
||||||
|
while True:
|
||||||
|
# There are two possibilities at this point: either we
|
||||||
|
# have previously unencoded data still in the buffer or we
|
||||||
|
# do not
|
||||||
|
if self._buffer_index == 0:
|
||||||
|
# We do not have unencoded data
|
||||||
|
|
||||||
|
# We are free to progress through the PCM that has
|
||||||
|
# been provided encoding frames without copying any
|
||||||
|
# bytes. Once there is insufficient data remaining
|
||||||
|
# for a complete frame, that data should be copied
|
||||||
|
# into the buffer and we have finished.
|
||||||
|
if pcm_len - pcm_index > self._frame_size_bytes:
|
||||||
|
# We have enough data remaining in the provided
|
||||||
|
# PCM to encode more than an entire frame without
|
||||||
|
# copying any data. Unfortunately, splicing a
|
||||||
|
# ctypes array copies the array. To avoid the
|
||||||
|
# copy we use memoryview see
|
||||||
|
# https://mattgwwalker.wordpress.com/2020/12/12/python-ctypes-slicing/
|
||||||
|
frame_data = memoryview(pcm_bytes)[
|
||||||
|
pcm_index:pcm_index+self._frame_size_bytes
|
||||||
|
]
|
||||||
|
|
||||||
|
# Update the PCM index
|
||||||
|
pcm_index += self._frame_size_bytes
|
||||||
|
|
||||||
|
# Store number of samples (per channel) of actual
|
||||||
|
# data
|
||||||
|
samples = (
|
||||||
|
len(frame_data)
|
||||||
|
// self._channels
|
||||||
|
// ctypes.sizeof(opus.opus_int16)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Encode the PCM
|
||||||
|
encoded_packet = super().encode(frame_data)
|
||||||
|
|
||||||
|
# Either store the encoded packet or call the
|
||||||
|
# callback
|
||||||
|
store_or_callback(encoded_packet, samples)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# We do not have enough data to fill a frame while
|
||||||
|
# still having data left over. Copy the data into
|
||||||
|
# the buffer.
|
||||||
|
copy_insufficient_data()
|
||||||
|
return results
|
||||||
|
|
||||||
|
else:
|
||||||
|
# We have unencoded data.
|
||||||
|
|
||||||
|
# Copy the provided PCM into the buffer (up until the
|
||||||
|
# buffer is full). If we can fill it, then we can
|
||||||
|
# encode the filled buffer and continue. If we can't
|
||||||
|
# fill it then we've finished.
|
||||||
|
data_required = len(self._buffer) - self._buffer_index
|
||||||
|
if pcm_len > data_required:
|
||||||
|
# We have sufficient data to fill the buffer and
|
||||||
|
# have data left over. Copy data into the buffer.
|
||||||
|
assert pcm_index == 0
|
||||||
|
remaining = len(self._buffer) - self._buffer_index
|
||||||
|
ctypes.memmove(
|
||||||
|
# destination
|
||||||
|
ctypes.byref(self._buffer, self._buffer_index),
|
||||||
|
# source
|
||||||
|
pcm_ctypes,
|
||||||
|
# count
|
||||||
|
remaining
|
||||||
|
)
|
||||||
|
pcm_index += remaining
|
||||||
|
self._buffer_index += remaining
|
||||||
|
assert self._buffer_index == len(self._buffer)
|
||||||
|
|
||||||
|
# Encode the PCM
|
||||||
|
encoded_packet = super().encode(
|
||||||
|
# Memoryviews of ctypes do work, even though
|
||||||
|
# mypy complains.
|
||||||
|
memoryview(self._buffer) # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store number of samples (per channel) of actual
|
||||||
|
# data
|
||||||
|
samples = (
|
||||||
|
self._buffer_index
|
||||||
|
// self._channels
|
||||||
|
// ctypes.sizeof(opus.opus_int16)
|
||||||
|
)
|
||||||
|
|
||||||
|
# We've now processed the buffer
|
||||||
|
self._buffer_index = 0
|
||||||
|
|
||||||
|
# Either store the encoded packet or call the
|
||||||
|
# callback
|
||||||
|
store_or_callback(encoded_packet, samples)
|
||||||
|
else:
|
||||||
|
# We have insufficient data to fill the buffer
|
||||||
|
# while still having data left over. Copy the
|
||||||
|
# data into the buffer.
|
||||||
|
copy_insufficient_data()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _calc_frame_size(self):
|
||||||
|
"""Calculates the number of bytes in a frame.
|
||||||
|
|
||||||
|
If the frame size (in milliseconds) and the number of
|
||||||
|
samples per seconds have already been specified, then the
|
||||||
|
frame size in bytes is set. Otherwise, this method does
|
||||||
|
nothing.
|
||||||
|
|
||||||
|
The frame size is measured in bytes required to store the
|
||||||
|
sample.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if (self._frame_size_ms is None
|
||||||
|
or self._samples_per_second is None):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._frame_size_bytes = (
|
||||||
|
self._frame_size_ms
|
||||||
|
* self._samples_per_second
|
||||||
|
// 1000
|
||||||
|
* ctypes.sizeof(opus.opus_int16)
|
||||||
|
* self._channels
|
||||||
|
)
|
||||||
|
|
||||||
|
# Allocate space for the buffer
|
||||||
|
Buffer = ctypes.c_ubyte * self._frame_size_bytes
|
||||||
|
self._buffer = Buffer()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_next_frame(self, add_silence=False):
|
||||||
|
"""Gets the next Opus-encoded frame.
|
||||||
|
|
||||||
|
Returns a tuple where the first item is the Opus-encoded
|
||||||
|
frame and the second item is the number of encoded samples
|
||||||
|
(per channel).
|
||||||
|
|
||||||
|
Returns None if insufficient data is available.
|
||||||
|
|
||||||
|
"""
|
||||||
|
next_frame = bytes()
|
||||||
|
samples = 0
|
||||||
|
|
||||||
|
# Ensure frame size has been specified
|
||||||
|
if self._frame_size_bytes is None:
|
||||||
|
raise PyOggError(
|
||||||
|
"Desired frame size hasn't been set. Perhaps "+
|
||||||
|
"encode() was called before set_frame_size() "+
|
||||||
|
"and set_sampling_frequency()?"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if there's insufficient data in the buffer to fill
|
||||||
|
# a frame.
|
||||||
|
if self._frame_size_bytes > self._buffer_size:
|
||||||
|
if len(self._buffer) == 0:
|
||||||
|
# No data at all in buffer
|
||||||
|
return None
|
||||||
|
if add_silence:
|
||||||
|
# Get all remaining data
|
||||||
|
while len(self._buffer) != 0:
|
||||||
|
next_frame += self._buffer.popleft()
|
||||||
|
self._buffer_size = 0
|
||||||
|
# Store number of samples (per channel) of actual
|
||||||
|
# data
|
||||||
|
samples = (
|
||||||
|
len(next_frame)
|
||||||
|
// self._channels
|
||||||
|
// ctypes.sizeof(opus.opus_int16)
|
||||||
|
)
|
||||||
|
# Fill remainder of frame with silence
|
||||||
|
bytes_remaining = self._frame_size_bytes - len(next_frame)
|
||||||
|
next_frame += b'\x00' * bytes_remaining
|
||||||
|
return (next_frame, samples)
|
||||||
|
else:
|
||||||
|
# Insufficient data to fill a frame and we're not
|
||||||
|
# adding silence
|
||||||
|
return None
|
||||||
|
|
||||||
|
bytes_remaining = self._frame_size_bytes
|
||||||
|
while bytes_remaining > 0:
|
||||||
|
if len(self._buffer[0]) <= bytes_remaining:
|
||||||
|
# Take the whole first item
|
||||||
|
buffer_ = self._buffer.popleft()
|
||||||
|
next_frame += buffer_
|
||||||
|
bytes_remaining -= len(buffer_)
|
||||||
|
self._buffer_size -= len(buffer_)
|
||||||
|
else:
|
||||||
|
# Take only part of the buffer
|
||||||
|
|
||||||
|
# TODO: This could be more efficiently
|
||||||
|
# implemented. Rather than appending back the
|
||||||
|
# remaining data, we could just update an index
|
||||||
|
# saying where we were up to in regards to the
|
||||||
|
# first entry of the buffer.
|
||||||
|
buffer_ = self._buffer.popleft()
|
||||||
|
next_frame += buffer_[:bytes_remaining]
|
||||||
|
self._buffer_size -= bytes_remaining
|
||||||
|
# And put the unused part back into the buffer
|
||||||
|
self._buffer.appendleft(buffer_[bytes_remaining:])
|
||||||
|
bytes_remaining = 0
|
||||||
|
|
||||||
|
# Calculate number of samples (per channel)
|
||||||
|
samples = (
|
||||||
|
len(next_frame)
|
||||||
|
// self._channels
|
||||||
|
// ctypes.sizeof(opus.opus_int16)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (next_frame, samples)
|
273
sbapp/pyogg/opus_decoder.py
Normal file
273
sbapp/pyogg/opus_decoder.py
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
import ctypes
|
||||||
|
|
||||||
|
from . import opus
|
||||||
|
from .pyogg_error import PyOggError
|
||||||
|
|
||||||
|
class OpusDecoder:
|
||||||
|
def __init__(self):
|
||||||
|
self._decoder = None
|
||||||
|
self._channels = None
|
||||||
|
self._samples_per_second = None
|
||||||
|
self._pcm_buffer = None
|
||||||
|
self._pcm_buffer_ptr = None
|
||||||
|
self._pcm_buffer_size_int = None
|
||||||
|
|
||||||
|
# TODO: Check if there is clean up that we need to do when
|
||||||
|
# closing a decoder.
|
||||||
|
|
||||||
|
#
|
||||||
|
# User visible methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def set_channels(self, n):
|
||||||
|
|
||||||
|
"""Set the number of channels.
|
||||||
|
|
||||||
|
n must be either 1 or 2.
|
||||||
|
|
||||||
|
The decoder is capable of filling in either mono or
|
||||||
|
interleaved stereo pcm buffers.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self._decoder is None:
|
||||||
|
if n < 0 or n > 2:
|
||||||
|
raise PyOggError(
|
||||||
|
"Invalid number of channels in call to "+
|
||||||
|
"set_channels()"
|
||||||
|
)
|
||||||
|
self._channels = n
|
||||||
|
else:
|
||||||
|
raise PyOggError(
|
||||||
|
"Cannot change the number of channels after "+
|
||||||
|
"the decoder was created. Perhaps "+
|
||||||
|
"set_channels() was called after decode()?"
|
||||||
|
)
|
||||||
|
self._create_pcm_buffer()
|
||||||
|
|
||||||
|
def set_sampling_frequency(self, samples_per_second):
|
||||||
|
"""Set the number of samples (per channel) per second.
|
||||||
|
|
||||||
|
samples_per_second must be one of 8000, 12000, 16000,
|
||||||
|
24000, or 48000.
|
||||||
|
|
||||||
|
Internally Opus stores data at 48000 Hz, so that should be
|
||||||
|
the default value for Fs. However, the decoder can
|
||||||
|
efficiently decode to buffers at 8, 12, 16, and 24 kHz so
|
||||||
|
if for some reason the caller cannot use data at the full
|
||||||
|
sample rate, or knows the compressed data doesn't use the
|
||||||
|
full frequency range, it can request decoding at a reduced
|
||||||
|
rate.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self._decoder is None:
|
||||||
|
if samples_per_second in [8000, 12000, 16000, 24000, 48000]:
|
||||||
|
self._samples_per_second = samples_per_second
|
||||||
|
else:
|
||||||
|
raise PyOggError(
|
||||||
|
"Specified sampling frequency "+
|
||||||
|
"({:d}) ".format(samples_per_second)+
|
||||||
|
"was not one of the accepted values"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise PyOggError(
|
||||||
|
"Cannot change the sampling frequency after "+
|
||||||
|
"the decoder was created. Perhaps "+
|
||||||
|
"set_sampling_frequency() was called after decode()?"
|
||||||
|
)
|
||||||
|
self._create_pcm_buffer()
|
||||||
|
|
||||||
|
def decode(self, encoded_bytes: memoryview):
|
||||||
|
"""Decodes an Opus-encoded packet into PCM.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# If we haven't already created a decoder, do so now
|
||||||
|
if self._decoder is None:
|
||||||
|
self._decoder = self._create_decoder()
|
||||||
|
|
||||||
|
# Create a ctypes array from the memoryview (without copying
|
||||||
|
# data)
|
||||||
|
Buffer = ctypes.c_char * len(encoded_bytes)
|
||||||
|
encoded_bytes_ctypes = Buffer.from_buffer(encoded_bytes)
|
||||||
|
|
||||||
|
# Create pointer to encoded bytes
|
||||||
|
encoded_bytes_ptr = ctypes.cast(
|
||||||
|
encoded_bytes_ctypes,
|
||||||
|
ctypes.POINTER(ctypes.c_ubyte)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store length of encoded bytes into int32
|
||||||
|
len_int32 = opus.opus_int32(
|
||||||
|
len(encoded_bytes)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that we have a PCM buffer
|
||||||
|
if self._pcm_buffer is None:
|
||||||
|
raise PyOggError("PCM buffer was not configured.")
|
||||||
|
|
||||||
|
# Decode the encoded frame
|
||||||
|
result = opus.opus_decode(
|
||||||
|
self._decoder,
|
||||||
|
encoded_bytes_ptr,
|
||||||
|
len_int32,
|
||||||
|
self._pcm_buffer_ptr,
|
||||||
|
self._pcm_buffer_size_int,
|
||||||
|
0 # TODO: What's Forward Error Correction about?
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for any errors
|
||||||
|
if result < 0:
|
||||||
|
raise PyOggError(
|
||||||
|
"An error occurred while decoding an Opus-encoded "+
|
||||||
|
"packet: "+
|
||||||
|
opus.opus_strerror(result).decode("utf")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract just the valid data as bytes
|
||||||
|
end_valid_data = (
|
||||||
|
result
|
||||||
|
* ctypes.sizeof(opus.opus_int16)
|
||||||
|
* self._channels
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create memoryview of PCM buffer to avoid copying data during slice.
|
||||||
|
mv = memoryview(self._pcm_buffer)
|
||||||
|
|
||||||
|
# Cast memoryview to chars
|
||||||
|
mv = mv.cast('c')
|
||||||
|
|
||||||
|
# Slice memoryview to extract only valid data
|
||||||
|
mv = mv[:end_valid_data]
|
||||||
|
|
||||||
|
return mv
|
||||||
|
|
||||||
|
|
||||||
|
def decode_missing_packet(self, frame_duration):
|
||||||
|
""" Obtain PCM data despite missing a frame.
|
||||||
|
|
||||||
|
frame_duration is in milliseconds.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Consider frame duration in units of 0.1ms in order to
|
||||||
|
# avoid floating-point comparisons.
|
||||||
|
if int(frame_duration*10) not in [25, 50, 100, 200, 400, 600]:
|
||||||
|
raise PyOggError(
|
||||||
|
"Frame duration ({:f}) is not one of the accepted values".format(frame_duration)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate frame size
|
||||||
|
frame_size = int(
|
||||||
|
frame_duration
|
||||||
|
* self._samples_per_second
|
||||||
|
// 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store frame size as int
|
||||||
|
frame_size_int = ctypes.c_int(frame_size)
|
||||||
|
|
||||||
|
# Decode missing packet
|
||||||
|
result = opus.opus_decode(
|
||||||
|
self._decoder,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
self._pcm_buffer_ptr,
|
||||||
|
frame_size_int,
|
||||||
|
0 # TODO: What is this Forward Error Correction about?
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for any errors
|
||||||
|
if result < 0:
|
||||||
|
raise PyOggError(
|
||||||
|
"An error occurred while decoding an Opus-encoded "+
|
||||||
|
"packet: "+
|
||||||
|
opus.opus_strerror(result).decode("utf")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract just the valid data as bytes
|
||||||
|
end_valid_data = (
|
||||||
|
result
|
||||||
|
* ctypes.sizeof(opus.opus_int16)
|
||||||
|
* self._channels
|
||||||
|
)
|
||||||
|
return bytes(self._pcm_buffer)[:end_valid_data]
|
||||||
|
|
||||||
|
#
|
||||||
|
# Internal methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def _create_pcm_buffer(self):
|
||||||
|
if (self._samples_per_second is None
|
||||||
|
or self._channels is None):
|
||||||
|
# We cannot define the buffer yet
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create buffer to hold 120ms of samples. See "opus_decode()" at
|
||||||
|
# https://opus-codec.org/docs/opus_api-1.3.1/group__opus__decoder.html
|
||||||
|
max_duration = 120 # milliseconds
|
||||||
|
max_samples = max_duration * self._samples_per_second // 1000
|
||||||
|
PCMBuffer = opus.opus_int16 * (max_samples * self._channels)
|
||||||
|
self._pcm_buffer = PCMBuffer()
|
||||||
|
self._pcm_buffer_ptr = (
|
||||||
|
ctypes.cast(ctypes.pointer(self._pcm_buffer),
|
||||||
|
ctypes.POINTER(opus.opus_int16))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store samples per channel in an int
|
||||||
|
self._pcm_buffer_size_int = ctypes.c_int(max_samples)
|
||||||
|
|
||||||
|
def _create_decoder(self):
|
||||||
|
# To create a decoder, we must first allocate resources for it.
|
||||||
|
# We want Python to be responsible for the memory deallocation,
|
||||||
|
# and thus Python must be responsible for the initial memory
|
||||||
|
# allocation.
|
||||||
|
|
||||||
|
# Check that the sampling frequency has been defined
|
||||||
|
if self._samples_per_second is None:
|
||||||
|
raise PyOggError(
|
||||||
|
"The sampling frequency was not specified before "+
|
||||||
|
"attempting to create an Opus decoder. Perhaps "+
|
||||||
|
"decode() was called before set_sampling_frequency()?"
|
||||||
|
)
|
||||||
|
|
||||||
|
# The sampling frequency must be passed in as a 32-bit int
|
||||||
|
samples_per_second = opus.opus_int32(self._samples_per_second)
|
||||||
|
|
||||||
|
# Check that the number of channels has been defined
|
||||||
|
if self._channels is None:
|
||||||
|
raise PyOggError(
|
||||||
|
"The number of channels were not specified before "+
|
||||||
|
"attempting to create an Opus decoder. Perhaps "+
|
||||||
|
"decode() was called before set_channels()?"
|
||||||
|
)
|
||||||
|
|
||||||
|
# The number of channels must also be passed in as a 32-bit int
|
||||||
|
channels = opus.opus_int32(self._channels)
|
||||||
|
|
||||||
|
# Obtain the number of bytes of memory required for the decoder
|
||||||
|
size = opus.opus_decoder_get_size(channels);
|
||||||
|
|
||||||
|
# Allocate the required memory for the decoder
|
||||||
|
memory = ctypes.create_string_buffer(size)
|
||||||
|
|
||||||
|
# Cast the newly-allocated memory as a pointer to a decoder. We
|
||||||
|
# could also have used opus.od_p as the pointer type, but writing
|
||||||
|
# it out in full may be clearer.
|
||||||
|
decoder = ctypes.cast(memory, ctypes.POINTER(opus.OpusDecoder))
|
||||||
|
|
||||||
|
# Initialise the decoder
|
||||||
|
error = opus.opus_decoder_init(
|
||||||
|
decoder,
|
||||||
|
samples_per_second,
|
||||||
|
channels
|
||||||
|
);
|
||||||
|
|
||||||
|
# Check that there hasn't been an error when initialising the
|
||||||
|
# decoder
|
||||||
|
if error != opus.OPUS_OK:
|
||||||
|
raise PyOggError(
|
||||||
|
"An error occurred while creating the decoder: "+
|
||||||
|
opus.opus_strerror(error).decode("utf")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return our newly-created decoder
|
||||||
|
return decoder
|
358
sbapp/pyogg/opus_encoder.py
Normal file
358
sbapp/pyogg/opus_encoder.py
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
import ctypes
|
||||||
|
from typing import Optional, Union, ByteString
|
||||||
|
|
||||||
|
from . import opus
|
||||||
|
from .pyogg_error import PyOggError
|
||||||
|
|
||||||
|
class OpusEncoder:
|
||||||
|
"""Encodes PCM data into Opus frames."""
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._encoder: Optional[ctypes.pointer] = None
|
||||||
|
self._channels: Optional[int] = None
|
||||||
|
self._samples_per_second: Optional[int] = None
|
||||||
|
self._application: Optional[int] = None
|
||||||
|
self._max_bytes_per_frame: Optional[opus.opus_int32] = None
|
||||||
|
self._output_buffer: Optional[ctypes.Array] = None
|
||||||
|
self._output_buffer_ptr: Optional[ctypes.pointer] = None
|
||||||
|
|
||||||
|
# An output buffer of 4,000 bytes is recommended in
|
||||||
|
# https://opus-codec.org/docs/opus_api-1.3.1/group__opus__encoder.html
|
||||||
|
self.set_max_bytes_per_frame(4000)
|
||||||
|
|
||||||
|
#
|
||||||
|
# User visible methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def set_channels(self, n: int) -> None:
|
||||||
|
"""Set the number of channels.
|
||||||
|
|
||||||
|
n must be either 1 or 2.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self._encoder is None:
|
||||||
|
if n < 0 or n > 2:
|
||||||
|
raise PyOggError(
|
||||||
|
"Invalid number of channels in call to "+
|
||||||
|
"set_channels()"
|
||||||
|
)
|
||||||
|
self._channels = n
|
||||||
|
else:
|
||||||
|
raise PyOggError(
|
||||||
|
"Cannot change the number of channels after "+
|
||||||
|
"the encoder was created. Perhaps "+
|
||||||
|
"set_channels() was called after encode()?"
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_sampling_frequency(self, samples_per_second: int) -> None:
|
||||||
|
"""Set the number of samples (per channel) per second.
|
||||||
|
|
||||||
|
This must be one of 8000, 12000, 16000, 24000, or 48000.
|
||||||
|
|
||||||
|
Regardless of the sampling rate and number of channels
|
||||||
|
selected, the Opus encoder can switch to a lower audio
|
||||||
|
bandwidth or number of channels if the bitrate selected is
|
||||||
|
too low. This also means that it is safe to always use 48
|
||||||
|
kHz stereo input and let the encoder optimize the
|
||||||
|
encoding.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self._encoder is None:
|
||||||
|
if samples_per_second in [8000, 12000, 16000, 24000, 48000]:
|
||||||
|
self._samples_per_second = samples_per_second
|
||||||
|
else:
|
||||||
|
raise PyOggError(
|
||||||
|
"Specified sampling frequency "+
|
||||||
|
"({:d}) ".format(samples_per_second)+
|
||||||
|
"was not one of the accepted values"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise PyOggError(
|
||||||
|
"Cannot change the sampling frequency after "+
|
||||||
|
"the encoder was created. Perhaps "+
|
||||||
|
"set_sampling_frequency() was called after encode()?"
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_application(self, application: str) -> None:
|
||||||
|
"""Set the encoding mode.
|
||||||
|
|
||||||
|
This must be one of 'voip', 'audio', or 'restricted_lowdelay'.
|
||||||
|
|
||||||
|
'voip': Gives best quality at a given bitrate for voice
|
||||||
|
signals. It enhances the input signal by high-pass
|
||||||
|
filtering and emphasizing formants and
|
||||||
|
harmonics. Optionally it includes in-band forward error
|
||||||
|
correction to protect against packet loss. Use this mode
|
||||||
|
for typical VoIP applications. Because of the enhancement,
|
||||||
|
even at high bitrates the output may sound different from
|
||||||
|
the input.
|
||||||
|
|
||||||
|
'audio': Gives best quality at a given bitrate for most
|
||||||
|
non-voice signals like music. Use this mode for music and
|
||||||
|
mixed (music/voice) content, broadcast, and applications
|
||||||
|
requiring less than 15 ms of coding delay.
|
||||||
|
|
||||||
|
'restricted_lowdelay': configures low-delay mode that
|
||||||
|
disables the speech-optimized mode in exchange for
|
||||||
|
slightly reduced delay. This mode can only be set on an
|
||||||
|
newly initialized encoder because it changes the codec
|
||||||
|
delay.
|
||||||
|
"""
|
||||||
|
if self._encoder is not None:
|
||||||
|
raise PyOggError(
|
||||||
|
"Cannot change the application after "+
|
||||||
|
"the encoder was created. Perhaps "+
|
||||||
|
"set_application() was called after encode()?"
|
||||||
|
)
|
||||||
|
if application == "voip":
|
||||||
|
self._application = opus.OPUS_APPLICATION_VOIP
|
||||||
|
elif application == "audio":
|
||||||
|
self._application = opus.OPUS_APPLICATION_AUDIO
|
||||||
|
elif application == "restricted_lowdelay":
|
||||||
|
self._application = opus.OPUS_APPLICATION_RESTRICTED_LOWDELAY
|
||||||
|
else:
|
||||||
|
raise PyOggError(
|
||||||
|
"The application specification '{:s}' ".format(application)+
|
||||||
|
"wasn't one of the accepted values."
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_max_bytes_per_frame(self, max_bytes: int) -> None:
|
||||||
|
"""Set the maximum number of bytes in an encoded frame.
|
||||||
|
|
||||||
|
Size of the output payload. This may be used to impose an
|
||||||
|
upper limit on the instant bitrate, but should not be used
|
||||||
|
as the only bitrate control.
|
||||||
|
|
||||||
|
TODO: Use OPUS_SET_BITRATE to control the bitrate.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._max_bytes_per_frame = opus.opus_int32(max_bytes)
|
||||||
|
OutputBuffer = ctypes.c_ubyte * max_bytes
|
||||||
|
self._output_buffer = OutputBuffer()
|
||||||
|
self._output_buffer_ptr = (
|
||||||
|
ctypes.cast(ctypes.pointer(self._output_buffer),
|
||||||
|
ctypes.POINTER(ctypes.c_ubyte))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def encode(self, pcm: Union[bytes, bytearray, memoryview]) -> memoryview:
|
||||||
|
"""Encodes PCM data into an Opus frame.
|
||||||
|
|
||||||
|
`pcm` must be formatted as bytes-like, with each sample taking
|
||||||
|
two bytes (signed 16-bit integers; interleaved left, then
|
||||||
|
right channels if in stereo).
|
||||||
|
|
||||||
|
If `pcm` is not writeable, a copy of the array will be made.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# If we haven't already created an encoder, do so now
|
||||||
|
if self._encoder is None:
|
||||||
|
self._encoder = self._create_encoder()
|
||||||
|
|
||||||
|
# Sanity checks also satisfy mypy type checking
|
||||||
|
assert self._channels is not None
|
||||||
|
assert self._samples_per_second is not None
|
||||||
|
assert self._output_buffer is not None
|
||||||
|
|
||||||
|
# Calculate the effective frame duration of the given PCM
|
||||||
|
# data. Calculate it in units of 0.1ms in order to avoid
|
||||||
|
# floating point comparisons.
|
||||||
|
bytes_per_sample = 2
|
||||||
|
frame_size = (
|
||||||
|
len(pcm) # bytes
|
||||||
|
// bytes_per_sample
|
||||||
|
// self._channels
|
||||||
|
)
|
||||||
|
frame_duration = (
|
||||||
|
(10*frame_size)
|
||||||
|
// (self._samples_per_second//1000)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that we have a valid frame size
|
||||||
|
if int(frame_duration) not in [25, 50, 100, 200, 400, 600]:
|
||||||
|
raise PyOggError(
|
||||||
|
"The effective frame duration ({:.1f} ms) "
|
||||||
|
.format(frame_duration/10)+
|
||||||
|
"was not one of the acceptable values."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a ctypes object sharing the memory of the PCM data
|
||||||
|
PcmCtypes = ctypes.c_ubyte * len(pcm)
|
||||||
|
try:
|
||||||
|
# Attempt to share the PCM memory
|
||||||
|
|
||||||
|
# Unfortunately, as at 2020-09-27, the type hinting for
|
||||||
|
# read-only and writeable buffer protocols was a
|
||||||
|
# work-in-progress. The following only works for writable
|
||||||
|
# cases, but the method's parameters include a read-only
|
||||||
|
# possibility (bytes), thus we ignore mypy's error.
|
||||||
|
pcm_ctypes = PcmCtypes.from_buffer(pcm) # type: ignore[arg-type]
|
||||||
|
except TypeError:
|
||||||
|
# The data must be copied if it's not writeable
|
||||||
|
pcm_ctypes = PcmCtypes.from_buffer_copy(pcm)
|
||||||
|
|
||||||
|
# Create a pointer to the PCM data
|
||||||
|
pcm_ptr = ctypes.cast(
|
||||||
|
pcm_ctypes,
|
||||||
|
ctypes.POINTER(opus.opus_int16)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an int giving the frame size per channel
|
||||||
|
frame_size_int = ctypes.c_int(frame_size)
|
||||||
|
|
||||||
|
# Encode PCM
|
||||||
|
result = opus.opus_encode(
|
||||||
|
self._encoder,
|
||||||
|
pcm_ptr,
|
||||||
|
frame_size_int,
|
||||||
|
self._output_buffer_ptr,
|
||||||
|
self._max_bytes_per_frame
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for any errors
|
||||||
|
if result < 0:
|
||||||
|
raise PyOggError(
|
||||||
|
"An error occurred while encoding to Opus format: "+
|
||||||
|
opus.opus_strerror(result).decode("utf")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get memoryview of buffer so that the slice operation doesn't
|
||||||
|
# copy the data.
|
||||||
|
#
|
||||||
|
# Unfortunately, as at 2020-09-27, the type hints for
|
||||||
|
# memoryview do not include ctype arrays. This is because
|
||||||
|
# there is no currently accepted manner to label a class as
|
||||||
|
# supporting the buffer protocol. However, it's clearly a
|
||||||
|
# work in progress. For more information, see:
|
||||||
|
# * https://bugs.python.org/issue27501
|
||||||
|
# * https://github.com/python/typing/issues/593
|
||||||
|
# * https://github.com/python/typeshed/pull/4232
|
||||||
|
mv = memoryview(self._output_buffer) # type: ignore
|
||||||
|
|
||||||
|
# Cast the memoryview to char
|
||||||
|
mv = mv.cast('c')
|
||||||
|
|
||||||
|
# Slice just the valid data from the memoryview
|
||||||
|
valid_data_as_bytes = mv[:result]
|
||||||
|
|
||||||
|
# DEBUG
|
||||||
|
# Convert memoryview back to ctypes instance
|
||||||
|
Buffer = ctypes.c_ubyte * len(valid_data_as_bytes)
|
||||||
|
buf = Buffer.from_buffer( valid_data_as_bytes )
|
||||||
|
|
||||||
|
# Convert PCM back to pointer and dump 4,000-byte buffer
|
||||||
|
ptr = ctypes.cast(
|
||||||
|
buf,
|
||||||
|
ctypes.POINTER(ctypes.c_ubyte)
|
||||||
|
)
|
||||||
|
|
||||||
|
return valid_data_as_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def get_algorithmic_delay(self):
|
||||||
|
"""Gets the total samples of delay added by the entire codec.
|
||||||
|
|
||||||
|
This can be queried by the encoder and then the provided
|
||||||
|
number of samples can be skipped on from the start of the
|
||||||
|
decoder's output to provide time aligned input and
|
||||||
|
output. From the perspective of a decoding application the
|
||||||
|
real data begins this many samples late.
|
||||||
|
|
||||||
|
The decoder contribution to this delay is identical for all
|
||||||
|
decoders, but the encoder portion of the delay may vary from
|
||||||
|
implementation to implementation, version to version, or even
|
||||||
|
depend on the encoder's initial configuration. Applications
|
||||||
|
needing delay compensation should call this method rather than
|
||||||
|
hard-coding a value.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# If we haven't already created an encoder, do so now
|
||||||
|
if self._encoder is None:
|
||||||
|
self._encoder = self._create_encoder()
|
||||||
|
|
||||||
|
# Obtain the algorithmic delay of the Opus encoder. See
|
||||||
|
# https://tools.ietf.org/html/rfc7845#page-27
|
||||||
|
delay = opus.opus_int32()
|
||||||
|
|
||||||
|
result = opus.opus_encoder_ctl(
|
||||||
|
self._encoder,
|
||||||
|
opus.OPUS_GET_LOOKAHEAD_REQUEST,
|
||||||
|
ctypes.pointer(delay)
|
||||||
|
)
|
||||||
|
if result != opus.OPUS_OK:
|
||||||
|
raise PyOggError(
|
||||||
|
"Failed to obtain the algorithmic delay of "+
|
||||||
|
"the Opus encoder: "+
|
||||||
|
opus.opus_strerror(result).decode("utf")
|
||||||
|
)
|
||||||
|
delay_samples = delay.value
|
||||||
|
return delay_samples
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Internal methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def _create_encoder(self) -> ctypes.pointer:
|
||||||
|
# To create an encoder, we must first allocate resources for it.
|
||||||
|
# We want Python to be responsible for the memory deallocation,
|
||||||
|
# and thus Python must be responsible for the initial memory
|
||||||
|
# allocation.
|
||||||
|
|
||||||
|
# Check that the application has been defined
|
||||||
|
if self._application is None:
|
||||||
|
raise PyOggError(
|
||||||
|
"The application was not specified before "+
|
||||||
|
"attempting to create an Opus encoder. Perhaps "+
|
||||||
|
"encode() was called before set_application()?"
|
||||||
|
)
|
||||||
|
application = self._application
|
||||||
|
|
||||||
|
# Check that the sampling frequency has been defined
|
||||||
|
if self._samples_per_second is None:
|
||||||
|
raise PyOggError(
|
||||||
|
"The sampling frequency was not specified before "+
|
||||||
|
"attempting to create an Opus encoder. Perhaps "+
|
||||||
|
"encode() was called before set_sampling_frequency()?"
|
||||||
|
)
|
||||||
|
|
||||||
|
# The frequency must be passed in as a 32-bit int
|
||||||
|
samples_per_second = opus.opus_int32(self._samples_per_second)
|
||||||
|
|
||||||
|
# Check that the number of channels has been defined
|
||||||
|
if self._channels is None:
|
||||||
|
raise PyOggError(
|
||||||
|
"The number of channels were not specified before "+
|
||||||
|
"attempting to create an Opus encoder. Perhaps "+
|
||||||
|
"encode() was called before set_channels()?"
|
||||||
|
)
|
||||||
|
channels = self._channels
|
||||||
|
|
||||||
|
# Obtain the number of bytes of memory required for the encoder
|
||||||
|
size = opus.opus_encoder_get_size(channels);
|
||||||
|
|
||||||
|
# Allocate the required memory for the encoder
|
||||||
|
memory = ctypes.create_string_buffer(size)
|
||||||
|
|
||||||
|
# Cast the newly-allocated memory as a pointer to an encoder. We
|
||||||
|
# could also have used opus.oe_p as the pointer type, but writing
|
||||||
|
# it out in full may be clearer.
|
||||||
|
encoder = ctypes.cast(memory, ctypes.POINTER(opus.OpusEncoder))
|
||||||
|
|
||||||
|
# Initialise the encoder
|
||||||
|
error = opus.opus_encoder_init(
|
||||||
|
encoder,
|
||||||
|
samples_per_second,
|
||||||
|
channels,
|
||||||
|
application
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that there hasn't been an error when initialising the
|
||||||
|
# encoder
|
||||||
|
if error != opus.OPUS_OK:
|
||||||
|
raise PyOggError(
|
||||||
|
"An error occurred while creating the encoder: "+
|
||||||
|
opus.opus_strerror(error).decode("utf")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return our newly-created encoder
|
||||||
|
return encoder
|
106
sbapp/pyogg/opus_file.py
Normal file
106
sbapp/pyogg/opus_file.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import ctypes
|
||||||
|
|
||||||
|
from . import ogg
|
||||||
|
from . import opus
|
||||||
|
from .pyogg_error import PyOggError
|
||||||
|
from .audio_file import AudioFile
|
||||||
|
|
||||||
|
class OpusFile(AudioFile):
|
||||||
|
def __init__(self, path: str) -> None:
|
||||||
|
# Open the file
|
||||||
|
error = ctypes.c_int()
|
||||||
|
of = opus.op_open_file(
|
||||||
|
ogg.to_char_p(path),
|
||||||
|
ctypes.pointer(error)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
if error.value != 0:
|
||||||
|
raise PyOggError(
|
||||||
|
("File '{}' couldn't be opened or doesn't exist. "+
|
||||||
|
"Error code: {}").format(path, error.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract the number of channels in the newly opened file
|
||||||
|
#: Number of channels in audio file.
|
||||||
|
self.channels = opus.op_channel_count(of, -1)
|
||||||
|
|
||||||
|
# Allocate sufficient memory to store the entire PCM
|
||||||
|
pcm_size = opus.op_pcm_total(of, -1)
|
||||||
|
Buf = opus.opus_int16*(pcm_size*self.channels)
|
||||||
|
buf = Buf()
|
||||||
|
|
||||||
|
# Create a pointer to the newly allocated memory. It
|
||||||
|
# seems we can only do pointer arithmetic on void
|
||||||
|
# pointers. See
|
||||||
|
# https://mattgwwalker.wordpress.com/2020/05/30/pointer-manipulation-in-python/
|
||||||
|
buf_ptr = ctypes.cast(
|
||||||
|
ctypes.pointer(buf),
|
||||||
|
ctypes.c_void_p
|
||||||
|
)
|
||||||
|
assert buf_ptr.value is not None # for mypy
|
||||||
|
buf_ptr_zero = buf_ptr.value
|
||||||
|
|
||||||
|
#: Bytes per sample
|
||||||
|
self.bytes_per_sample = ctypes.sizeof(opus.opus_int16)
|
||||||
|
|
||||||
|
# Read through the entire file, copying the PCM into the
|
||||||
|
# buffer
|
||||||
|
samples = 0
|
||||||
|
while True:
|
||||||
|
# Calculate remaining buffer size
|
||||||
|
remaining_buffer = (
|
||||||
|
len(buf) # int
|
||||||
|
- (buf_ptr.value
|
||||||
|
- buf_ptr_zero) // self.bytes_per_sample
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert buffer pointer to the desired type
|
||||||
|
ptr = ctypes.cast(
|
||||||
|
buf_ptr,
|
||||||
|
ctypes.POINTER(opus.opus_int16)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read the next section of PCM
|
||||||
|
ns = opus.op_read(
|
||||||
|
of,
|
||||||
|
ptr,
|
||||||
|
remaining_buffer,
|
||||||
|
ogg.c_int_p()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
if ns<0:
|
||||||
|
raise PyOggError(
|
||||||
|
"Error while reading OggOpus file. "+
|
||||||
|
"Error code: {}".format(ns)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Increment the pointer
|
||||||
|
buf_ptr.value += (
|
||||||
|
ns
|
||||||
|
* self.bytes_per_sample
|
||||||
|
* self.channels
|
||||||
|
)
|
||||||
|
assert buf_ptr.value is not None # for mypy
|
||||||
|
|
||||||
|
samples += ns
|
||||||
|
|
||||||
|
# Check if we've finished
|
||||||
|
if ns==0:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Close the open file
|
||||||
|
opus.op_free(of)
|
||||||
|
|
||||||
|
# Opus files are always stored at 48k samples per second
|
||||||
|
#: Number of samples per second (per channel). Always 48,000.
|
||||||
|
self.frequency = 48000
|
||||||
|
|
||||||
|
# Cast buffer to a one-dimensional array of chars
|
||||||
|
#: Raw PCM data from audio file.
|
||||||
|
CharBuffer = (
|
||||||
|
ctypes.c_byte
|
||||||
|
* (self.bytes_per_sample * self.channels * pcm_size)
|
||||||
|
)
|
||||||
|
self.buffer = CharBuffer.from_buffer(buf)
|
127
sbapp/pyogg/opus_file_stream.py
Normal file
127
sbapp/pyogg/opus_file_stream.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import ctypes
|
||||||
|
|
||||||
|
from . import ogg
|
||||||
|
from . import opus
|
||||||
|
from .pyogg_error import PyOggError
|
||||||
|
|
||||||
|
class OpusFileStream:
|
||||||
|
def __init__(self, path):
|
||||||
|
"""Opens an OggOpus file as a stream.
|
||||||
|
|
||||||
|
path should be a string giving the filename of the file to
|
||||||
|
open. Unicode file names may not work correctly.
|
||||||
|
|
||||||
|
An exception will be raised if the file cannot be opened
|
||||||
|
correctly.
|
||||||
|
|
||||||
|
"""
|
||||||
|
error = ctypes.c_int()
|
||||||
|
|
||||||
|
self.of = opus.op_open_file(ogg.to_char_p(path), ctypes.pointer(error))
|
||||||
|
|
||||||
|
if error.value != 0:
|
||||||
|
self.of = None
|
||||||
|
raise PyOggError("file couldn't be opened or doesn't exist. Error code : {}".format(error.value))
|
||||||
|
|
||||||
|
#: Number of channels in audio file
|
||||||
|
self.channels = opus.op_channel_count(self.of, -1)
|
||||||
|
|
||||||
|
#: Total PCM Length
|
||||||
|
self.pcm_size = opus.op_pcm_total(self.of, -1)
|
||||||
|
|
||||||
|
#: Number of samples per second (per channel)
|
||||||
|
self.frequency = 48000
|
||||||
|
|
||||||
|
# The buffer size should be (per channel) large enough to
|
||||||
|
# hold 120ms (the largest possible Opus frame) at 48kHz.
|
||||||
|
# See https://opus-codec.org/docs/opusfile_api-0.7/group__stream__decoding.html#ga963c917749335e29bb2b698c1cb20a10
|
||||||
|
self.buffer_size = self.frequency // 1000 * 120 * self.channels
|
||||||
|
self.Buf = opus.opus_int16 * self.buffer_size
|
||||||
|
self._buf = self.Buf()
|
||||||
|
self.buffer_ptr = ctypes.cast(
|
||||||
|
ctypes.pointer(self._buf),
|
||||||
|
opus.opus_int16_p
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Bytes per sample
|
||||||
|
self.bytes_per_sample = ctypes.sizeof(opus.opus_int16)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self.of is not None:
|
||||||
|
opus.op_free(self.of)
|
||||||
|
|
||||||
|
def get_buffer(self):
|
||||||
|
"""Obtains the next frame of PCM samples.
|
||||||
|
|
||||||
|
Returns an array of signed 16-bit integers. If the file
|
||||||
|
is in stereo, the left and right channels are interleaved.
|
||||||
|
|
||||||
|
Returns None when all data has been read.
|
||||||
|
|
||||||
|
The array that is returned should be either processed or
|
||||||
|
copied before the next call to :meth:`~get_buffer` or
|
||||||
|
:meth:`~get_buffer_as_array` as the array's memory is reused for
|
||||||
|
each call.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Read the next frame
|
||||||
|
samples_read = opus.op_read(
|
||||||
|
self.of,
|
||||||
|
self.buffer_ptr,
|
||||||
|
self.buffer_size,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
if samples_read < 0:
|
||||||
|
raise PyOggError(
|
||||||
|
"Failed to read OpusFileStream. Error {:d}".format(samples_read)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if we've reached the end of the stream
|
||||||
|
if samples_read == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Cast the pointer to opus_int16 to an array of the
|
||||||
|
# correct size
|
||||||
|
result_ptr = ctypes.cast(
|
||||||
|
self.buffer_ptr,
|
||||||
|
ctypes.POINTER(opus.opus_int16 * (samples_read*self.channels))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert the array to Python bytes
|
||||||
|
return bytes(result_ptr.contents)
|
||||||
|
|
||||||
|
def get_buffer_as_array(self):
|
||||||
|
"""Provides the buffer as a NumPy array.
|
||||||
|
|
||||||
|
Note that the underlying data type is 16-bit signed
|
||||||
|
integers.
|
||||||
|
|
||||||
|
Does not copy the underlying data, so the returned array
|
||||||
|
should either be processed or copied before the next call
|
||||||
|
to :meth:`~get_buffer` or :meth:`~get_buffer_as_array`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import numpy # type: ignore
|
||||||
|
|
||||||
|
# Read the next samples from the stream
|
||||||
|
buf = self.get_buffer()
|
||||||
|
|
||||||
|
# Check if we've come to the end of the stream
|
||||||
|
if buf is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Convert the bytes buffer to a NumPy array
|
||||||
|
array = numpy.frombuffer(
|
||||||
|
buf,
|
||||||
|
dtype=numpy.int16
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape the array
|
||||||
|
return array.reshape(
|
||||||
|
(len(buf)
|
||||||
|
// self.bytes_per_sample
|
||||||
|
// self.channels,
|
||||||
|
self.channels)
|
||||||
|
)
|
1
sbapp/pyogg/py.typed
Normal file
1
sbapp/pyogg/py.typed
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Marker file for PEP 561. This package uses inline types.
|
2
sbapp/pyogg/pyogg_error.py
Normal file
2
sbapp/pyogg/pyogg_error.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
class PyOggError(Exception):
|
||||||
|
pass
|
855
sbapp/pyogg/vorbis.py
Normal file
855
sbapp/pyogg/vorbis.py
Normal file
@ -0,0 +1,855 @@
|
|||||||
|
############################################################
|
||||||
|
# Vorbis license: #
|
||||||
|
############################################################
|
||||||
|
"""
|
||||||
|
Copyright (c) 2002-2015 Xiph.org Foundation
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
- Neither the name of the Xiph.org Foundation nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
|
||||||
|
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import ctypes.util
|
||||||
|
from traceback import print_exc as _print_exc
|
||||||
|
import os
|
||||||
|
|
||||||
|
OV_EXCLUDE_STATIC_CALLBACKS = False
|
||||||
|
|
||||||
|
__MINGW32__ = False
|
||||||
|
|
||||||
|
_WIN32 = False
|
||||||
|
|
||||||
|
from .ogg import *
|
||||||
|
|
||||||
|
from .library_loader import ExternalLibrary, ExternalLibraryError
|
||||||
|
|
||||||
|
__here = os.getcwd()
|
||||||
|
|
||||||
|
libvorbis = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
names = {
|
||||||
|
"Windows": "libvorbis.dll",
|
||||||
|
"Darwin": "libvorbis.0.dylib",
|
||||||
|
"external": "vorbis"
|
||||||
|
}
|
||||||
|
libvorbis = Library.load(names, tests = [lambda lib: hasattr(lib, "vorbis_info_init")])
|
||||||
|
except ExternalLibraryError:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
_print_exc()
|
||||||
|
|
||||||
|
libvorbisfile = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
names = {
|
||||||
|
"Windows": "libvorbisfile.dll",
|
||||||
|
"Darwin": "libvorbisfile.3.dylib",
|
||||||
|
"external": "vorbisfile"
|
||||||
|
}
|
||||||
|
libvorbisfile = Library.load(names, tests = [lambda lib: hasattr(lib, "ov_clear")])
|
||||||
|
except ExternalLibraryError:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
_print_exc()
|
||||||
|
|
||||||
|
libvorbisenc = None
|
||||||
|
|
||||||
|
# In some cases, libvorbis may also have the libvorbisenc functionality.
|
||||||
|
libvorbis_is_also_libvorbisenc = True
|
||||||
|
|
||||||
|
for f in ("vorbis_encode_ctl",
|
||||||
|
"vorbis_encode_init",
|
||||||
|
"vorbis_encode_init_vbr",
|
||||||
|
"vorbis_encode_setup_init",
|
||||||
|
"vorbis_encode_setup_managed",
|
||||||
|
"vorbis_encode_setup_vbr"):
|
||||||
|
if not hasattr(libvorbis, f):
|
||||||
|
libvorbis_is_also_libvorbisenc = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if libvorbis_is_also_libvorbisenc:
|
||||||
|
libvorbisenc = libvorbis
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
names = {
|
||||||
|
"Windows": "libvorbisenc.dll",
|
||||||
|
"Darwin": "libvorbisenc.2.dylib",
|
||||||
|
"external": "vorbisenc"
|
||||||
|
}
|
||||||
|
libvorbisenc = Library.load(names, tests = [lambda lib: hasattr(lib, "vorbis_encode_init")])
|
||||||
|
except ExternalLibraryError:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
_print_exc()
|
||||||
|
|
||||||
|
if libvorbis is None:
|
||||||
|
PYOGG_VORBIS_AVAIL = False
|
||||||
|
else:
|
||||||
|
PYOGG_VORBIS_AVAIL = True
|
||||||
|
|
||||||
|
if libvorbisfile is None:
|
||||||
|
PYOGG_VORBIS_FILE_AVAIL = False
|
||||||
|
else:
|
||||||
|
PYOGG_VORBIS_FILE_AVAIL = True
|
||||||
|
|
||||||
|
if libvorbisenc is None:
|
||||||
|
PYOGG_VORBIS_ENC_AVAIL = False
|
||||||
|
else:
|
||||||
|
PYOGG_VORBIS_ENC_AVAIL = True
|
||||||
|
|
||||||
|
# FIXME: What's the story with the lack of checking for PYOGG_VORBIS_ENC_AVAIL?
|
||||||
|
# We just seem to assume that it's available.
|
||||||
|
|
||||||
|
if PYOGG_OGG_AVAIL and PYOGG_VORBIS_AVAIL and PYOGG_VORBIS_FILE_AVAIL:
|
||||||
|
# Sanity check also satisfies mypy type checking
|
||||||
|
assert libogg is not None
|
||||||
|
assert libvorbis is not None
|
||||||
|
assert libvorbisfile is not None
|
||||||
|
|
||||||
|
|
||||||
|
# codecs
|
||||||
|
class vorbis_info(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct vorbis_info vorbis_info;
|
||||||
|
"""
|
||||||
|
_fields_ = [("version", c_int),
|
||||||
|
("channels", c_int),
|
||||||
|
("rate", c_long),
|
||||||
|
|
||||||
|
("bitrate_upper", c_long),
|
||||||
|
("bitrate_nominal", c_long),
|
||||||
|
("bitrate_lower", c_long),
|
||||||
|
("bitrate_window", c_long),
|
||||||
|
("codec_setup", c_void_p)]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class vorbis_dsp_state(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct vorbis_dsp_state vorbis_dsp_state;
|
||||||
|
"""
|
||||||
|
_fields_ = [("analysisp", c_int),
|
||||||
|
("vi", POINTER(vorbis_info)),
|
||||||
|
("pcm", c_float_p_p),
|
||||||
|
("pcmret", c_float_p_p),
|
||||||
|
("pcm_storage", c_int),
|
||||||
|
("pcm_current", c_int),
|
||||||
|
("pcm_returned", c_int),
|
||||||
|
|
||||||
|
("preextrapolate", c_int),
|
||||||
|
("eofflag", c_int),
|
||||||
|
|
||||||
|
("lW", c_long),
|
||||||
|
("W", c_long),
|
||||||
|
("nW", c_long),
|
||||||
|
("centerW", c_long),
|
||||||
|
|
||||||
|
("granulepos", ogg_int64_t),
|
||||||
|
("sequence", ogg_int64_t),
|
||||||
|
|
||||||
|
("glue_bits", ogg_int64_t),
|
||||||
|
("time_bits", ogg_int64_t),
|
||||||
|
("floor_bits", ogg_int64_t),
|
||||||
|
("res_bits", ogg_int64_t),
|
||||||
|
|
||||||
|
("backend_state", c_void_p)]
|
||||||
|
|
||||||
|
class alloc_chain(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct alloc_chain;
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
alloc_chain._fields_ = [("ptr", c_void_p),
|
||||||
|
("next", POINTER(alloc_chain))]
|
||||||
|
|
||||||
|
class vorbis_block(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct vorbis_block vorbis_block;
|
||||||
|
"""
|
||||||
|
_fields_ = [("pcm", c_float_p_p),
|
||||||
|
("opb", oggpack_buffer),
|
||||||
|
("lW", c_long),
|
||||||
|
("W", c_long),
|
||||||
|
("nW", c_long),
|
||||||
|
("pcmend", c_int),
|
||||||
|
("mode", c_int),
|
||||||
|
|
||||||
|
("eofflag", c_int),
|
||||||
|
("granulepos", ogg_int64_t),
|
||||||
|
("sequence", ogg_int64_t),
|
||||||
|
("vd", POINTER(vorbis_dsp_state)),
|
||||||
|
|
||||||
|
("localstore", c_void_p),
|
||||||
|
("localtop", c_long),
|
||||||
|
("localalloc", c_long),
|
||||||
|
("totaluse", c_long),
|
||||||
|
("reap", POINTER(alloc_chain)),
|
||||||
|
|
||||||
|
("glue_bits", c_long),
|
||||||
|
("time_bits", c_long),
|
||||||
|
("floor_bits", c_long),
|
||||||
|
("res_bits", c_long),
|
||||||
|
|
||||||
|
("internal", c_void_p)]
|
||||||
|
|
||||||
|
class vorbis_comment(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct vorbis_comment vorbis_comment;
|
||||||
|
"""
|
||||||
|
_fields_ = [("user_comments", c_char_p_p),
|
||||||
|
("comment_lengths", c_int_p),
|
||||||
|
("comments", c_int),
|
||||||
|
("vendor", c_char_p)]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
vi_p = POINTER(vorbis_info)
|
||||||
|
vc_p = POINTER(vorbis_comment)
|
||||||
|
vd_p = POINTER(vorbis_dsp_state)
|
||||||
|
vb_p = POINTER(vorbis_block)
|
||||||
|
|
||||||
|
libvorbis.vorbis_info_init.restype = None
|
||||||
|
libvorbis.vorbis_info_init.argtypes = [vi_p]
|
||||||
|
def vorbis_info_init(vi):
|
||||||
|
libvorbis.vorbis_info_init(vi)
|
||||||
|
|
||||||
|
libvorbis.vorbis_info_clear.restype = None
|
||||||
|
libvorbis.vorbis_info_clear.argtypes = [vi_p]
|
||||||
|
def vorbis_info_clear(vi):
|
||||||
|
libvorbis.vorbis_info_clear(vi)
|
||||||
|
|
||||||
|
libvorbis.vorbis_info_blocksize.restype = c_int
|
||||||
|
libvorbis.vorbis_info_blocksize.argtypes = [vi_p, c_int]
|
||||||
|
def vorbis_info_blocksize(vi, zo):
|
||||||
|
return libvorbis.vorbis_info_blocksize(vi, zo)
|
||||||
|
|
||||||
|
libvorbis.vorbis_comment_init.restype = None
|
||||||
|
libvorbis.vorbis_comment_init.argtypes = [vc_p]
|
||||||
|
def vorbis_comment_init(vc):
|
||||||
|
libvorbis.vorbis_comment_init(vc)
|
||||||
|
|
||||||
|
libvorbis.vorbis_comment_add.restype = None
|
||||||
|
libvorbis.vorbis_comment_add.argtypes = [vc_p, c_char_p]
|
||||||
|
def vorbis_comment_add(vc, comment):
|
||||||
|
libvorbis.vorbis_comment_add(vc, comment)
|
||||||
|
|
||||||
|
libvorbis.vorbis_comment_add_tag.restype = None
|
||||||
|
libvorbis.vorbis_comment_add_tag.argtypes = [vc_p, c_char_p, c_char_p]
|
||||||
|
def vorbis_comment_add_tag(vc, tag, comment):
|
||||||
|
libvorbis.vorbis_comment_add_tag(vc, tag, comment)
|
||||||
|
|
||||||
|
libvorbis.vorbis_comment_query.restype = c_char_p
|
||||||
|
libvorbis.vorbis_comment_query.argtypes = [vc_p, c_char_p, c_int]
|
||||||
|
def vorbis_comment_query(vc, tag, count):
|
||||||
|
libvorbis.vorbis_comment_query(vc, tag, count)
|
||||||
|
|
||||||
|
libvorbis.vorbis_comment_query_count.restype = c_int
|
||||||
|
libvorbis.vorbis_comment_query_count.argtypes = [vc_p, c_char_p]
|
||||||
|
def vorbis_comment_query_count(vc, tag):
|
||||||
|
libvorbis.vorbis_comment_query_count(vc, tag)
|
||||||
|
|
||||||
|
libvorbis.vorbis_comment_clear.restype = None
|
||||||
|
libvorbis.vorbis_comment_clear.argtypes = [vc_p]
|
||||||
|
def vorbis_comment_clear(vc):
|
||||||
|
libvorbis.vorbis_comment_clear(vc)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbis.vorbis_block_init.restype = c_int
|
||||||
|
libvorbis.vorbis_block_init.argtypes = [vd_p, vb_p]
|
||||||
|
def vorbis_block_init(v,vb):
|
||||||
|
return libvorbis.vorbis_block_init(v,vb)
|
||||||
|
|
||||||
|
libvorbis.vorbis_block_clear.restype = c_int
|
||||||
|
libvorbis.vorbis_block_clear.argtypes = [vb_p]
|
||||||
|
def vorbis_block_clear(vb):
|
||||||
|
return libvorbis.vorbis_block_clear(vb)
|
||||||
|
|
||||||
|
libvorbis.vorbis_dsp_clear.restype = None
|
||||||
|
libvorbis.vorbis_dsp_clear.argtypes = [vd_p]
|
||||||
|
def vorbis_dsp_clear(v):
|
||||||
|
return libvorbis.vorbis_dsp_clear(v)
|
||||||
|
|
||||||
|
libvorbis.vorbis_granule_time.restype = c_double
|
||||||
|
libvorbis.vorbis_granule_time.argtypes = [vd_p, ogg_int64_t]
|
||||||
|
def vorbis_granule_time(v, granulepos):
|
||||||
|
return libvorbis.vorbis_granule_time(v, granulepos)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbis.vorbis_version_string.restype = c_char_p
|
||||||
|
libvorbis.vorbis_version_string.argtypes = []
|
||||||
|
def vorbis_version_string():
|
||||||
|
return libvorbis.vorbis_version_string()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbis.vorbis_analysis_init.restype = c_int
|
||||||
|
libvorbis.vorbis_analysis_init.argtypes = [vd_p, vi_p]
|
||||||
|
def vorbis_analysis_init(v, vi):
|
||||||
|
return libvorbis.vorbis_analysis_init(v, vi)
|
||||||
|
|
||||||
|
libvorbis.vorbis_commentheader_out.restype = c_int
|
||||||
|
libvorbis.vorbis_commentheader_out.argtypes = [vc_p, op_p]
|
||||||
|
def vorbis_commentheader_out(vc, op):
|
||||||
|
return libvorbis.vorbis_commentheader_out(vc, op)
|
||||||
|
|
||||||
|
libvorbis.vorbis_analysis_headerout.restype = c_int
|
||||||
|
libvorbis.vorbis_analysis_headerout.argtypes = [vd_p, vc_p, op_p, op_p, op_p]
|
||||||
|
def vorbis_analysis_headerout(v,vc, op, op_comm, op_code):
|
||||||
|
return libvorbis.vorbis_analysis_headerout(v,vc, op, op_comm, op_code)
|
||||||
|
|
||||||
|
libvorbis.vorbis_analysis_buffer.restype = c_float_p_p
|
||||||
|
libvorbis.vorbis_analysis_buffer.argtypes = [vd_p, c_int]
|
||||||
|
def vorbis_analysis_buffer(v, vals):
|
||||||
|
return libvorbis.vorbis_analysis_buffer(v, vals)
|
||||||
|
|
||||||
|
libvorbis.vorbis_analysis_wrote.restype = c_int
|
||||||
|
libvorbis.vorbis_analysis_wrote.argtypes = [vd_p, c_int]
|
||||||
|
def vorbis_analysis_wrote(v, vals):
|
||||||
|
return libvorbis.vorbis_analysis_wrote(v, vals)
|
||||||
|
|
||||||
|
libvorbis.vorbis_analysis_blockout.restype = c_int
|
||||||
|
libvorbis.vorbis_analysis_blockout.argtypes = [vd_p, vb_p]
|
||||||
|
def vorbis_analysis_blockout(v, vb):
|
||||||
|
return libvorbis.vorbis_analysis_blockout(v, vb)
|
||||||
|
|
||||||
|
libvorbis.vorbis_analysis.restype = c_int
|
||||||
|
libvorbis.vorbis_analysis.argtypes = [vb_p, op_p]
|
||||||
|
def vorbis_analysis(vb, op):
|
||||||
|
return libvorbis.vorbis_analysis(vb, op)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbis.vorbis_bitrate_addblock.restype = c_int
|
||||||
|
libvorbis.vorbis_bitrate_addblock.argtypes = [vb_p]
|
||||||
|
def vorbis_bitrate_addblock(vb):
|
||||||
|
return libvorbis.vorbis_bitrate_addblock(vb)
|
||||||
|
|
||||||
|
libvorbis.vorbis_bitrate_flushpacket.restype = c_int
|
||||||
|
libvorbis.vorbis_bitrate_flushpacket.argtypes = [vd_p, op_p]
|
||||||
|
def vorbis_bitrate_flushpacket(vd, op):
|
||||||
|
return libvorbis.vorbis_bitrate_flushpacket(vd, op)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis_idheader.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis_idheader.argtypes = [op_p]
|
||||||
|
def vorbis_synthesis_idheader(op):
|
||||||
|
return libvorbis.vorbis_synthesis_idheader(op)
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis_headerin.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis_headerin.argtypes = [vi_p, vc_p, op_p]
|
||||||
|
def vorbis_synthesis_headerin(vi, vc, op):
|
||||||
|
return libvorbis.vorbis_synthesis_headerin(vi, vc, op)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis_init.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis_init.argtypes = [vd_p, vi_p]
|
||||||
|
def vorbis_synthesis_init(v,vi):
|
||||||
|
return libvorbis.vorbis_synthesis_init(v,vi)
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis_restart.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis_restart.argtypes = [vd_p]
|
||||||
|
def vorbis_synthesis_restart(v):
|
||||||
|
return libvorbis.vorbis_synthesis_restart(v)
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis.argtypes = [vb_p, op_p]
|
||||||
|
def vorbis_synthesis(vb, op):
|
||||||
|
return libvorbis.vorbis_synthesis(vb, op)
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis_trackonly.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis_trackonly.argtypes = [vb_p, op_p]
|
||||||
|
def vorbis_synthesis_trackonly(vb, op):
|
||||||
|
return libvorbis.vorbis_synthesis_trackonly(vb, op)
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis_blockin.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis_blockin.argtypes = [vd_p, vb_p]
|
||||||
|
def vorbis_synthesis_blockin(v, vb):
|
||||||
|
return libvorbis.vorbis_synthesis_blockin(v, vb)
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis_pcmout.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis_pcmout.argtypes = [vd_p, c_float_p_p_p]
|
||||||
|
def vorbis_synthesis_pcmout(v, pcm):
|
||||||
|
return libvorbis.vorbis_synthesis_pcmout(v, pcm)
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis_lapout.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis_lapout.argtypes = [vd_p, c_float_p_p_p]
|
||||||
|
def vorbis_synthesis_lapout(v, pcm):
|
||||||
|
return libvorbis.vorbis_synthesis_lapout(v, pcm)
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis_read.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis_read.argtypes = [vd_p, c_int]
|
||||||
|
def vorbis_synthesis_read(v, samples):
|
||||||
|
return libvorbis.vorbis_synthesis_read(v, samples)
|
||||||
|
|
||||||
|
libvorbis.vorbis_packet_blocksize.restype = c_long
|
||||||
|
libvorbis.vorbis_packet_blocksize.argtypes = [vi_p, op_p]
|
||||||
|
def vorbis_packet_blocksize(vi, op):
|
||||||
|
return libvorbis.vorbis_packet_blocksize(vi, op)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis_halfrate.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis_halfrate.argtypes = [vi_p, c_int]
|
||||||
|
def vorbis_synthesis_halfrate(v, flag):
|
||||||
|
return libvorbis.vorbis_synthesis_halfrate(v, flag)
|
||||||
|
|
||||||
|
libvorbis.vorbis_synthesis_halfrate_p.restype = c_int
|
||||||
|
libvorbis.vorbis_synthesis_halfrate_p.argtypes = [vi_p]
|
||||||
|
def vorbis_synthesis_halfrate_p(vi):
|
||||||
|
return libvorbis.vorbis_synthesis_halfrate_p(vi)
|
||||||
|
|
||||||
|
OV_FALSE = -1
|
||||||
|
OV_EOF = -2
|
||||||
|
OV_HOLE = -3
|
||||||
|
|
||||||
|
OV_EREAD = -128
|
||||||
|
OV_EFAULT = -129
|
||||||
|
OV_EIMPL =-130
|
||||||
|
OV_EINVAL =-131
|
||||||
|
OV_ENOTVORBIS =-132
|
||||||
|
OV_EBADHEADER =-133
|
||||||
|
OV_EVERSION =-134
|
||||||
|
OV_ENOTAUDIO =-135
|
||||||
|
OV_EBADPACKET =-136
|
||||||
|
OV_EBADLINK =-137
|
||||||
|
OV_ENOSEEK =-138
|
||||||
|
# end of codecs
|
||||||
|
|
||||||
|
# vorbisfile
|
||||||
|
read_func = ctypes.CFUNCTYPE(c_size_t,
|
||||||
|
c_void_p,
|
||||||
|
c_size_t,
|
||||||
|
c_size_t,
|
||||||
|
c_void_p)
|
||||||
|
|
||||||
|
seek_func = ctypes.CFUNCTYPE(c_int,
|
||||||
|
c_void_p,
|
||||||
|
ogg_int64_t,
|
||||||
|
c_int)
|
||||||
|
|
||||||
|
close_func = ctypes.CFUNCTYPE(c_int,
|
||||||
|
c_void_p)
|
||||||
|
|
||||||
|
tell_func = ctypes.CFUNCTYPE(c_long,
|
||||||
|
c_void_p)
|
||||||
|
|
||||||
|
class ov_callbacks(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct ov_callbacks;
|
||||||
|
"""
|
||||||
|
|
||||||
|
_fields_ = [("read_func", read_func),
|
||||||
|
("seek_func", seek_func),
|
||||||
|
("close_func", close_func),
|
||||||
|
("tell_func", tell_func)]
|
||||||
|
|
||||||
|
NOTOPEN = 0
|
||||||
|
PARTOPEN = 1
|
||||||
|
OPENED = 2
|
||||||
|
STREAMSET = 3
|
||||||
|
INITSET = 4
|
||||||
|
|
||||||
|
class OggVorbis_File(ctypes.Structure):
|
||||||
|
"""
|
||||||
|
Wrapper for:
|
||||||
|
typedef struct OggVorbis_File OggVorbis_File;
|
||||||
|
"""
|
||||||
|
|
||||||
|
_fields_ = [("datasource", c_void_p),
|
||||||
|
("seekable", c_int),
|
||||||
|
("offset", ogg_int64_t),
|
||||||
|
("end", ogg_int64_t),
|
||||||
|
("oy", ogg_sync_state),
|
||||||
|
|
||||||
|
("links", c_int),
|
||||||
|
("offsets", ogg_int64_t_p),
|
||||||
|
("dataoffsets", ogg_int64_t_p),
|
||||||
|
("serialnos", c_long_p),
|
||||||
|
("pcmlengths", ogg_int64_t_p),
|
||||||
|
("vi", vi_p),
|
||||||
|
("vc", vc_p),
|
||||||
|
|
||||||
|
("pcm_offset", ogg_int64_t),
|
||||||
|
("ready_state", c_int),
|
||||||
|
("current_serialno", c_long),
|
||||||
|
("current_link", c_int),
|
||||||
|
|
||||||
|
("bittrack", c_double),
|
||||||
|
("samptrack", c_double),
|
||||||
|
|
||||||
|
("os", ogg_stream_state),
|
||||||
|
|
||||||
|
("vd", vorbis_dsp_state),
|
||||||
|
("vb", vorbis_block),
|
||||||
|
|
||||||
|
("callbacks", ov_callbacks)]
|
||||||
|
vf_p = POINTER(OggVorbis_File)
|
||||||
|
|
||||||
|
libvorbisfile.ov_clear.restype = c_int
|
||||||
|
libvorbisfile.ov_clear.argtypes = [vf_p]
|
||||||
|
|
||||||
|
def ov_clear(vf):
|
||||||
|
return libvorbisfile.ov_clear(vf)
|
||||||
|
|
||||||
|
libvorbisfile.ov_fopen.restype = c_int
|
||||||
|
libvorbisfile.ov_fopen.argtypes = [c_char_p, vf_p]
|
||||||
|
|
||||||
|
def ov_fopen(path, vf):
|
||||||
|
return libvorbisfile.ov_fopen(to_char_p(path), vf)
|
||||||
|
|
||||||
|
libvorbisfile.ov_open_callbacks.restype = c_int
|
||||||
|
libvorbisfile.ov_open_callbacks.argtypes = [c_void_p, vf_p, c_char_p, c_long, ov_callbacks]
|
||||||
|
|
||||||
|
def ov_open_callbacks(datasource, vf, initial, ibytes, callbacks):
|
||||||
|
return libvorbisfile.ov_open_callbacks(datasource, vf, initial, ibytes, callbacks)
|
||||||
|
|
||||||
|
def ov_open(*args, **kw):
|
||||||
|
raise PyOggError("ov_open is not supported, please use ov_fopen instead")
|
||||||
|
|
||||||
|
def ov_test(*args, **kw):
|
||||||
|
raise PyOggError("ov_test is not supported")
|
||||||
|
|
||||||
|
libvorbisfile.ov_test_callbacks.restype = c_int
|
||||||
|
libvorbisfile.ov_test_callbacks.argtypes = [c_void_p, vf_p, c_char_p, c_long, ov_callbacks]
|
||||||
|
|
||||||
|
def ov_test_callbacks(datasource, vf, initial, ibytes, callbacks):
|
||||||
|
return libvorbisfile.ov_test_callbacks(datasource, vf, initial, ibytes, callbacks)
|
||||||
|
|
||||||
|
libvorbisfile.ov_test_open.restype = c_int
|
||||||
|
libvorbisfile.ov_test_open.argtypes = [vf_p]
|
||||||
|
|
||||||
|
def ov_test_open(vf):
|
||||||
|
return libvorbisfile.ov_test_open(vf)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbisfile.ov_bitrate.restype = c_long
|
||||||
|
libvorbisfile.ov_bitrate.argtypes = [vf_p, c_int]
|
||||||
|
|
||||||
|
def ov_bitrate(vf, i):
|
||||||
|
return libvorbisfile.ov_bitrate(vf, i)
|
||||||
|
|
||||||
|
libvorbisfile.ov_bitrate_instant.restype = c_long
|
||||||
|
libvorbisfile.ov_bitrate_instant.argtypes = [vf_p]
|
||||||
|
|
||||||
|
def ov_bitrate_instant(vf):
|
||||||
|
return libvorbisfile.ov_bitrate_instant(vf)
|
||||||
|
|
||||||
|
libvorbisfile.ov_streams.restype = c_long
|
||||||
|
libvorbisfile.ov_streams.argtypes = [vf_p]
|
||||||
|
|
||||||
|
def ov_streams(vf):
|
||||||
|
return libvorbisfile.ov_streams(vf)
|
||||||
|
|
||||||
|
libvorbisfile.ov_seekable.restype = c_long
|
||||||
|
libvorbisfile.ov_seekable.argtypes = [vf_p]
|
||||||
|
|
||||||
|
def ov_seekable(vf):
|
||||||
|
return libvorbisfile.ov_seekable(vf)
|
||||||
|
|
||||||
|
libvorbisfile.ov_serialnumber.restype = c_long
|
||||||
|
libvorbisfile.ov_serialnumber.argtypes = [vf_p, c_int]
|
||||||
|
|
||||||
|
def ov_serialnumber(vf, i):
|
||||||
|
return libvorbisfile.ov_serialnumber(vf, i)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbisfile.ov_raw_total.restype = ogg_int64_t
|
||||||
|
libvorbisfile.ov_raw_total.argtypes = [vf_p, c_int]
|
||||||
|
|
||||||
|
def ov_raw_total(vf, i):
|
||||||
|
return libvorbisfile.ov_raw_total(vf, i)
|
||||||
|
|
||||||
|
libvorbisfile.ov_pcm_total.restype = ogg_int64_t
|
||||||
|
libvorbisfile.ov_pcm_total.argtypes = [vf_p, c_int]
|
||||||
|
|
||||||
|
def ov_pcm_total(vf, i):
|
||||||
|
return libvorbisfile.ov_pcm_total(vf, i)
|
||||||
|
|
||||||
|
libvorbisfile.ov_time_total.restype = c_double
|
||||||
|
libvorbisfile.ov_time_total.argtypes = [vf_p, c_int]
|
||||||
|
|
||||||
|
def ov_time_total(vf, i):
|
||||||
|
return libvorbisfile.ov_time_total(vf, i)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbisfile.ov_raw_seek.restype = c_int
|
||||||
|
libvorbisfile.ov_raw_seek.argtypes = [vf_p, ogg_int64_t]
|
||||||
|
|
||||||
|
def ov_raw_seek(vf, pos):
|
||||||
|
return libvorbisfile.ov_raw_seek(vf, pos)
|
||||||
|
|
||||||
|
libvorbisfile.ov_pcm_seek.restype = c_int
|
||||||
|
libvorbisfile.ov_pcm_seek.argtypes = [vf_p, ogg_int64_t]
|
||||||
|
|
||||||
|
def ov_pcm_seek(vf, pos):
|
||||||
|
return libvorbisfile.ov_pcm_seek(vf, pos)
|
||||||
|
|
||||||
|
libvorbisfile.ov_pcm_seek_page.restype = c_int
|
||||||
|
libvorbisfile.ov_pcm_seek_page.argtypes = [vf_p, ogg_int64_t]
|
||||||
|
|
||||||
|
def ov_pcm_seek_page(vf, pos):
|
||||||
|
return libvorbisfile.ov_pcm_seek_page(vf, pos)
|
||||||
|
|
||||||
|
libvorbisfile.ov_time_seek.restype = c_int
|
||||||
|
libvorbisfile.ov_time_seek.argtypes = [vf_p, c_double]
|
||||||
|
|
||||||
|
def ov_time_seek(vf, pos):
|
||||||
|
return libvorbisfile.ov_time_seek(vf, pos)
|
||||||
|
|
||||||
|
libvorbisfile.ov_time_seek_page.restype = c_int
|
||||||
|
libvorbisfile.ov_time_seek_page.argtypes = [vf_p, c_double]
|
||||||
|
|
||||||
|
def ov_time_seek_page(vf, pos):
|
||||||
|
return libvorbisfile.ov_time_seek_page(vf, pos)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbisfile.ov_raw_seek_lap.restype = c_int
|
||||||
|
libvorbisfile.ov_raw_seek_lap.argtypes = [vf_p, ogg_int64_t]
|
||||||
|
|
||||||
|
def ov_raw_seek_lap(vf, pos):
|
||||||
|
return libvorbisfile.ov_raw_seek_lap(vf, pos)
|
||||||
|
|
||||||
|
libvorbisfile.ov_pcm_seek_lap.restype = c_int
|
||||||
|
libvorbisfile.ov_pcm_seek_lap.argtypes = [vf_p, ogg_int64_t]
|
||||||
|
|
||||||
|
def ov_pcm_seek_lap(vf, pos):
|
||||||
|
return libvorbisfile.ov_pcm_seek_lap(vf, pos)
|
||||||
|
|
||||||
|
libvorbisfile.ov_pcm_seek_page_lap.restype = c_int
|
||||||
|
libvorbisfile.ov_pcm_seek_page_lap.argtypes = [vf_p, ogg_int64_t]
|
||||||
|
|
||||||
|
def ov_pcm_seek_page_lap(vf, pos):
|
||||||
|
return libvorbisfile.ov_pcm_seek_page_lap(vf, pos)
|
||||||
|
|
||||||
|
libvorbisfile.ov_time_seek_lap.restype = c_int
|
||||||
|
libvorbisfile.ov_time_seek_lap.argtypes = [vf_p, c_double]
|
||||||
|
|
||||||
|
def ov_time_seek_lap(vf, pos):
|
||||||
|
return libvorbisfile.ov_time_seek_lap(vf, pos)
|
||||||
|
|
||||||
|
libvorbisfile.ov_time_seek_page_lap.restype = c_int
|
||||||
|
libvorbisfile.ov_time_seek_page_lap.argtypes = [vf_p, c_double]
|
||||||
|
|
||||||
|
def ov_time_seek_page_lap(vf, pos):
|
||||||
|
return libvorbisfile.ov_time_seek_page_lap(vf, pos)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbisfile.ov_raw_tell.restype = ogg_int64_t
|
||||||
|
libvorbisfile.ov_raw_tell.argtypes = [vf_p]
|
||||||
|
|
||||||
|
def ov_raw_tell(vf):
|
||||||
|
return libvorbisfile.ov_raw_tell(vf)
|
||||||
|
|
||||||
|
libvorbisfile.ov_pcm_tell.restype = ogg_int64_t
|
||||||
|
libvorbisfile.ov_pcm_tell.argtypes = [vf_p]
|
||||||
|
|
||||||
|
def ov_pcm_tell(vf):
|
||||||
|
return libvorbisfile.ov_pcm_tell(vf)
|
||||||
|
|
||||||
|
libvorbisfile.ov_time_tell.restype = c_double
|
||||||
|
libvorbisfile.ov_time_tell.argtypes = [vf_p]
|
||||||
|
|
||||||
|
def ov_time_tell(vf):
|
||||||
|
return libvorbisfile.ov_time_tell(vf)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbisfile.ov_info.restype = vi_p
|
||||||
|
libvorbisfile.ov_info.argtypes = [vf_p, c_int]
|
||||||
|
|
||||||
|
def ov_info(vf, link):
|
||||||
|
return libvorbisfile.ov_info(vf, link)
|
||||||
|
|
||||||
|
libvorbisfile.ov_comment.restype = vc_p
|
||||||
|
libvorbisfile.ov_comment.argtypes = [vf_p, c_int]
|
||||||
|
|
||||||
|
def ov_comment(vf, link):
|
||||||
|
return libvorbisfile.ov_comment(vf, link)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbisfile.ov_read_float.restype = c_long
|
||||||
|
libvorbisfile.ov_read_float.argtypes = [vf_p, c_float_p_p_p, c_int, c_int_p]
|
||||||
|
|
||||||
|
def ov_read_float(vf, pcm_channels, samples, bitstream):
|
||||||
|
return libvorbisfile.ov_read_float(vf, pcm_channels, samples, bitstream)
|
||||||
|
|
||||||
|
filter_ = ctypes.CFUNCTYPE(None,
|
||||||
|
c_float_p_p,
|
||||||
|
c_long,
|
||||||
|
c_long,
|
||||||
|
c_void_p)
|
||||||
|
|
||||||
|
try:
|
||||||
|
libvorbisfile.ov_read_filter.restype = c_long
|
||||||
|
libvorbisfile.ov_read_filter.argtypes = [vf_p, c_char_p, c_int, c_int, c_int, c_int, c_int_p, filter_, c_void_p]
|
||||||
|
|
||||||
|
def ov_read_filter(vf, buffer, length, bigendianp, word, sgned, bitstream, filter_, filter_param):
|
||||||
|
return libvorbisfile.ov_read_filter(vf, buffer, length, bigendianp, word, sgned, bitstream, filter_, filter_param)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
libvorbisfile.ov_read.restype = c_long
|
||||||
|
libvorbisfile.ov_read.argtypes = [vf_p, c_char_p, c_int, c_int, c_int, c_int, c_int_p]
|
||||||
|
|
||||||
|
def ov_read(vf, buffer, length, bigendianp, word, sgned, bitstream):
|
||||||
|
return libvorbisfile.ov_read(vf, buffer, length, bigendianp, word, sgned, bitstream)
|
||||||
|
|
||||||
|
libvorbisfile.ov_crosslap.restype = c_int
|
||||||
|
libvorbisfile.ov_crosslap.argtypes = [vf_p, vf_p]
|
||||||
|
|
||||||
|
def ov_crosslap(vf1, cf2):
|
||||||
|
return libvorbisfile.ov_crosslap(vf1, vf2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
libvorbisfile.ov_halfrate.restype = c_int
|
||||||
|
libvorbisfile.ov_halfrate.argtypes = [vf_p, c_int]
|
||||||
|
|
||||||
|
def ov_halfrate(vf, flag):
|
||||||
|
return libvorbisfile.ov_halfrate(vf, flag)
|
||||||
|
|
||||||
|
libvorbisfile.ov_halfrate_p.restype = c_int
|
||||||
|
libvorbisfile.ov_halfrate_p.argtypes = [vf_p]
|
||||||
|
|
||||||
|
def ov_halfrate_p(vf):
|
||||||
|
return libvorbisfile.ov_halfrate_p(vf)
|
||||||
|
# end of vorbisfile
|
||||||
|
|
||||||
|
try:
|
||||||
|
# vorbisenc
|
||||||
|
|
||||||
|
# Sanity check also satisfies mypy type checking
|
||||||
|
assert libvorbisenc is not None
|
||||||
|
|
||||||
|
libvorbisenc.vorbis_encode_init.restype = c_int
|
||||||
|
libvorbisenc.vorbis_encode_init.argtypes = [vi_p, c_long, c_long, c_long, c_long, c_long]
|
||||||
|
|
||||||
|
def vorbis_encode_init(vi, channels, rate, max_bitrate, nominal_bitrate, min_bitrate):
|
||||||
|
return libvorbisenc.vorbis_encode_init(vi, channels, rate, max_bitrate, nominal_bitrate, min_bitrate)
|
||||||
|
|
||||||
|
libvorbisenc.vorbis_encode_setup_managed.restype = c_int
|
||||||
|
libvorbisenc.vorbis_encode_setup_managed.argtypes = [vi_p, c_long, c_long, c_long, c_long, c_long]
|
||||||
|
|
||||||
|
def vorbis_encode_setup_managed(vi, channels, rate, max_bitrate, nominal_bitrate, min_bitrate):
|
||||||
|
return libvorbisenc.vorbis_encode_setup_managed(vi, channels, rate, max_bitrate, nominal_bitrate, min_bitrate)
|
||||||
|
|
||||||
|
libvorbisenc.vorbis_encode_setup_vbr.restype = c_int
|
||||||
|
libvorbisenc.vorbis_encode_setup_vbr.argtypes = [vi_p, c_long, c_long, c_float]
|
||||||
|
|
||||||
|
def vorbis_encode_setup_vbr(vi, channels, rate, quality):
|
||||||
|
return libvorbisenc.vorbis_encode_setup_vbr(vi, channels, rate, quality)
|
||||||
|
|
||||||
|
libvorbisenc.vorbis_encode_init_vbr.restype = c_int
|
||||||
|
libvorbisenc.vorbis_encode_init_vbr.argtypes = [vi_p, c_long, c_long, c_float]
|
||||||
|
|
||||||
|
def vorbis_encode_init_vbr(vi, channels, rate, quality):
|
||||||
|
return libvorbisenc.vorbis_encode_init_vbr(vi, channels, rate, quality)
|
||||||
|
|
||||||
|
libvorbisenc.vorbis_encode_setup_init.restype = c_int
|
||||||
|
libvorbisenc.vorbis_encode_setup_init.argtypes = [vi_p]
|
||||||
|
|
||||||
|
def vorbis_encode_setup_init(vi):
|
||||||
|
return libvorbisenc.vorbis_encode_setup_init(vi)
|
||||||
|
|
||||||
|
libvorbisenc.vorbis_encode_ctl.restype = c_int
|
||||||
|
libvorbisenc.vorbis_encode_ctl.argtypes = [vi_p, c_int, c_void_p]
|
||||||
|
|
||||||
|
def vorbis_encode_ctl(vi, number, arg):
|
||||||
|
return libvorbisenc.vorbis_encode_ctl(vi, number, arg)
|
||||||
|
|
||||||
|
class ovectl_ratemanage_arg(ctypes.Structure):
|
||||||
|
_fields_ = [("management_active", c_int),
|
||||||
|
("bitrate_hard_min", c_long),
|
||||||
|
("bitrate_hard_max", c_long),
|
||||||
|
("bitrate_hard_window", c_double),
|
||||||
|
("bitrate_av_lo", c_long),
|
||||||
|
("bitrate_av_hi", c_long),
|
||||||
|
("bitrate_av_window", c_double),
|
||||||
|
("bitrate_av_window_center", c_double)]
|
||||||
|
|
||||||
|
class ovectl_ratemanage2_arg(ctypes.Structure):
|
||||||
|
_fields_ = [("management_active", c_int),
|
||||||
|
("bitrate_limit_min_kbps", c_long),
|
||||||
|
("bitrate_limit_max_kbps", c_long),
|
||||||
|
("bitrate_limit_reservoir_bits", c_long),
|
||||||
|
("bitrate_limit_reservoir_bias", c_double),
|
||||||
|
("bitrate_average_kbps", c_long),
|
||||||
|
("bitrate_average_damping", c_double)]
|
||||||
|
|
||||||
|
OV_ECTL_RATEMANAGE2_GET =0x14
|
||||||
|
|
||||||
|
OV_ECTL_RATEMANAGE2_SET =0x15
|
||||||
|
|
||||||
|
OV_ECTL_LOWPASS_GET =0x20
|
||||||
|
|
||||||
|
OV_ECTL_LOWPASS_SET =0x21
|
||||||
|
|
||||||
|
OV_ECTL_IBLOCK_GET =0x30
|
||||||
|
|
||||||
|
OV_ECTL_IBLOCK_SET =0x31
|
||||||
|
|
||||||
|
OV_ECTL_COUPLING_GET =0x40
|
||||||
|
|
||||||
|
OV_ECTL_COUPLING_SET =0x41
|
||||||
|
|
||||||
|
OV_ECTL_RATEMANAGE_GET =0x10
|
||||||
|
|
||||||
|
OV_ECTL_RATEMANAGE_SET =0x11
|
||||||
|
|
||||||
|
OV_ECTL_RATEMANAGE_AVG =0x12
|
||||||
|
|
||||||
|
OV_ECTL_RATEMANAGE_HARD =0x13
|
||||||
|
# end of vorbisenc
|
||||||
|
except:
|
||||||
|
pass
|
161
sbapp/pyogg/vorbis_file.py
Normal file
161
sbapp/pyogg/vorbis_file.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import ctypes
|
||||||
|
|
||||||
|
from . import vorbis
|
||||||
|
from .audio_file import AudioFile
|
||||||
|
from .pyogg_error import PyOggError
|
||||||
|
|
||||||
|
# TODO: Issue #70: Vorbis files with multiple logical bitstreams could
|
||||||
|
# be supported by chaining VorbisFile instances (with say a 'next'
|
||||||
|
# attribute that points to the next VorbisFile that would contain the
|
||||||
|
# PCM for the next logical bitstream). A considerable constraint to
|
||||||
|
# implementing this was that examples files that demonstrated multiple
|
||||||
|
# logical bitstreams couldn't be found or created. Note that even
|
||||||
|
# Audacity doesn't handle multiple logical bitstreams (see
|
||||||
|
# https://wiki.audacityteam.org/wiki/OGG#Importing_multiple_stream_files).
|
||||||
|
|
||||||
|
# TODO: Issue #53: Unicode file names are not well supported.
|
||||||
|
# They may work in macOS and Linux, they don't work under Windows.
|
||||||
|
|
||||||
|
class VorbisFile(AudioFile):
|
||||||
|
def __init__(self,
|
||||||
|
path: str,
|
||||||
|
bytes_per_sample: int = 2,
|
||||||
|
signed:bool = True) -> None:
|
||||||
|
"""Load an OggVorbis File.
|
||||||
|
|
||||||
|
path specifies the location of the Vorbis file. Unicode
|
||||||
|
filenames may not work correctly under Windows.
|
||||||
|
|
||||||
|
bytes_per_sample specifies the word size of the PCM. It may
|
||||||
|
be either 1 or 2. Specifying one byte per sample will save
|
||||||
|
memory but will likely decrease the quality of the decoded
|
||||||
|
audio.
|
||||||
|
|
||||||
|
Only Vorbis files with a single logical bitstream are
|
||||||
|
supported.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Sanity check the number of bytes per sample
|
||||||
|
assert bytes_per_sample==1 or bytes_per_sample==2
|
||||||
|
|
||||||
|
# Sanity check that the vorbis library is available (for mypy)
|
||||||
|
assert vorbis.libvorbisfile is not None
|
||||||
|
|
||||||
|
#: Bytes per sample
|
||||||
|
self.bytes_per_sample = bytes_per_sample
|
||||||
|
|
||||||
|
#: Samples are signed (rather than unsigned)
|
||||||
|
self.signed = signed
|
||||||
|
|
||||||
|
# Create a Vorbis File structure
|
||||||
|
vf = vorbis.OggVorbis_File()
|
||||||
|
|
||||||
|
# Attempt to open the Vorbis file
|
||||||
|
error = vorbis.libvorbisfile.ov_fopen(
|
||||||
|
vorbis.to_char_p(path),
|
||||||
|
ctypes.byref(vf)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for errors during opening
|
||||||
|
if error != 0:
|
||||||
|
raise PyOggError(
|
||||||
|
("File '{}' couldn't be opened or doesn't exist. "+
|
||||||
|
"Error code : {}").format(path, error)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract info from the Vorbis file
|
||||||
|
info = vorbis.libvorbisfile.ov_info(
|
||||||
|
ctypes.byref(vf),
|
||||||
|
-1 # the current logical bitstream
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Number of channels in audio file.
|
||||||
|
self.channels = info.contents.channels
|
||||||
|
|
||||||
|
#: Number of samples per second (per channel), 44100 for
|
||||||
|
# example.
|
||||||
|
self.frequency = info.contents.rate
|
||||||
|
|
||||||
|
# Extract the total number of PCM samples for the first
|
||||||
|
# logical bitstream
|
||||||
|
pcm_length_samples = vorbis.libvorbisfile.ov_pcm_total(
|
||||||
|
ctypes.byref(vf),
|
||||||
|
0 # to extract the length of the first logical bitstream
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a memory block to store the entire PCM
|
||||||
|
Buffer = (
|
||||||
|
ctypes.c_char
|
||||||
|
* (
|
||||||
|
pcm_length_samples
|
||||||
|
* self.bytes_per_sample
|
||||||
|
* self.channels
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.buffer = Buffer()
|
||||||
|
|
||||||
|
# Create a pointer to the newly allocated memory. It
|
||||||
|
# seems we can only do pointer arithmetic on void
|
||||||
|
# pointers. See
|
||||||
|
# https://mattgwwalker.wordpress.com/2020/05/30/pointer-manipulation-in-python/
|
||||||
|
buf_ptr = ctypes.cast(
|
||||||
|
ctypes.pointer(self.buffer),
|
||||||
|
ctypes.c_void_p
|
||||||
|
)
|
||||||
|
|
||||||
|
# Storage for the index of the logical bitstream
|
||||||
|
bitstream_previous = None
|
||||||
|
bitstream = ctypes.c_int()
|
||||||
|
|
||||||
|
# Set bytes remaining to read into PCM
|
||||||
|
read_size = len(self.buffer)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Convert buffer pointer to the desired type
|
||||||
|
ptr = ctypes.cast(
|
||||||
|
buf_ptr,
|
||||||
|
ctypes.POINTER(ctypes.c_char)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attempt to decode PCM from the Vorbis file
|
||||||
|
result = vorbis.libvorbisfile.ov_read(
|
||||||
|
ctypes.byref(vf),
|
||||||
|
ptr,
|
||||||
|
read_size,
|
||||||
|
0, # Little endian
|
||||||
|
self.bytes_per_sample,
|
||||||
|
int(self.signed),
|
||||||
|
ctypes.byref(bitstream)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
if result < 0:
|
||||||
|
raise PyOggError(
|
||||||
|
"An error occurred decoding the Vorbis file: "+
|
||||||
|
f"Error code: {result}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the bitstream hasn't changed as we only
|
||||||
|
# support Vorbis files with a single logical bitstream.
|
||||||
|
if bitstream_previous is None:
|
||||||
|
bitstream_previous = bitstream
|
||||||
|
else:
|
||||||
|
if bitstream_previous != bitstream:
|
||||||
|
raise PyOggError(
|
||||||
|
"PyOgg currently supports Vorbis files "+
|
||||||
|
"with only one logical stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for end of file
|
||||||
|
if result == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Calculate the number of bytes remaining to read into PCM
|
||||||
|
read_size -= result
|
||||||
|
|
||||||
|
# Update the pointer into the buffer
|
||||||
|
buf_ptr.value += result
|
||||||
|
|
||||||
|
|
||||||
|
# Close the file and clean up memory
|
||||||
|
vorbis.libvorbisfile.ov_clear(ctypes.byref(vf))
|
110
sbapp/pyogg/vorbis_file_stream.py
Normal file
110
sbapp/pyogg/vorbis_file_stream.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import ctypes
|
||||||
|
|
||||||
|
from . import vorbis
|
||||||
|
from .pyogg_error import PyOggError
|
||||||
|
|
||||||
|
class VorbisFileStream:
|
||||||
|
def __init__(self, path, buffer_size=8192):
|
||||||
|
self.exists = False
|
||||||
|
self._buffer_size = buffer_size
|
||||||
|
|
||||||
|
self.vf = vorbis.OggVorbis_File()
|
||||||
|
error = vorbis.ov_fopen(path, ctypes.byref(self.vf))
|
||||||
|
if error != 0:
|
||||||
|
raise PyOggError("file couldn't be opened or doesn't exist. Error code : {}".format(error))
|
||||||
|
|
||||||
|
info = vorbis.ov_info(ctypes.byref(self.vf), -1)
|
||||||
|
|
||||||
|
#: Number of channels in audio file.
|
||||||
|
self.channels = info.contents.channels
|
||||||
|
|
||||||
|
#: Number of samples per second (per channel). Always
|
||||||
|
# 48,000.
|
||||||
|
self.frequency = info.contents.rate
|
||||||
|
|
||||||
|
array = (ctypes.c_char*(self._buffer_size*self.channels))()
|
||||||
|
|
||||||
|
self.buffer_ = ctypes.cast(ctypes.pointer(array), ctypes.c_char_p)
|
||||||
|
|
||||||
|
self.bitstream = ctypes.c_int()
|
||||||
|
self.bitstream_pointer = ctypes.pointer(self.bitstream)
|
||||||
|
|
||||||
|
self.exists = True # TODO: is this the best place for this statement?
|
||||||
|
|
||||||
|
#: Bytes per sample
|
||||||
|
self.bytes_per_sample = 2 # TODO: Where is this defined?
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self.exists:
|
||||||
|
vorbis.ov_clear(ctypes.byref(self.vf))
|
||||||
|
self.exists = False
|
||||||
|
|
||||||
|
def clean_up(self):
|
||||||
|
vorbis.ov_clear(ctypes.byref(self.vf))
|
||||||
|
|
||||||
|
self.exists = False
|
||||||
|
|
||||||
|
def get_buffer(self):
|
||||||
|
"""get_buffer() -> bytesBuffer, bufferLength
|
||||||
|
|
||||||
|
Returns None when all data has been read from the file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.exists:
|
||||||
|
return None
|
||||||
|
buffer = []
|
||||||
|
total_bytes_written = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
new_bytes = vorbis.ov_read(ctypes.byref(self.vf), self.buffer_, self._buffer_size*self.channels - total_bytes_written, 0, 2, 1, self.bitstream_pointer)
|
||||||
|
|
||||||
|
array_ = ctypes.cast(self.buffer_, ctypes.POINTER(ctypes.c_char*(self._buffer_size*self.channels))).contents
|
||||||
|
|
||||||
|
buffer.append(array_.raw[:new_bytes])
|
||||||
|
|
||||||
|
total_bytes_written += new_bytes
|
||||||
|
|
||||||
|
if new_bytes == 0 or total_bytes_written >= self._buffer_size*self.channels:
|
||||||
|
break
|
||||||
|
|
||||||
|
out_buffer = b"".join(buffer)
|
||||||
|
|
||||||
|
if total_bytes_written == 0:
|
||||||
|
self.clean_up()
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
return out_buffer
|
||||||
|
|
||||||
|
def get_buffer_as_array(self):
|
||||||
|
"""Provides the buffer as a NumPy array.
|
||||||
|
|
||||||
|
Note that the underlying data type is 16-bit signed
|
||||||
|
integers.
|
||||||
|
|
||||||
|
Does not copy the underlying data, so the returned array
|
||||||
|
should either be processed or copied before the next call
|
||||||
|
to get_buffer() or get_buffer_as_array().
|
||||||
|
|
||||||
|
"""
|
||||||
|
import numpy # type: ignore
|
||||||
|
|
||||||
|
# Read the next samples from the stream
|
||||||
|
buf = self.get_buffer()
|
||||||
|
|
||||||
|
# Check if we've come to the end of the stream
|
||||||
|
if buf is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Convert the bytes buffer to a NumPy array
|
||||||
|
array = numpy.frombuffer(
|
||||||
|
buf,
|
||||||
|
dtype=numpy.int16
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape the array
|
||||||
|
return array.reshape(
|
||||||
|
(len(buf)
|
||||||
|
// self.bytes_per_sample
|
||||||
|
// self.channels,
|
||||||
|
self.channels)
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user