Added pyogg

This commit is contained in:
Mark Qvist 2024-06-03 01:54:58 +02:00
parent dcf722d85f
commit 17c4febc96
19 changed files with 7500 additions and 0 deletions

108
sbapp/pyogg/__init__.py Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

114
sbapp/pyogg/flac_file.py Normal file
View 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

View 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)
)

View 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
View 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)

View 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

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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)

View 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
View File

@ -0,0 +1 @@
# Marker file for PEP 561. This package uses inline types.

View File

@ -0,0 +1,2 @@
class PyOggError(Exception):
pass

855
sbapp/pyogg/vorbis.py Normal file
View 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
View 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))

View 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)
)