openCom-Companion/sbapp/pyogg/vorbis_file.py

162 lines
5.3 KiB
Python
Raw Normal View History

2024-06-03 01:54:58 +02:00
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))