Cleanup unneeded dependencies
This commit is contained in:
parent
65e08a9d41
commit
3560b4c142
30 changed files with 0 additions and 10844 deletions
|
|
@ -1 +0,0 @@
|
|||
from .audio_segment import AudioSegment
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,341 +0,0 @@
|
|||
import sys
|
||||
import math
|
||||
import array
|
||||
from .utils import (
|
||||
db_to_float,
|
||||
ratio_to_db,
|
||||
register_pydub_effect,
|
||||
make_chunks,
|
||||
audioop,
|
||||
get_min_max_value
|
||||
)
|
||||
from .silence import split_on_silence
|
||||
from .exceptions import TooManyMissingFrames, InvalidDuration
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
xrange = range
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def apply_mono_filter_to_each_channel(seg, filter_fn):
|
||||
n_channels = seg.channels
|
||||
|
||||
channel_segs = seg.split_to_mono()
|
||||
channel_segs = [filter_fn(channel_seg) for channel_seg in channel_segs]
|
||||
|
||||
out_data = seg.get_array_of_samples()
|
||||
for channel_i, channel_seg in enumerate(channel_segs):
|
||||
for sample_i, sample in enumerate(channel_seg.get_array_of_samples()):
|
||||
index = (sample_i * n_channels) + channel_i
|
||||
out_data[index] = sample
|
||||
|
||||
return seg._spawn(out_data)
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def normalize(seg, headroom=0.1):
|
||||
"""
|
||||
headroom is how close to the maximum volume to boost the signal up to (specified in dB)
|
||||
"""
|
||||
peak_sample_val = seg.max
|
||||
|
||||
# if the max is 0, this audio segment is silent, and can't be normalized
|
||||
if peak_sample_val == 0:
|
||||
return seg
|
||||
|
||||
target_peak = seg.max_possible_amplitude * db_to_float(-headroom)
|
||||
|
||||
needed_boost = ratio_to_db(target_peak / peak_sample_val)
|
||||
return seg.apply_gain(needed_boost)
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def speedup(seg, playback_speed=1.5, chunk_size=150, crossfade=25):
|
||||
# we will keep audio in 150ms chunks since one waveform at 20Hz is 50ms long
|
||||
# (20 Hz is the lowest frequency audible to humans)
|
||||
|
||||
# portion of AUDIO TO KEEP. if playback speed is 1.25 we keep 80% (0.8) and
|
||||
# discard 20% (0.2)
|
||||
atk = 1.0 / playback_speed
|
||||
|
||||
if playback_speed < 2.0:
|
||||
# throwing out more than half the audio - keep 50ms chunks
|
||||
ms_to_remove_per_chunk = int(chunk_size * (1 - atk) / atk)
|
||||
else:
|
||||
# throwing out less than half the audio - throw out 50ms chunks
|
||||
ms_to_remove_per_chunk = int(chunk_size)
|
||||
chunk_size = int(atk * chunk_size / (1 - atk))
|
||||
|
||||
# the crossfade cannot be longer than the amount of audio we're removing
|
||||
crossfade = min(crossfade, ms_to_remove_per_chunk - 1)
|
||||
|
||||
# DEBUG
|
||||
#print("chunk: {0}, rm: {1}".format(chunk_size, ms_to_remove_per_chunk))
|
||||
|
||||
chunks = make_chunks(seg, chunk_size + ms_to_remove_per_chunk)
|
||||
if len(chunks) < 2:
|
||||
raise Exception("Could not speed up AudioSegment, it was too short {2:0.2f}s for the current settings:\n{0}ms chunks at {1:0.1f}x speedup".format(
|
||||
chunk_size, playback_speed, seg.duration_seconds))
|
||||
|
||||
# we'll actually truncate a bit less than we calculated to make up for the
|
||||
# crossfade between chunks
|
||||
ms_to_remove_per_chunk -= crossfade
|
||||
|
||||
# we don't want to truncate the last chunk since it is not guaranteed to be
|
||||
# the full chunk length
|
||||
last_chunk = chunks[-1]
|
||||
chunks = [chunk[:-ms_to_remove_per_chunk] for chunk in chunks[:-1]]
|
||||
|
||||
out = chunks[0]
|
||||
for chunk in chunks[1:]:
|
||||
out = out.append(chunk, crossfade=crossfade)
|
||||
|
||||
out += last_chunk
|
||||
return out
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def strip_silence(seg, silence_len=1000, silence_thresh=-16, padding=100):
|
||||
if padding > silence_len:
|
||||
raise InvalidDuration("padding cannot be longer than silence_len")
|
||||
|
||||
chunks = split_on_silence(seg, silence_len, silence_thresh, padding)
|
||||
crossfade = padding / 2
|
||||
|
||||
if not len(chunks):
|
||||
return seg[0:0]
|
||||
|
||||
seg = chunks[0]
|
||||
for chunk in chunks[1:]:
|
||||
seg = seg.append(chunk, crossfade=crossfade)
|
||||
|
||||
return seg
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def compress_dynamic_range(seg, threshold=-20.0, ratio=4.0, attack=5.0, release=50.0):
|
||||
"""
|
||||
Keyword Arguments:
|
||||
|
||||
threshold - default: -20.0
|
||||
Threshold in dBFS. default of -20.0 means -20dB relative to the
|
||||
maximum possible volume. 0dBFS is the maximum possible value so
|
||||
all values for this argument sould be negative.
|
||||
|
||||
ratio - default: 4.0
|
||||
Compression ratio. Audio louder than the threshold will be
|
||||
reduced to 1/ratio the volume. A ratio of 4.0 is equivalent to
|
||||
a setting of 4:1 in a pro-audio compressor like the Waves C1.
|
||||
|
||||
attack - default: 5.0
|
||||
Attack in milliseconds. How long it should take for the compressor
|
||||
to kick in once the audio has exceeded the threshold.
|
||||
|
||||
release - default: 50.0
|
||||
Release in milliseconds. How long it should take for the compressor
|
||||
to stop compressing after the audio has falled below the threshold.
|
||||
|
||||
|
||||
For an overview of Dynamic Range Compression, and more detailed explanation
|
||||
of the related terminology, see:
|
||||
|
||||
http://en.wikipedia.org/wiki/Dynamic_range_compression
|
||||
"""
|
||||
|
||||
thresh_rms = seg.max_possible_amplitude * db_to_float(threshold)
|
||||
|
||||
look_frames = int(seg.frame_count(ms=attack))
|
||||
def rms_at(frame_i):
|
||||
return seg.get_sample_slice(frame_i - look_frames, frame_i).rms
|
||||
def db_over_threshold(rms):
|
||||
if rms == 0: return 0.0
|
||||
db = ratio_to_db(rms / thresh_rms)
|
||||
return max(db, 0)
|
||||
|
||||
output = []
|
||||
|
||||
# amount to reduce the volume of the audio by (in dB)
|
||||
attenuation = 0.0
|
||||
|
||||
attack_frames = seg.frame_count(ms=attack)
|
||||
release_frames = seg.frame_count(ms=release)
|
||||
for i in xrange(int(seg.frame_count())):
|
||||
rms_now = rms_at(i)
|
||||
|
||||
# with a ratio of 4.0 this means the volume will exceed the threshold by
|
||||
# 1/4 the amount (of dB) that it would otherwise
|
||||
max_attenuation = (1 - (1.0 / ratio)) * db_over_threshold(rms_now)
|
||||
|
||||
attenuation_inc = max_attenuation / attack_frames
|
||||
attenuation_dec = max_attenuation / release_frames
|
||||
|
||||
if rms_now > thresh_rms and attenuation <= max_attenuation:
|
||||
attenuation += attenuation_inc
|
||||
attenuation = min(attenuation, max_attenuation)
|
||||
else:
|
||||
attenuation -= attenuation_dec
|
||||
attenuation = max(attenuation, 0)
|
||||
|
||||
frame = seg.get_frame(i)
|
||||
if attenuation != 0.0:
|
||||
frame = audioop.mul(frame,
|
||||
seg.sample_width,
|
||||
db_to_float(-attenuation))
|
||||
|
||||
output.append(frame)
|
||||
|
||||
return seg._spawn(data=b''.join(output))
|
||||
|
||||
|
||||
# Invert the phase of the signal.
|
||||
|
||||
@register_pydub_effect
|
||||
|
||||
def invert_phase(seg, channels=(1, 1)):
|
||||
"""
|
||||
channels- specifies which channel (left or right) to reverse the phase of.
|
||||
Note that mono AudioSegments will become stereo.
|
||||
"""
|
||||
if channels == (1, 1):
|
||||
inverted = audioop.mul(seg._data, seg.sample_width, -1.0)
|
||||
return seg._spawn(data=inverted)
|
||||
|
||||
else:
|
||||
if seg.channels == 2:
|
||||
left, right = seg.split_to_mono()
|
||||
else:
|
||||
raise Exception("Can't implicitly convert an AudioSegment with " + str(seg.channels) + " channels to stereo.")
|
||||
|
||||
if channels == (1, 0):
|
||||
left = left.invert_phase()
|
||||
else:
|
||||
right = right.invert_phase()
|
||||
|
||||
return seg.from_mono_audiosegments(left, right)
|
||||
|
||||
|
||||
|
||||
# High and low pass filters based on implementation found on Stack Overflow:
|
||||
# http://stackoverflow.com/questions/13882038/implementing-simple-high-and-low-pass-filters-in-c
|
||||
|
||||
@register_pydub_effect
|
||||
def low_pass_filter(seg, cutoff):
|
||||
"""
|
||||
cutoff - Frequency (in Hz) where higher frequency signal will begin to
|
||||
be reduced by 6dB per octave (doubling in frequency) above this point
|
||||
"""
|
||||
RC = 1.0 / (cutoff * 2 * math.pi)
|
||||
dt = 1.0 / seg.frame_rate
|
||||
|
||||
alpha = dt / (RC + dt)
|
||||
|
||||
original = seg.get_array_of_samples()
|
||||
filteredArray = array.array(seg.array_type, original)
|
||||
|
||||
frame_count = int(seg.frame_count())
|
||||
|
||||
last_val = [0] * seg.channels
|
||||
for i in range(seg.channels):
|
||||
last_val[i] = filteredArray[i] = original[i]
|
||||
|
||||
for i in range(1, frame_count):
|
||||
for j in range(seg.channels):
|
||||
offset = (i * seg.channels) + j
|
||||
last_val[j] = last_val[j] + (alpha * (original[offset] - last_val[j]))
|
||||
filteredArray[offset] = int(last_val[j])
|
||||
|
||||
return seg._spawn(data=filteredArray)
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def high_pass_filter(seg, cutoff):
|
||||
"""
|
||||
cutoff - Frequency (in Hz) where lower frequency signal will begin to
|
||||
be reduced by 6dB per octave (doubling in frequency) below this point
|
||||
"""
|
||||
RC = 1.0 / (cutoff * 2 * math.pi)
|
||||
dt = 1.0 / seg.frame_rate
|
||||
|
||||
alpha = RC / (RC + dt)
|
||||
|
||||
minval, maxval = get_min_max_value(seg.sample_width * 8)
|
||||
|
||||
original = seg.get_array_of_samples()
|
||||
filteredArray = array.array(seg.array_type, original)
|
||||
|
||||
frame_count = int(seg.frame_count())
|
||||
|
||||
last_val = [0] * seg.channels
|
||||
for i in range(seg.channels):
|
||||
last_val[i] = filteredArray[i] = original[i]
|
||||
|
||||
for i in range(1, frame_count):
|
||||
for j in range(seg.channels):
|
||||
offset = (i * seg.channels) + j
|
||||
offset_minus_1 = ((i-1) * seg.channels) + j
|
||||
|
||||
last_val[j] = alpha * (last_val[j] + original[offset] - original[offset_minus_1])
|
||||
filteredArray[offset] = int(min(max(last_val[j], minval), maxval))
|
||||
|
||||
return seg._spawn(data=filteredArray)
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def pan(seg, pan_amount):
|
||||
"""
|
||||
pan_amount should be between -1.0 (100% left) and +1.0 (100% right)
|
||||
|
||||
When pan_amount == 0.0 the left/right balance is not changed.
|
||||
|
||||
Panning does not alter the *perceived* loundness, but since loudness
|
||||
is decreasing on one side, the other side needs to get louder to
|
||||
compensate. When panned hard left, the left channel will be 3dB louder.
|
||||
"""
|
||||
if not -1.0 <= pan_amount <= 1.0:
|
||||
raise ValueError("pan_amount should be between -1.0 (100% left) and +1.0 (100% right)")
|
||||
|
||||
max_boost_db = ratio_to_db(2.0)
|
||||
boost_db = abs(pan_amount) * max_boost_db
|
||||
|
||||
boost_factor = db_to_float(boost_db)
|
||||
reduce_factor = db_to_float(max_boost_db) - boost_factor
|
||||
|
||||
reduce_db = ratio_to_db(reduce_factor)
|
||||
|
||||
# Cut boost in half (max boost== 3dB) - in reality 2 speakers
|
||||
# do not sum to a full 6 dB.
|
||||
boost_db = boost_db / 2.0
|
||||
|
||||
if pan_amount < 0:
|
||||
return seg.apply_gain_stereo(boost_db, reduce_db)
|
||||
else:
|
||||
return seg.apply_gain_stereo(reduce_db, boost_db)
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def apply_gain_stereo(seg, left_gain=0.0, right_gain=0.0):
|
||||
"""
|
||||
left_gain - amount of gain to apply to the left channel (in dB)
|
||||
right_gain - amount of gain to apply to the right channel (in dB)
|
||||
|
||||
note: mono audio segments will be converted to stereo
|
||||
"""
|
||||
if seg.channels == 1:
|
||||
left = right = seg
|
||||
elif seg.channels == 2:
|
||||
left, right = seg.split_to_mono()
|
||||
|
||||
l_mult_factor = db_to_float(left_gain)
|
||||
r_mult_factor = db_to_float(right_gain)
|
||||
|
||||
left_data = audioop.mul(left._data, left.sample_width, l_mult_factor)
|
||||
left_data = audioop.tostereo(left_data, left.sample_width, 1, 0)
|
||||
|
||||
right_data = audioop.mul(right._data, right.sample_width, r_mult_factor)
|
||||
right_data = audioop.tostereo(right_data, right.sample_width, 0, 1)
|
||||
|
||||
output = audioop.add(left_data, right_data, seg.sample_width)
|
||||
|
||||
return seg._spawn(data=output,
|
||||
overrides={'channels': 2,
|
||||
'frame_width': 2 * seg.sample_width})
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
class PydubException(Exception):
|
||||
"""
|
||||
Base class for any Pydub exception
|
||||
"""
|
||||
|
||||
|
||||
class TooManyMissingFrames(PydubException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidDuration(PydubException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidTag(PydubException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidID3TagVersion(PydubException):
|
||||
pass
|
||||
|
||||
|
||||
class CouldntDecodeError(PydubException):
|
||||
pass
|
||||
|
||||
|
||||
class CouldntEncodeError(PydubException):
|
||||
pass
|
||||
|
||||
|
||||
class MissingAudioParameter(PydubException):
|
||||
pass
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
"""
|
||||
Each generator will return float samples from -1.0 to 1.0, which can be
|
||||
converted to actual audio with 8, 16, 24, or 32 bit depth using the
|
||||
SiganlGenerator.to_audio_segment() method (on any of it's subclasses).
|
||||
|
||||
See Wikipedia's "waveform" page for info on some of the generators included
|
||||
here: http://en.wikipedia.org/wiki/Waveform
|
||||
"""
|
||||
|
||||
import math
|
||||
import array
|
||||
import itertools
|
||||
import random
|
||||
from .audio_segment import AudioSegment
|
||||
from .utils import (
|
||||
db_to_float,
|
||||
get_frame_width,
|
||||
get_array_type,
|
||||
get_min_max_value
|
||||
)
|
||||
|
||||
|
||||
|
||||
class SignalGenerator(object):
|
||||
def __init__(self, sample_rate=44100, bit_depth=16):
|
||||
self.sample_rate = sample_rate
|
||||
self.bit_depth = bit_depth
|
||||
|
||||
def to_audio_segment(self, duration=1000.0, volume=0.0):
|
||||
"""
|
||||
Duration in milliseconds
|
||||
(default: 1 second)
|
||||
Volume in DB relative to maximum amplitude
|
||||
(default 0.0 dBFS, which is the maximum value)
|
||||
"""
|
||||
minval, maxval = get_min_max_value(self.bit_depth)
|
||||
sample_width = get_frame_width(self.bit_depth)
|
||||
array_type = get_array_type(self.bit_depth)
|
||||
|
||||
gain = db_to_float(volume)
|
||||
sample_count = int(self.sample_rate * (duration / 1000.0))
|
||||
|
||||
sample_data = (int(val * maxval * gain) for val in self.generate())
|
||||
sample_data = itertools.islice(sample_data, 0, sample_count)
|
||||
|
||||
data = array.array(array_type, sample_data)
|
||||
|
||||
try:
|
||||
data = data.tobytes()
|
||||
except:
|
||||
data = data.tostring()
|
||||
|
||||
return AudioSegment(data=data, metadata={
|
||||
"channels": 1,
|
||||
"sample_width": sample_width,
|
||||
"frame_rate": self.sample_rate,
|
||||
"frame_width": sample_width,
|
||||
})
|
||||
|
||||
def generate(self):
|
||||
raise NotImplementedError("SignalGenerator subclasses must implement the generate() method, and *should not* call the superclass implementation.")
|
||||
|
||||
|
||||
|
||||
class Sine(SignalGenerator):
|
||||
def __init__(self, freq, **kwargs):
|
||||
super(Sine, self).__init__(**kwargs)
|
||||
self.freq = freq
|
||||
|
||||
def generate(self):
|
||||
sine_of = (self.freq * 2 * math.pi) / self.sample_rate
|
||||
sample_n = 0
|
||||
while True:
|
||||
yield math.sin(sine_of * sample_n)
|
||||
sample_n += 1
|
||||
|
||||
|
||||
|
||||
class Pulse(SignalGenerator):
|
||||
def __init__(self, freq, duty_cycle=0.5, **kwargs):
|
||||
super(Pulse, self).__init__(**kwargs)
|
||||
self.freq = freq
|
||||
self.duty_cycle = duty_cycle
|
||||
|
||||
def generate(self):
|
||||
sample_n = 0
|
||||
|
||||
# in samples
|
||||
cycle_length = self.sample_rate / float(self.freq)
|
||||
pulse_length = cycle_length * self.duty_cycle
|
||||
|
||||
while True:
|
||||
if (sample_n % cycle_length) < pulse_length:
|
||||
yield 1.0
|
||||
else:
|
||||
yield -1.0
|
||||
sample_n += 1
|
||||
|
||||
|
||||
|
||||
class Square(Pulse):
|
||||
def __init__(self, freq, **kwargs):
|
||||
kwargs['duty_cycle'] = 0.5
|
||||
super(Square, self).__init__(freq, **kwargs)
|
||||
|
||||
|
||||
|
||||
class Sawtooth(SignalGenerator):
|
||||
def __init__(self, freq, duty_cycle=1.0, **kwargs):
|
||||
super(Sawtooth, self).__init__(**kwargs)
|
||||
self.freq = freq
|
||||
self.duty_cycle = duty_cycle
|
||||
|
||||
def generate(self):
|
||||
sample_n = 0
|
||||
|
||||
# in samples
|
||||
cycle_length = self.sample_rate / float(self.freq)
|
||||
midpoint = cycle_length * self.duty_cycle
|
||||
ascend_length = midpoint
|
||||
descend_length = cycle_length - ascend_length
|
||||
|
||||
while True:
|
||||
cycle_position = sample_n % cycle_length
|
||||
if cycle_position < midpoint:
|
||||
yield (2 * cycle_position / ascend_length) - 1.0
|
||||
else:
|
||||
yield 1.0 - (2 * (cycle_position - midpoint) / descend_length)
|
||||
sample_n += 1
|
||||
|
||||
|
||||
|
||||
class Triangle(Sawtooth):
|
||||
def __init__(self, freq, **kwargs):
|
||||
kwargs['duty_cycle'] = 0.5
|
||||
super(Triangle, self).__init__(freq, **kwargs)
|
||||
|
||||
|
||||
class WhiteNoise(SignalGenerator):
|
||||
def generate(self):
|
||||
while True:
|
||||
yield (random.random() * 2) - 1.0
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"""
|
||||
|
||||
"""
|
||||
import logging
|
||||
|
||||
converter_logger = logging.getLogger("pydub.converter")
|
||||
|
||||
def log_conversion(conversion_command):
|
||||
converter_logger.debug("subprocess.call(%s)", repr(conversion_command))
|
||||
|
||||
def log_subprocess_output(output):
|
||||
if output:
|
||||
for line in output.rstrip().splitlines():
|
||||
converter_logger.debug('subprocess output: %s', line.rstrip())
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
"""
|
||||
Support for playing AudioSegments. Pyaudio will be used if it's installed,
|
||||
otherwise will fallback to ffplay. Pyaudio is a *much* nicer solution, but
|
||||
is tricky to install. See my notes on installing pyaudio in a virtualenv (on
|
||||
OSX 10.10): https://gist.github.com/jiaaro/9767512210a1d80a8a0d
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from tempfile import NamedTemporaryFile
|
||||
from .utils import get_player_name, make_chunks
|
||||
|
||||
def _play_with_ffplay(seg):
|
||||
PLAYER = get_player_name()
|
||||
with NamedTemporaryFile("w+b", suffix=".wav") as f:
|
||||
seg.export(f.name, "wav")
|
||||
subprocess.call([PLAYER, "-nodisp", "-autoexit", "-hide_banner", f.name])
|
||||
|
||||
|
||||
def _play_with_pyaudio(seg):
|
||||
import pyaudio
|
||||
|
||||
p = pyaudio.PyAudio()
|
||||
stream = p.open(format=p.get_format_from_width(seg.sample_width),
|
||||
channels=seg.channels,
|
||||
rate=seg.frame_rate,
|
||||
output=True)
|
||||
|
||||
# Just in case there were any exceptions/interrupts, we release the resource
|
||||
# So as not to raise OSError: Device Unavailable should play() be used again
|
||||
try:
|
||||
# break audio into half-second chunks (to allows keyboard interrupts)
|
||||
for chunk in make_chunks(seg, 500):
|
||||
stream.write(chunk._data)
|
||||
finally:
|
||||
stream.stop_stream()
|
||||
stream.close()
|
||||
|
||||
p.terminate()
|
||||
|
||||
|
||||
def _play_with_simpleaudio(seg):
|
||||
import simpleaudio
|
||||
return simpleaudio.play_buffer(
|
||||
seg.raw_data,
|
||||
num_channels=seg.channels,
|
||||
bytes_per_sample=seg.sample_width,
|
||||
sample_rate=seg.frame_rate
|
||||
)
|
||||
|
||||
|
||||
def play(audio_segment):
|
||||
try:
|
||||
playback = _play_with_simpleaudio(audio_segment)
|
||||
try:
|
||||
playback.wait_done()
|
||||
except KeyboardInterrupt:
|
||||
playback.stop()
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
|
||||
try:
|
||||
_play_with_pyaudio(audio_segment)
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
|
||||
_play_with_ffplay(audio_segment)
|
||||
|
|
@ -1,553 +0,0 @@
|
|||
try:
|
||||
from __builtin__ import max as builtin_max
|
||||
from __builtin__ import min as builtin_min
|
||||
except ImportError:
|
||||
from builtins import max as builtin_max
|
||||
from builtins import min as builtin_min
|
||||
import math
|
||||
import struct
|
||||
try:
|
||||
from fractions import gcd
|
||||
except ImportError: # Python 3.9+
|
||||
from math import gcd
|
||||
from ctypes import create_string_buffer
|
||||
|
||||
|
||||
class error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _check_size(size):
|
||||
if size != 1 and size != 2 and size != 4:
|
||||
raise error("Size should be 1, 2 or 4")
|
||||
|
||||
|
||||
def _check_params(length, size):
|
||||
_check_size(size)
|
||||
if length % size != 0:
|
||||
raise error("not a whole number of frames")
|
||||
|
||||
|
||||
def _sample_count(cp, size):
|
||||
return len(cp) / size
|
||||
|
||||
|
||||
def _get_samples(cp, size, signed=True):
|
||||
for i in range(_sample_count(cp, size)):
|
||||
yield _get_sample(cp, size, i, signed)
|
||||
|
||||
|
||||
def _struct_format(size, signed):
|
||||
if size == 1:
|
||||
return "b" if signed else "B"
|
||||
elif size == 2:
|
||||
return "h" if signed else "H"
|
||||
elif size == 4:
|
||||
return "i" if signed else "I"
|
||||
|
||||
|
||||
def _get_sample(cp, size, i, signed=True):
|
||||
fmt = _struct_format(size, signed)
|
||||
start = i * size
|
||||
end = start + size
|
||||
return struct.unpack_from(fmt, buffer(cp)[start:end])[0]
|
||||
|
||||
|
||||
def _put_sample(cp, size, i, val, signed=True):
|
||||
fmt = _struct_format(size, signed)
|
||||
struct.pack_into(fmt, cp, i * size, val)
|
||||
|
||||
|
||||
def _get_maxval(size, signed=True):
|
||||
if signed and size == 1:
|
||||
return 0x7f
|
||||
elif size == 1:
|
||||
return 0xff
|
||||
elif signed and size == 2:
|
||||
return 0x7fff
|
||||
elif size == 2:
|
||||
return 0xffff
|
||||
elif signed and size == 4:
|
||||
return 0x7fffffff
|
||||
elif size == 4:
|
||||
return 0xffffffff
|
||||
|
||||
|
||||
def _get_minval(size, signed=True):
|
||||
if not signed:
|
||||
return 0
|
||||
elif size == 1:
|
||||
return -0x80
|
||||
elif size == 2:
|
||||
return -0x8000
|
||||
elif size == 4:
|
||||
return -0x80000000
|
||||
|
||||
|
||||
def _get_clipfn(size, signed=True):
|
||||
maxval = _get_maxval(size, signed)
|
||||
minval = _get_minval(size, signed)
|
||||
return lambda val: builtin_max(min(val, maxval), minval)
|
||||
|
||||
|
||||
def _overflow(val, size, signed=True):
|
||||
minval = _get_minval(size, signed)
|
||||
maxval = _get_maxval(size, signed)
|
||||
if minval <= val <= maxval:
|
||||
return val
|
||||
|
||||
bits = size * 8
|
||||
if signed:
|
||||
offset = 2**(bits-1)
|
||||
return ((val + offset) % (2**bits)) - offset
|
||||
else:
|
||||
return val % (2**bits)
|
||||
|
||||
|
||||
def getsample(cp, size, i):
|
||||
_check_params(len(cp), size)
|
||||
if not (0 <= i < len(cp) / size):
|
||||
raise error("Index out of range")
|
||||
return _get_sample(cp, size, i)
|
||||
|
||||
|
||||
def max(cp, size):
|
||||
_check_params(len(cp), size)
|
||||
|
||||
if len(cp) == 0:
|
||||
return 0
|
||||
|
||||
return builtin_max(abs(sample) for sample in _get_samples(cp, size))
|
||||
|
||||
|
||||
def minmax(cp, size):
|
||||
_check_params(len(cp), size)
|
||||
|
||||
max_sample, min_sample = 0, 0
|
||||
for sample in _get_samples(cp, size):
|
||||
max_sample = builtin_max(sample, max_sample)
|
||||
min_sample = builtin_min(sample, min_sample)
|
||||
|
||||
return min_sample, max_sample
|
||||
|
||||
|
||||
def avg(cp, size):
|
||||
_check_params(len(cp), size)
|
||||
sample_count = _sample_count(cp, size)
|
||||
if sample_count == 0:
|
||||
return 0
|
||||
return sum(_get_samples(cp, size)) / sample_count
|
||||
|
||||
|
||||
def rms(cp, size):
|
||||
_check_params(len(cp), size)
|
||||
|
||||
sample_count = _sample_count(cp, size)
|
||||
if sample_count == 0:
|
||||
return 0
|
||||
|
||||
sum_squares = sum(sample**2 for sample in _get_samples(cp, size))
|
||||
return int(math.sqrt(sum_squares / sample_count))
|
||||
|
||||
|
||||
def _sum2(cp1, cp2, length):
|
||||
size = 2
|
||||
total = 0
|
||||
for i in range(length):
|
||||
total += getsample(cp1, size, i) * getsample(cp2, size, i)
|
||||
return total
|
||||
|
||||
|
||||
def findfit(cp1, cp2):
|
||||
size = 2
|
||||
|
||||
if len(cp1) % 2 != 0 or len(cp2) % 2 != 0:
|
||||
raise error("Strings should be even-sized")
|
||||
|
||||
if len(cp1) < len(cp2):
|
||||
raise error("First sample should be longer")
|
||||
|
||||
len1 = _sample_count(cp1, size)
|
||||
len2 = _sample_count(cp2, size)
|
||||
|
||||
sum_ri_2 = _sum2(cp2, cp2, len2)
|
||||
sum_aij_2 = _sum2(cp1, cp1, len2)
|
||||
sum_aij_ri = _sum2(cp1, cp2, len2)
|
||||
|
||||
result = (sum_ri_2 * sum_aij_2 - sum_aij_ri * sum_aij_ri) / sum_aij_2
|
||||
|
||||
best_result = result
|
||||
best_i = 0
|
||||
|
||||
for i in range(1, len1 - len2 + 1):
|
||||
aj_m1 = _get_sample(cp1, size, i - 1)
|
||||
aj_lm1 = _get_sample(cp1, size, i + len2 - 1)
|
||||
|
||||
sum_aij_2 += aj_lm1**2 - aj_m1**2
|
||||
sum_aij_ri = _sum2(buffer(cp1)[i*size:], cp2, len2)
|
||||
|
||||
result = (sum_ri_2 * sum_aij_2 - sum_aij_ri * sum_aij_ri) / sum_aij_2
|
||||
|
||||
if result < best_result:
|
||||
best_result = result
|
||||
best_i = i
|
||||
|
||||
factor = _sum2(buffer(cp1)[best_i*size:], cp2, len2) / sum_ri_2
|
||||
|
||||
return best_i, factor
|
||||
|
||||
|
||||
def findfactor(cp1, cp2):
|
||||
size = 2
|
||||
|
||||
if len(cp1) % 2 != 0:
|
||||
raise error("Strings should be even-sized")
|
||||
|
||||
if len(cp1) != len(cp2):
|
||||
raise error("Samples should be same size")
|
||||
|
||||
sample_count = _sample_count(cp1, size)
|
||||
|
||||
sum_ri_2 = _sum2(cp2, cp2, sample_count)
|
||||
sum_aij_ri = _sum2(cp1, cp2, sample_count)
|
||||
|
||||
return sum_aij_ri / sum_ri_2
|
||||
|
||||
|
||||
def findmax(cp, len2):
|
||||
size = 2
|
||||
sample_count = _sample_count(cp, size)
|
||||
|
||||
if len(cp) % 2 != 0:
|
||||
raise error("Strings should be even-sized")
|
||||
|
||||
if len2 < 0 or sample_count < len2:
|
||||
raise error("Input sample should be longer")
|
||||
|
||||
if sample_count == 0:
|
||||
return 0
|
||||
|
||||
result = _sum2(cp, cp, len2)
|
||||
best_result = result
|
||||
best_i = 0
|
||||
|
||||
for i in range(1, sample_count - len2 + 1):
|
||||
sample_leaving_window = getsample(cp, size, i - 1)
|
||||
sample_entering_window = getsample(cp, size, i + len2 - 1)
|
||||
|
||||
result -= sample_leaving_window**2
|
||||
result += sample_entering_window**2
|
||||
|
||||
if result > best_result:
|
||||
best_result = result
|
||||
best_i = i
|
||||
|
||||
return best_i
|
||||
|
||||
|
||||
def avgpp(cp, size):
|
||||
_check_params(len(cp), size)
|
||||
sample_count = _sample_count(cp, size)
|
||||
|
||||
prevextremevalid = False
|
||||
prevextreme = None
|
||||
avg = 0
|
||||
nextreme = 0
|
||||
|
||||
prevval = getsample(cp, size, 0)
|
||||
val = getsample(cp, size, 1)
|
||||
|
||||
prevdiff = val - prevval
|
||||
|
||||
for i in range(1, sample_count):
|
||||
val = getsample(cp, size, i)
|
||||
diff = val - prevval
|
||||
|
||||
if diff * prevdiff < 0:
|
||||
if prevextremevalid:
|
||||
avg += abs(prevval - prevextreme)
|
||||
nextreme += 1
|
||||
|
||||
prevextremevalid = True
|
||||
prevextreme = prevval
|
||||
|
||||
prevval = val
|
||||
if diff != 0:
|
||||
prevdiff = diff
|
||||
|
||||
if nextreme == 0:
|
||||
return 0
|
||||
|
||||
return avg / nextreme
|
||||
|
||||
|
||||
def maxpp(cp, size):
|
||||
_check_params(len(cp), size)
|
||||
sample_count = _sample_count(cp, size)
|
||||
|
||||
prevextremevalid = False
|
||||
prevextreme = None
|
||||
max = 0
|
||||
|
||||
prevval = getsample(cp, size, 0)
|
||||
val = getsample(cp, size, 1)
|
||||
|
||||
prevdiff = val - prevval
|
||||
|
||||
for i in range(1, sample_count):
|
||||
val = getsample(cp, size, i)
|
||||
diff = val - prevval
|
||||
|
||||
if diff * prevdiff < 0:
|
||||
if prevextremevalid:
|
||||
extremediff = abs(prevval - prevextreme)
|
||||
if extremediff > max:
|
||||
max = extremediff
|
||||
prevextremevalid = True
|
||||
prevextreme = prevval
|
||||
|
||||
prevval = val
|
||||
if diff != 0:
|
||||
prevdiff = diff
|
||||
|
||||
return max
|
||||
|
||||
|
||||
def cross(cp, size):
|
||||
_check_params(len(cp), size)
|
||||
|
||||
crossings = 0
|
||||
last_sample = 0
|
||||
for sample in _get_samples(cp, size):
|
||||
if sample <= 0 < last_sample or sample >= 0 > last_sample:
|
||||
crossings += 1
|
||||
last_sample = sample
|
||||
|
||||
return crossings
|
||||
|
||||
|
||||
def mul(cp, size, factor):
|
||||
_check_params(len(cp), size)
|
||||
clip = _get_clipfn(size)
|
||||
|
||||
result = create_string_buffer(len(cp))
|
||||
|
||||
for i, sample in enumerate(_get_samples(cp, size)):
|
||||
sample = clip(int(sample * factor))
|
||||
_put_sample(result, size, i, sample)
|
||||
|
||||
return result.raw
|
||||
|
||||
|
||||
def tomono(cp, size, fac1, fac2):
|
||||
_check_params(len(cp), size)
|
||||
clip = _get_clipfn(size)
|
||||
|
||||
sample_count = _sample_count(cp, size)
|
||||
|
||||
result = create_string_buffer(len(cp) / 2)
|
||||
|
||||
for i in range(0, sample_count, 2):
|
||||
l_sample = getsample(cp, size, i)
|
||||
r_sample = getsample(cp, size, i + 1)
|
||||
|
||||
sample = (l_sample * fac1) + (r_sample * fac2)
|
||||
sample = clip(sample)
|
||||
|
||||
_put_sample(result, size, i / 2, sample)
|
||||
|
||||
return result.raw
|
||||
|
||||
|
||||
def tostereo(cp, size, fac1, fac2):
|
||||
_check_params(len(cp), size)
|
||||
|
||||
sample_count = _sample_count(cp, size)
|
||||
|
||||
result = create_string_buffer(len(cp) * 2)
|
||||
clip = _get_clipfn(size)
|
||||
|
||||
for i in range(sample_count):
|
||||
sample = _get_sample(cp, size, i)
|
||||
|
||||
l_sample = clip(sample * fac1)
|
||||
r_sample = clip(sample * fac2)
|
||||
|
||||
_put_sample(result, size, i * 2, l_sample)
|
||||
_put_sample(result, size, i * 2 + 1, r_sample)
|
||||
|
||||
return result.raw
|
||||
|
||||
|
||||
def add(cp1, cp2, size):
|
||||
_check_params(len(cp1), size)
|
||||
|
||||
if len(cp1) != len(cp2):
|
||||
raise error("Lengths should be the same")
|
||||
|
||||
clip = _get_clipfn(size)
|
||||
sample_count = _sample_count(cp1, size)
|
||||
result = create_string_buffer(len(cp1))
|
||||
|
||||
for i in range(sample_count):
|
||||
sample1 = getsample(cp1, size, i)
|
||||
sample2 = getsample(cp2, size, i)
|
||||
|
||||
sample = clip(sample1 + sample2)
|
||||
|
||||
_put_sample(result, size, i, sample)
|
||||
|
||||
return result.raw
|
||||
|
||||
|
||||
def bias(cp, size, bias):
|
||||
_check_params(len(cp), size)
|
||||
|
||||
result = create_string_buffer(len(cp))
|
||||
|
||||
for i, sample in enumerate(_get_samples(cp, size)):
|
||||
sample = _overflow(sample + bias, size)
|
||||
_put_sample(result, size, i, sample)
|
||||
|
||||
return result.raw
|
||||
|
||||
|
||||
def reverse(cp, size):
|
||||
_check_params(len(cp), size)
|
||||
sample_count = _sample_count(cp, size)
|
||||
|
||||
result = create_string_buffer(len(cp))
|
||||
for i, sample in enumerate(_get_samples(cp, size)):
|
||||
_put_sample(result, size, sample_count - i - 1, sample)
|
||||
|
||||
return result.raw
|
||||
|
||||
|
||||
def lin2lin(cp, size, size2):
|
||||
_check_params(len(cp), size)
|
||||
_check_size(size2)
|
||||
|
||||
if size == size2:
|
||||
return cp
|
||||
|
||||
new_len = (len(cp) / size) * size2
|
||||
|
||||
result = create_string_buffer(new_len)
|
||||
|
||||
for i in range(_sample_count(cp, size)):
|
||||
sample = _get_sample(cp, size, i)
|
||||
if size < size2:
|
||||
sample = sample << (4 * size2 / size)
|
||||
elif size > size2:
|
||||
sample = sample >> (4 * size / size2)
|
||||
|
||||
sample = _overflow(sample, size2)
|
||||
|
||||
_put_sample(result, size2, i, sample)
|
||||
|
||||
return result.raw
|
||||
|
||||
|
||||
def ratecv(cp, size, nchannels, inrate, outrate, state, weightA=1, weightB=0):
|
||||
_check_params(len(cp), size)
|
||||
if nchannels < 1:
|
||||
raise error("# of channels should be >= 1")
|
||||
|
||||
bytes_per_frame = size * nchannels
|
||||
frame_count = len(cp) / bytes_per_frame
|
||||
|
||||
if bytes_per_frame / nchannels != size:
|
||||
raise OverflowError("width * nchannels too big for a C int")
|
||||
|
||||
if weightA < 1 or weightB < 0:
|
||||
raise error("weightA should be >= 1, weightB should be >= 0")
|
||||
|
||||
if len(cp) % bytes_per_frame != 0:
|
||||
raise error("not a whole number of frames")
|
||||
|
||||
if inrate <= 0 or outrate <= 0:
|
||||
raise error("sampling rate not > 0")
|
||||
|
||||
d = gcd(inrate, outrate)
|
||||
inrate /= d
|
||||
outrate /= d
|
||||
|
||||
prev_i = [0] * nchannels
|
||||
cur_i = [0] * nchannels
|
||||
|
||||
if state is None:
|
||||
d = -outrate
|
||||
else:
|
||||
d, samps = state
|
||||
|
||||
if len(samps) != nchannels:
|
||||
raise error("illegal state argument")
|
||||
|
||||
prev_i, cur_i = zip(*samps)
|
||||
prev_i, cur_i = list(prev_i), list(cur_i)
|
||||
|
||||
q = frame_count / inrate
|
||||
ceiling = (q + 1) * outrate
|
||||
nbytes = ceiling * bytes_per_frame
|
||||
|
||||
result = create_string_buffer(nbytes)
|
||||
|
||||
samples = _get_samples(cp, size)
|
||||
out_i = 0
|
||||
while True:
|
||||
while d < 0:
|
||||
if frame_count == 0:
|
||||
samps = zip(prev_i, cur_i)
|
||||
retval = result.raw
|
||||
|
||||
# slice off extra bytes
|
||||
trim_index = (out_i * bytes_per_frame) - len(retval)
|
||||
retval = buffer(retval)[:trim_index]
|
||||
|
||||
return (retval, (d, tuple(samps)))
|
||||
|
||||
for chan in range(nchannels):
|
||||
prev_i[chan] = cur_i[chan]
|
||||
cur_i[chan] = samples.next()
|
||||
|
||||
cur_i[chan] = (
|
||||
(weightA * cur_i[chan] + weightB * prev_i[chan])
|
||||
/ (weightA + weightB)
|
||||
)
|
||||
|
||||
frame_count -= 1
|
||||
d += outrate
|
||||
|
||||
while d >= 0:
|
||||
for chan in range(nchannels):
|
||||
cur_o = (
|
||||
(prev_i[chan] * d + cur_i[chan] * (outrate - d))
|
||||
/ outrate
|
||||
)
|
||||
_put_sample(result, size, out_i, _overflow(cur_o, size))
|
||||
out_i += 1
|
||||
d -= inrate
|
||||
|
||||
|
||||
def lin2ulaw(cp, size):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def ulaw2lin(cp, size):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def lin2alaw(cp, size):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def alaw2lin(cp, size):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def lin2adpcm(cp, size, state):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def adpcm2lin(cp, size, state):
|
||||
raise NotImplementedError()
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
"""
|
||||
This module provides scipy versions of high_pass_filter, and low_pass_filter
|
||||
as well as an additional band_pass_filter.
|
||||
|
||||
Of course, you will need to install scipy for these to work.
|
||||
|
||||
When this module is imported the high and low pass filters from this module
|
||||
will be used when calling audio_segment.high_pass_filter() and
|
||||
audio_segment.high_pass_filter() instead of the slower, less powerful versions
|
||||
provided by pydub.effects.
|
||||
"""
|
||||
from scipy.signal import butter, sosfilt
|
||||
from .utils import (register_pydub_effect,stereo_to_ms,ms_to_stereo)
|
||||
|
||||
|
||||
def _mk_butter_filter(freq, type, order):
|
||||
"""
|
||||
Args:
|
||||
freq: The cutoff frequency for highpass and lowpass filters. For
|
||||
band filters, a list of [low_cutoff, high_cutoff]
|
||||
type: "lowpass", "highpass", or "band"
|
||||
order: nth order butterworth filter (default: 5th order). The
|
||||
attenuation is -6dB/octave beyond the cutoff frequency (for 1st
|
||||
order). A Higher order filter will have more attenuation, each level
|
||||
adding an additional -6dB (so a 3rd order butterworth filter would
|
||||
be -18dB/octave).
|
||||
|
||||
Returns:
|
||||
function which can filter a mono audio segment
|
||||
|
||||
"""
|
||||
def filter_fn(seg):
|
||||
assert seg.channels == 1
|
||||
|
||||
nyq = 0.5 * seg.frame_rate
|
||||
try:
|
||||
freqs = [f / nyq for f in freq]
|
||||
except TypeError:
|
||||
freqs = freq / nyq
|
||||
|
||||
sos = butter(order, freqs, btype=type, output='sos')
|
||||
y = sosfilt(sos, seg.get_array_of_samples())
|
||||
|
||||
return seg._spawn(y.astype(seg.array_type))
|
||||
|
||||
return filter_fn
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def band_pass_filter(seg, low_cutoff_freq, high_cutoff_freq, order=5):
|
||||
filter_fn = _mk_butter_filter([low_cutoff_freq, high_cutoff_freq], 'band', order=order)
|
||||
return seg.apply_mono_filter_to_each_channel(filter_fn)
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def high_pass_filter(seg, cutoff_freq, order=5):
|
||||
filter_fn = _mk_butter_filter(cutoff_freq, 'highpass', order=order)
|
||||
return seg.apply_mono_filter_to_each_channel(filter_fn)
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def low_pass_filter(seg, cutoff_freq, order=5):
|
||||
filter_fn = _mk_butter_filter(cutoff_freq, 'lowpass', order=order)
|
||||
return seg.apply_mono_filter_to_each_channel(filter_fn)
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def _eq(seg, focus_freq, bandwidth=100, mode="peak", gain_dB=0, order=2):
|
||||
"""
|
||||
Args:
|
||||
focus_freq - middle frequency or known frequency of band (in Hz)
|
||||
bandwidth - range of the equalizer band
|
||||
mode - Mode of Equalization(Peak/Notch(Bell Curve),High Shelf, Low Shelf)
|
||||
order - Rolloff factor(1 - 6dB/Octave 2 - 12dB/Octave)
|
||||
|
||||
Returns:
|
||||
Equalized/Filtered AudioSegment
|
||||
"""
|
||||
filt_mode = ["peak", "low_shelf", "high_shelf"]
|
||||
if mode not in filt_mode:
|
||||
raise ValueError("Incorrect Mode Selection")
|
||||
|
||||
if gain_dB >= 0:
|
||||
if mode == "peak":
|
||||
sec = band_pass_filter(seg, focus_freq - bandwidth/2, focus_freq + bandwidth/2, order = order)
|
||||
seg = seg.overlay(sec - (3 - gain_dB))
|
||||
return seg
|
||||
|
||||
if mode == "low_shelf":
|
||||
sec = low_pass_filter(seg, focus_freq, order=order)
|
||||
seg = seg.overlay(sec - (3 - gain_dB))
|
||||
return seg
|
||||
|
||||
if mode == "high_shelf":
|
||||
sec = high_pass_filter(seg, focus_freq, order=order)
|
||||
seg = seg.overlay(sec - (3 - gain_dB))
|
||||
return seg
|
||||
|
||||
if gain_dB < 0:
|
||||
if mode == "peak":
|
||||
sec = high_pass_filter(seg, focus_freq - bandwidth/2, order=order)
|
||||
seg = seg.overlay(sec - (3 + gain_dB)) + gain_dB
|
||||
sec = low_pass_filter(seg, focus_freq + bandwidth/2, order=order)
|
||||
seg = seg.overlay(sec - (3 + gain_dB)) + gain_dB
|
||||
return seg
|
||||
|
||||
if mode == "low_shelf":
|
||||
sec = high_pass_filter(seg, focus_freq, order=order)
|
||||
seg = seg.overlay(sec - (3 + gain_dB)) + gain_dB
|
||||
return seg
|
||||
|
||||
if mode=="high_shelf":
|
||||
sec=low_pass_filter(seg, focus_freq, order=order)
|
||||
seg=seg.overlay(sec - (3 + gain_dB)) +gain_dB
|
||||
return seg
|
||||
|
||||
|
||||
@register_pydub_effect
|
||||
def eq(seg, focus_freq, bandwidth=100, channel_mode="L+R", filter_mode="peak", gain_dB=0, order=2):
|
||||
"""
|
||||
Args:
|
||||
focus_freq - middle frequency or known frequency of band (in Hz)
|
||||
bandwidth - range of the equalizer band
|
||||
channel_mode - Select Channels to be affected by the filter.
|
||||
L+R - Standard Stereo Filter
|
||||
L - Only Left Channel is Filtered
|
||||
R - Only Right Channel is Filtered
|
||||
M+S - Blumlien Stereo Filter(Mid-Side)
|
||||
M - Only Mid Channel is Filtered
|
||||
S - Only Side Channel is Filtered
|
||||
Mono Audio Segments are completely filtered.
|
||||
filter_mode - Mode of Equalization(Peak/Notch(Bell Curve),High Shelf, Low Shelf)
|
||||
order - Rolloff factor(1 - 6dB/Octave 2 - 12dB/Octave)
|
||||
|
||||
Returns:
|
||||
Equalized/Filtered AudioSegment
|
||||
"""
|
||||
channel_modes = ["L+R", "M+S", "L", "R", "M", "S"]
|
||||
if channel_mode not in channel_modes:
|
||||
raise ValueError("Incorrect Channel Mode Selection")
|
||||
|
||||
if seg.channels == 1:
|
||||
return _eq(seg, focus_freq, bandwidth, filter_mode, gain_dB, order)
|
||||
|
||||
if channel_mode == "L+R":
|
||||
return _eq(seg, focus_freq, bandwidth, filter_mode, gain_dB, order)
|
||||
|
||||
if channel_mode == "L":
|
||||
seg = seg.split_to_mono()
|
||||
seg = [_eq(seg[0], focus_freq, bandwidth, filter_mode, gain_dB, order), seg[1]]
|
||||
return AudioSegment.from_mono_audio_segements(seg[0], seg[1])
|
||||
|
||||
if channel_mode == "R":
|
||||
seg = seg.split_to_mono()
|
||||
seg = [seg[0], _eq(seg[1], focus_freq, bandwidth, filter_mode, gain_dB, order)]
|
||||
return AudioSegment.from_mono_audio_segements(seg[0], seg[1])
|
||||
|
||||
if channel_mode == "M+S":
|
||||
seg = stereo_to_ms(seg)
|
||||
seg = _eq(seg, focus_freq, bandwidth, filter_mode, gain_dB, order)
|
||||
return ms_to_stereo(seg)
|
||||
|
||||
if channel_mode == "M":
|
||||
seg = stereo_to_ms(seg).split_to_mono()
|
||||
seg = [_eq(seg[0], focus_freq, bandwidth, filter_mode, gain_dB, order), seg[1]]
|
||||
seg = AudioSegment.from_mono_audio_segements(seg[0], seg[1])
|
||||
return ms_to_stereo(seg)
|
||||
|
||||
if channel_mode == "S":
|
||||
seg = stereo_to_ms(seg).split_to_mono()
|
||||
seg = [seg[0], _eq(seg[1], focus_freq, bandwidth, filter_mode, gain_dB, order)]
|
||||
seg = AudioSegment.from_mono_audio_segements(seg[0], seg[1])
|
||||
return ms_to_stereo(seg)
|
||||
|
||||
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
"""
|
||||
Various functions for finding/manipulating silence in AudioSegments
|
||||
"""
|
||||
import itertools
|
||||
|
||||
from .utils import db_to_float
|
||||
|
||||
|
||||
def detect_silence(audio_segment, min_silence_len=1000, silence_thresh=-16, seek_step=1):
|
||||
"""
|
||||
Returns a list of all silent sections [start, end] in milliseconds of audio_segment.
|
||||
Inverse of detect_nonsilent()
|
||||
|
||||
audio_segment - the segment to find silence in
|
||||
min_silence_len - the minimum length for any silent section
|
||||
silence_thresh - the upper bound for how quiet is silent in dFBS
|
||||
seek_step - step size for interating over the segment in ms
|
||||
"""
|
||||
seg_len = len(audio_segment)
|
||||
|
||||
# you can't have a silent portion of a sound that is longer than the sound
|
||||
if seg_len < min_silence_len:
|
||||
return []
|
||||
|
||||
# convert silence threshold to a float value (so we can compare it to rms)
|
||||
silence_thresh = db_to_float(silence_thresh) * audio_segment.max_possible_amplitude
|
||||
|
||||
# find silence and add start and end indicies to the to_cut list
|
||||
silence_starts = []
|
||||
|
||||
# check successive (1 sec by default) chunk of sound for silence
|
||||
# try a chunk at every "seek step" (or every chunk for a seek step == 1)
|
||||
last_slice_start = seg_len - min_silence_len
|
||||
slice_starts = range(0, last_slice_start + 1, seek_step)
|
||||
|
||||
# guarantee last_slice_start is included in the range
|
||||
# to make sure the last portion of the audio is searched
|
||||
if last_slice_start % seek_step:
|
||||
slice_starts = itertools.chain(slice_starts, [last_slice_start])
|
||||
|
||||
for i in slice_starts:
|
||||
audio_slice = audio_segment[i:i + min_silence_len]
|
||||
if audio_slice.rms <= silence_thresh:
|
||||
silence_starts.append(i)
|
||||
|
||||
# short circuit when there is no silence
|
||||
if not silence_starts:
|
||||
return []
|
||||
|
||||
# combine the silence we detected into ranges (start ms - end ms)
|
||||
silent_ranges = []
|
||||
|
||||
prev_i = silence_starts.pop(0)
|
||||
current_range_start = prev_i
|
||||
|
||||
for silence_start_i in silence_starts:
|
||||
continuous = (silence_start_i == prev_i + seek_step)
|
||||
|
||||
# sometimes two small blips are enough for one particular slice to be
|
||||
# non-silent, despite the silence all running together. Just combine
|
||||
# the two overlapping silent ranges.
|
||||
silence_has_gap = silence_start_i > (prev_i + min_silence_len)
|
||||
|
||||
if not continuous and silence_has_gap:
|
||||
silent_ranges.append([current_range_start,
|
||||
prev_i + min_silence_len])
|
||||
current_range_start = silence_start_i
|
||||
prev_i = silence_start_i
|
||||
|
||||
silent_ranges.append([current_range_start,
|
||||
prev_i + min_silence_len])
|
||||
|
||||
return silent_ranges
|
||||
|
||||
|
||||
def detect_nonsilent(audio_segment, min_silence_len=1000, silence_thresh=-16, seek_step=1):
|
||||
"""
|
||||
Returns a list of all nonsilent sections [start, end] in milliseconds of audio_segment.
|
||||
Inverse of detect_silent()
|
||||
|
||||
audio_segment - the segment to find silence in
|
||||
min_silence_len - the minimum length for any silent section
|
||||
silence_thresh - the upper bound for how quiet is silent in dFBS
|
||||
seek_step - step size for interating over the segment in ms
|
||||
"""
|
||||
silent_ranges = detect_silence(audio_segment, min_silence_len, silence_thresh, seek_step)
|
||||
len_seg = len(audio_segment)
|
||||
|
||||
# if there is no silence, the whole thing is nonsilent
|
||||
if not silent_ranges:
|
||||
return [[0, len_seg]]
|
||||
|
||||
# short circuit when the whole audio segment is silent
|
||||
if silent_ranges[0][0] == 0 and silent_ranges[0][1] == len_seg:
|
||||
return []
|
||||
|
||||
prev_end_i = 0
|
||||
nonsilent_ranges = []
|
||||
for start_i, end_i in silent_ranges:
|
||||
nonsilent_ranges.append([prev_end_i, start_i])
|
||||
prev_end_i = end_i
|
||||
|
||||
if end_i != len_seg:
|
||||
nonsilent_ranges.append([prev_end_i, len_seg])
|
||||
|
||||
if nonsilent_ranges[0] == [0, 0]:
|
||||
nonsilent_ranges.pop(0)
|
||||
|
||||
return nonsilent_ranges
|
||||
|
||||
|
||||
def split_on_silence(audio_segment, min_silence_len=1000, silence_thresh=-16, keep_silence=100,
|
||||
seek_step=1):
|
||||
"""
|
||||
Returns list of audio segments from splitting audio_segment on silent sections
|
||||
|
||||
audio_segment - original pydub.AudioSegment() object
|
||||
|
||||
min_silence_len - (in ms) minimum length of a silence to be used for
|
||||
a split. default: 1000ms
|
||||
|
||||
silence_thresh - (in dBFS) anything quieter than this will be
|
||||
considered silence. default: -16dBFS
|
||||
|
||||
keep_silence - (in ms or True/False) leave some silence at the beginning
|
||||
and end of the chunks. Keeps the sound from sounding like it
|
||||
is abruptly cut off.
|
||||
When the length of the silence is less than the keep_silence duration
|
||||
it is split evenly between the preceding and following non-silent
|
||||
segments.
|
||||
If True is specified, all the silence is kept, if False none is kept.
|
||||
default: 100ms
|
||||
|
||||
seek_step - step size for interating over the segment in ms
|
||||
"""
|
||||
|
||||
# from the itertools documentation
|
||||
def pairwise(iterable):
|
||||
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
|
||||
a, b = itertools.tee(iterable)
|
||||
next(b, None)
|
||||
return zip(a, b)
|
||||
|
||||
if isinstance(keep_silence, bool):
|
||||
keep_silence = len(audio_segment) if keep_silence else 0
|
||||
|
||||
output_ranges = [
|
||||
[ start - keep_silence, end + keep_silence ]
|
||||
for (start,end)
|
||||
in detect_nonsilent(audio_segment, min_silence_len, silence_thresh, seek_step)
|
||||
]
|
||||
|
||||
for range_i, range_ii in pairwise(output_ranges):
|
||||
last_end = range_i[1]
|
||||
next_start = range_ii[0]
|
||||
if next_start < last_end:
|
||||
range_i[1] = (last_end+next_start)//2
|
||||
range_ii[0] = range_i[1]
|
||||
|
||||
return [
|
||||
audio_segment[ max(start,0) : min(end,len(audio_segment)) ]
|
||||
for start,end in output_ranges
|
||||
]
|
||||
|
||||
|
||||
def detect_leading_silence(sound, silence_threshold=-50.0, chunk_size=10):
|
||||
"""
|
||||
Returns the millisecond/index that the leading silence ends.
|
||||
|
||||
audio_segment - the segment to find silence in
|
||||
silence_threshold - the upper bound for how quiet is silent in dFBS
|
||||
chunk_size - chunk size for interating over the segment in ms
|
||||
"""
|
||||
trim_ms = 0 # ms
|
||||
assert chunk_size > 0 # to avoid infinite loop
|
||||
while sound[trim_ms:trim_ms+chunk_size].dBFS < silence_threshold and trim_ms < len(sound):
|
||||
trim_ms += chunk_size
|
||||
|
||||
# if there is no end it should return the length of the segment
|
||||
return min(trim_ms, len(sound))
|
||||
|
||||
|
||||
|
|
@ -1,434 +0,0 @@
|
|||
from __future__ import division
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from subprocess import Popen, PIPE
|
||||
from math import log, ceil
|
||||
from tempfile import TemporaryFile
|
||||
from warnings import warn
|
||||
from functools import wraps
|
||||
|
||||
try:
|
||||
import audioop
|
||||
except ImportError:
|
||||
import pyaudioop as audioop
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
basestring = str
|
||||
|
||||
FRAME_WIDTHS = {
|
||||
8: 1,
|
||||
16: 2,
|
||||
32: 4,
|
||||
}
|
||||
ARRAY_TYPES = {
|
||||
8: "b",
|
||||
16: "h",
|
||||
32: "i",
|
||||
}
|
||||
ARRAY_RANGES = {
|
||||
8: (-0x80, 0x7f),
|
||||
16: (-0x8000, 0x7fff),
|
||||
32: (-0x80000000, 0x7fffffff),
|
||||
}
|
||||
|
||||
|
||||
def get_frame_width(bit_depth):
|
||||
return FRAME_WIDTHS[bit_depth]
|
||||
|
||||
|
||||
def get_array_type(bit_depth, signed=True):
|
||||
t = ARRAY_TYPES[bit_depth]
|
||||
if not signed:
|
||||
t = t.upper()
|
||||
return t
|
||||
|
||||
|
||||
def get_min_max_value(bit_depth):
|
||||
return ARRAY_RANGES[bit_depth]
|
||||
|
||||
|
||||
def _fd_or_path_or_tempfile(fd, mode='w+b', tempfile=True):
|
||||
close_fd = False
|
||||
if fd is None and tempfile:
|
||||
fd = TemporaryFile(mode=mode)
|
||||
close_fd = True
|
||||
|
||||
if isinstance(fd, basestring):
|
||||
fd = open(fd, mode=mode)
|
||||
close_fd = True
|
||||
|
||||
try:
|
||||
if isinstance(fd, os.PathLike):
|
||||
fd = open(fd, mode=mode)
|
||||
close_fd = True
|
||||
except AttributeError:
|
||||
# module os has no attribute PathLike, so we're on python < 3.6.
|
||||
# The protocol we're trying to support doesn't exist, so just pass.
|
||||
pass
|
||||
|
||||
return fd, close_fd
|
||||
|
||||
|
||||
def db_to_float(db, using_amplitude=True):
|
||||
"""
|
||||
Converts the input db to a float, which represents the equivalent
|
||||
ratio in power.
|
||||
"""
|
||||
db = float(db)
|
||||
if using_amplitude:
|
||||
return 10 ** (db / 20)
|
||||
else: # using power
|
||||
return 10 ** (db / 10)
|
||||
|
||||
|
||||
def ratio_to_db(ratio, val2=None, using_amplitude=True):
|
||||
"""
|
||||
Converts the input float to db, which represents the equivalent
|
||||
to the ratio in power represented by the multiplier passed in.
|
||||
"""
|
||||
ratio = float(ratio)
|
||||
|
||||
# accept 2 values and use the ratio of val1 to val2
|
||||
if val2 is not None:
|
||||
ratio = ratio / val2
|
||||
|
||||
# special case for multiply-by-zero (convert to silence)
|
||||
if ratio == 0:
|
||||
return -float('inf')
|
||||
|
||||
if using_amplitude:
|
||||
return 20 * log(ratio, 10)
|
||||
else: # using power
|
||||
return 10 * log(ratio, 10)
|
||||
|
||||
|
||||
def register_pydub_effect(fn, name=None):
|
||||
"""
|
||||
decorator for adding pydub effects to the AudioSegment objects.
|
||||
example use:
|
||||
@register_pydub_effect
|
||||
def normalize(audio_segment):
|
||||
...
|
||||
or you can specify a name:
|
||||
@register_pydub_effect("normalize")
|
||||
def normalize_audio_segment(audio_segment):
|
||||
...
|
||||
"""
|
||||
if isinstance(fn, basestring):
|
||||
name = fn
|
||||
return lambda fn: register_pydub_effect(fn, name)
|
||||
|
||||
if name is None:
|
||||
name = fn.__name__
|
||||
|
||||
from .audio_segment import AudioSegment
|
||||
setattr(AudioSegment, name, fn)
|
||||
return fn
|
||||
|
||||
|
||||
def make_chunks(audio_segment, chunk_length):
|
||||
"""
|
||||
Breaks an AudioSegment into chunks that are <chunk_length> milliseconds
|
||||
long.
|
||||
if chunk_length is 50 then you'll get a list of 50 millisecond long audio
|
||||
segments back (except the last one, which can be shorter)
|
||||
"""
|
||||
number_of_chunks = ceil(len(audio_segment) / float(chunk_length))
|
||||
return [audio_segment[i * chunk_length:(i + 1) * chunk_length]
|
||||
for i in range(int(number_of_chunks))]
|
||||
|
||||
|
||||
def which(program):
|
||||
"""
|
||||
Mimics behavior of UNIX which command.
|
||||
"""
|
||||
# Add .exe program extension for windows support
|
||||
if os.name == "nt" and not program.endswith(".exe"):
|
||||
program += ".exe"
|
||||
|
||||
envdir_list = [os.curdir] + os.environ["PATH"].split(os.pathsep)
|
||||
|
||||
for envdir in envdir_list:
|
||||
program_path = os.path.join(envdir, program)
|
||||
if os.path.isfile(program_path) and os.access(program_path, os.X_OK):
|
||||
return program_path
|
||||
|
||||
|
||||
def get_encoder_name():
|
||||
"""
|
||||
Return enconder default application for system, either avconv or ffmpeg
|
||||
"""
|
||||
if which("avconv"):
|
||||
return "avconv"
|
||||
elif which("ffmpeg"):
|
||||
return "ffmpeg"
|
||||
else:
|
||||
# should raise exception
|
||||
warn("Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work", RuntimeWarning)
|
||||
return "ffmpeg"
|
||||
|
||||
|
||||
def get_player_name():
|
||||
"""
|
||||
Return enconder default application for system, either avconv or ffmpeg
|
||||
"""
|
||||
if which("avplay"):
|
||||
return "avplay"
|
||||
elif which("ffplay"):
|
||||
return "ffplay"
|
||||
else:
|
||||
# should raise exception
|
||||
warn("Couldn't find ffplay or avplay - defaulting to ffplay, but may not work", RuntimeWarning)
|
||||
return "ffplay"
|
||||
|
||||
|
||||
def get_prober_name():
|
||||
"""
|
||||
Return probe application, either avconv or ffmpeg
|
||||
"""
|
||||
if which("avprobe"):
|
||||
return "avprobe"
|
||||
elif which("ffprobe"):
|
||||
return "ffprobe"
|
||||
else:
|
||||
# should raise exception
|
||||
warn("Couldn't find ffprobe or avprobe - defaulting to ffprobe, but may not work", RuntimeWarning)
|
||||
return "ffprobe"
|
||||
|
||||
|
||||
def fsdecode(filename):
|
||||
"""Wrapper for os.fsdecode which was introduced in python 3.2 ."""
|
||||
|
||||
if sys.version_info >= (3, 2):
|
||||
PathLikeTypes = (basestring, bytes)
|
||||
if sys.version_info >= (3, 6):
|
||||
PathLikeTypes += (os.PathLike,)
|
||||
if isinstance(filename, PathLikeTypes):
|
||||
return os.fsdecode(filename)
|
||||
else:
|
||||
if isinstance(filename, bytes):
|
||||
return filename.decode(sys.getfilesystemencoding())
|
||||
if isinstance(filename, basestring):
|
||||
return filename
|
||||
|
||||
raise TypeError("type {0} not accepted by fsdecode".format(type(filename)))
|
||||
|
||||
|
||||
def get_extra_info(stderr):
|
||||
"""
|
||||
avprobe sometimes gives more information on stderr than
|
||||
on the json output. The information has to be extracted
|
||||
from stderr of the format of:
|
||||
' Stream #0:0: Audio: flac, 88200 Hz, stereo, s32 (24 bit)'
|
||||
or (macOS version):
|
||||
' Stream #0:0: Audio: vorbis'
|
||||
' 44100 Hz, stereo, fltp, 320 kb/s'
|
||||
|
||||
:type stderr: str
|
||||
:rtype: list of dict
|
||||
"""
|
||||
extra_info = {}
|
||||
|
||||
re_stream = r'(?P<space_start> +)Stream #0[:\.](?P<stream_id>([0-9]+))(?P<content_0>.+)\n?(?! *Stream)((?P<space_end> +)(?P<content_1>.+))?'
|
||||
for i in re.finditer(re_stream, stderr):
|
||||
if i.group('space_end') is not None and len(i.group('space_start')) <= len(
|
||||
i.group('space_end')):
|
||||
content_line = ','.join([i.group('content_0'), i.group('content_1')])
|
||||
else:
|
||||
content_line = i.group('content_0')
|
||||
tokens = [x.strip() for x in re.split('[:,]', content_line) if x]
|
||||
extra_info[int(i.group('stream_id'))] = tokens
|
||||
return extra_info
|
||||
|
||||
|
||||
def mediainfo_json(filepath, read_ahead_limit=-1):
|
||||
"""Return json dictionary with media info(codec, duration, size, bitrate...) from filepath
|
||||
"""
|
||||
prober = get_prober_name()
|
||||
command_args = [
|
||||
"-v", "info",
|
||||
"-show_format",
|
||||
"-show_streams",
|
||||
]
|
||||
try:
|
||||
command_args += [fsdecode(filepath)]
|
||||
stdin_parameter = None
|
||||
stdin_data = None
|
||||
except TypeError:
|
||||
if prober == 'ffprobe':
|
||||
command_args += ["-read_ahead_limit", str(read_ahead_limit),
|
||||
"cache:pipe:0"]
|
||||
else:
|
||||
command_args += ["-"]
|
||||
stdin_parameter = PIPE
|
||||
file, close_file = _fd_or_path_or_tempfile(filepath, 'rb', tempfile=False)
|
||||
file.seek(0)
|
||||
stdin_data = file.read()
|
||||
if close_file:
|
||||
file.close()
|
||||
|
||||
command = [prober, '-of', 'json'] + command_args
|
||||
res = Popen(command, stdin=stdin_parameter, stdout=PIPE, stderr=PIPE)
|
||||
output, stderr = res.communicate(input=stdin_data)
|
||||
output = output.decode("utf-8", 'ignore')
|
||||
stderr = stderr.decode("utf-8", 'ignore')
|
||||
|
||||
info = json.loads(output)
|
||||
|
||||
if not info:
|
||||
# If ffprobe didn't give any information, just return it
|
||||
# (for example, because the file doesn't exist)
|
||||
return info
|
||||
|
||||
extra_info = get_extra_info(stderr)
|
||||
|
||||
audio_streams = [x for x in info['streams'] if x['codec_type'] == 'audio']
|
||||
if len(audio_streams) == 0:
|
||||
return info
|
||||
|
||||
# We just operate on the first audio stream in case there are more
|
||||
stream = audio_streams[0]
|
||||
|
||||
def set_property(stream, prop, value):
|
||||
if prop not in stream or stream[prop] == 0:
|
||||
stream[prop] = value
|
||||
|
||||
for token in extra_info[stream['index']]:
|
||||
m = re.match('([su]([0-9]{1,2})p?) \(([0-9]{1,2}) bit\)$', token)
|
||||
m2 = re.match('([su]([0-9]{1,2})p?)( \(default\))?$', token)
|
||||
if m:
|
||||
set_property(stream, 'sample_fmt', m.group(1))
|
||||
set_property(stream, 'bits_per_sample', int(m.group(2)))
|
||||
set_property(stream, 'bits_per_raw_sample', int(m.group(3)))
|
||||
elif m2:
|
||||
set_property(stream, 'sample_fmt', m2.group(1))
|
||||
set_property(stream, 'bits_per_sample', int(m2.group(2)))
|
||||
set_property(stream, 'bits_per_raw_sample', int(m2.group(2)))
|
||||
elif re.match('(flt)p?( \(default\))?$', token):
|
||||
set_property(stream, 'sample_fmt', token)
|
||||
set_property(stream, 'bits_per_sample', 32)
|
||||
set_property(stream, 'bits_per_raw_sample', 32)
|
||||
elif re.match('(dbl)p?( \(default\))?$', token):
|
||||
set_property(stream, 'sample_fmt', token)
|
||||
set_property(stream, 'bits_per_sample', 64)
|
||||
set_property(stream, 'bits_per_raw_sample', 64)
|
||||
return info
|
||||
|
||||
|
||||
def mediainfo(filepath):
|
||||
"""Return dictionary with media info(codec, duration, size, bitrate...) from filepath
|
||||
"""
|
||||
|
||||
prober = get_prober_name()
|
||||
command_args = [
|
||||
"-v", "quiet",
|
||||
"-show_format",
|
||||
"-show_streams",
|
||||
filepath
|
||||
]
|
||||
|
||||
command = [prober, '-of', 'old'] + command_args
|
||||
res = Popen(command, stdout=PIPE)
|
||||
output = res.communicate()[0].decode("utf-8")
|
||||
|
||||
if res.returncode != 0:
|
||||
command = [prober] + command_args
|
||||
output = Popen(command, stdout=PIPE).communicate()[0].decode("utf-8")
|
||||
|
||||
rgx = re.compile(r"(?:(?P<inner_dict>.*?):)?(?P<key>.*?)\=(?P<value>.*?)$")
|
||||
info = {}
|
||||
|
||||
if sys.platform == 'win32':
|
||||
output = output.replace("\r", "")
|
||||
|
||||
for line in output.split("\n"):
|
||||
# print(line)
|
||||
mobj = rgx.match(line)
|
||||
|
||||
if mobj:
|
||||
# print(mobj.groups())
|
||||
inner_dict, key, value = mobj.groups()
|
||||
|
||||
if inner_dict:
|
||||
try:
|
||||
info[inner_dict]
|
||||
except KeyError:
|
||||
info[inner_dict] = {}
|
||||
info[inner_dict][key] = value
|
||||
else:
|
||||
info[key] = value
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def cache_codecs(function):
|
||||
cache = {}
|
||||
|
||||
@wraps(function)
|
||||
def wrapper():
|
||||
try:
|
||||
return cache[0]
|
||||
except:
|
||||
cache[0] = function()
|
||||
return cache[0]
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@cache_codecs
|
||||
def get_supported_codecs():
|
||||
encoder = get_encoder_name()
|
||||
command = [encoder, "-codecs"]
|
||||
res = Popen(command, stdout=PIPE, stderr=PIPE)
|
||||
output = res.communicate()[0].decode("utf-8")
|
||||
if res.returncode != 0:
|
||||
return []
|
||||
|
||||
if sys.platform == 'win32':
|
||||
output = output.replace("\r", "")
|
||||
|
||||
|
||||
rgx = re.compile(r"^([D.][E.][AVS.][I.][L.][S.]) (\w*) +(.*)")
|
||||
decoders = set()
|
||||
encoders = set()
|
||||
for line in output.split('\n'):
|
||||
match = rgx.match(line.strip())
|
||||
if not match:
|
||||
continue
|
||||
flags, codec, name = match.groups()
|
||||
|
||||
if flags[0] == 'D':
|
||||
decoders.add(codec)
|
||||
|
||||
if flags[1] == 'E':
|
||||
encoders.add(codec)
|
||||
|
||||
return (decoders, encoders)
|
||||
|
||||
|
||||
def get_supported_decoders():
|
||||
return get_supported_codecs()[0]
|
||||
|
||||
|
||||
def get_supported_encoders():
|
||||
return get_supported_codecs()[1]
|
||||
|
||||
def stereo_to_ms(audio_segment):
|
||||
'''
|
||||
Left-Right -> Mid-Side
|
||||
'''
|
||||
channel = audio_segment.split_to_mono()
|
||||
channel = [channel[0].overlay(channel[1]), channel[0].overlay(channel[1].invert_phase())]
|
||||
return AudioSegment.from_mono_audiosegments(channel[0], channel[1])
|
||||
|
||||
def ms_to_stereo(audio_segment):
|
||||
'''
|
||||
Mid-Side -> Left-Right
|
||||
'''
|
||||
channel = audio_segment.split_to_mono()
|
||||
channel = [channel[0].overlay(channel[1]) - 3, channel[0].overlay(channel[1].invert_phase()) - 3]
|
||||
return AudioSegment.from_mono_audiosegments(channel[0], channel[1])
|
||||
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
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?)")
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
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
2061
sbapp/pyogg/flac.py
File diff suppressed because it is too large
Load diff
|
|
@ -1,114 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
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)
|
||||
)
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
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))
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,672 +0,0 @@
|
|||
############################################################
|
||||
# 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)
|
||||
|
|
@ -1,421 +0,0 @@
|
|||
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
1377
sbapp/pyogg/opus.py
File diff suppressed because it is too large
Load diff
|
|
@ -1,407 +0,0 @@
|
|||
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)
|
||||
|
|
@ -1,273 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,358 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
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)
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
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 +0,0 @@
|
|||
# Marker file for PEP 561. This package uses inline types.
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
class PyOggError(Exception):
|
||||
pass
|
||||
|
|
@ -1,855 +0,0 @@
|
|||
############################################################
|
||||
# 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
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
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))
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import ctypes
|
||||
|
||||
from . import vorbis
|
||||
from .pyogg_error import PyOggError
|
||||
|
||||
class VorbisFileStream:
|
||||
def __init__(self, path, buffer_size=8192):
|
||||
self.exists = False
|
||||
self._buffer_size = buffer_size
|
||||
|
||||
self.vf = vorbis.OggVorbis_File()
|
||||
error = vorbis.ov_fopen(path, ctypes.byref(self.vf))
|
||||
if error != 0:
|
||||
raise PyOggError("file couldn't be opened or doesn't exist. Error code : {}".format(error))
|
||||
|
||||
info = vorbis.ov_info(ctypes.byref(self.vf), -1)
|
||||
|
||||
#: Number of channels in audio file.
|
||||
self.channels = info.contents.channels
|
||||
|
||||
#: Number of samples per second (per channel). Always
|
||||
# 48,000.
|
||||
self.frequency = info.contents.rate
|
||||
|
||||
array = (ctypes.c_char*(self._buffer_size*self.channels))()
|
||||
|
||||
self.buffer_ = ctypes.cast(ctypes.pointer(array), ctypes.c_char_p)
|
||||
|
||||
self.bitstream = ctypes.c_int()
|
||||
self.bitstream_pointer = ctypes.pointer(self.bitstream)
|
||||
|
||||
self.exists = True # TODO: is this the best place for this statement?
|
||||
|
||||
#: Bytes per sample
|
||||
self.bytes_per_sample = 2 # TODO: Where is this defined?
|
||||
|
||||
def __del__(self):
|
||||
if self.exists:
|
||||
vorbis.ov_clear(ctypes.byref(self.vf))
|
||||
self.exists = False
|
||||
|
||||
def clean_up(self):
|
||||
vorbis.ov_clear(ctypes.byref(self.vf))
|
||||
|
||||
self.exists = False
|
||||
|
||||
def get_buffer(self):
|
||||
"""get_buffer() -> bytesBuffer, bufferLength
|
||||
|
||||
Returns None when all data has been read from the file.
|
||||
|
||||
"""
|
||||
if not self.exists:
|
||||
return None
|
||||
buffer = []
|
||||
total_bytes_written = 0
|
||||
|
||||
while True:
|
||||
new_bytes = vorbis.ov_read(ctypes.byref(self.vf), self.buffer_, self._buffer_size*self.channels - total_bytes_written, 0, 2, 1, self.bitstream_pointer)
|
||||
|
||||
array_ = ctypes.cast(self.buffer_, ctypes.POINTER(ctypes.c_char*(self._buffer_size*self.channels))).contents
|
||||
|
||||
buffer.append(array_.raw[:new_bytes])
|
||||
|
||||
total_bytes_written += new_bytes
|
||||
|
||||
if new_bytes == 0 or total_bytes_written >= self._buffer_size*self.channels:
|
||||
break
|
||||
|
||||
out_buffer = b"".join(buffer)
|
||||
|
||||
if total_bytes_written == 0:
|
||||
self.clean_up()
|
||||
return(None)
|
||||
|
||||
return out_buffer
|
||||
|
||||
def get_buffer_as_array(self):
|
||||
"""Provides the buffer as a NumPy array.
|
||||
|
||||
Note that the underlying data type is 16-bit signed
|
||||
integers.
|
||||
|
||||
Does not copy the underlying data, so the returned array
|
||||
should either be processed or copied before the next call
|
||||
to get_buffer() or get_buffer_as_array().
|
||||
|
||||
"""
|
||||
import numpy # type: ignore
|
||||
|
||||
# Read the next samples from the stream
|
||||
buf = self.get_buffer()
|
||||
|
||||
# Check if we've come to the end of the stream
|
||||
if buf is None:
|
||||
return None
|
||||
|
||||
# Convert the bytes buffer to a NumPy array
|
||||
array = numpy.frombuffer(
|
||||
buf,
|
||||
dtype=numpy.int16
|
||||
)
|
||||
|
||||
# Reshape the array
|
||||
return array.reshape(
|
||||
(len(buf)
|
||||
// self.bytes_per_sample
|
||||
// self.channels,
|
||||
self.channels)
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue