Add in Decoder
This commit is contained in:
parent
a4b20d08c3
commit
a20ddfa766
214
discord/opus.py
214
discord/opus.py
@ -28,13 +28,16 @@ import array
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import logging
|
||||
import math
|
||||
import os.path
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from .errors import DiscordException
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
c_int_ptr = ctypes.POINTER(ctypes.c_int)
|
||||
|
||||
c_int_ptr = ctypes.POINTER(ctypes.c_int)
|
||||
c_int16_ptr = ctypes.POINTER(ctypes.c_int16)
|
||||
c_float_ptr = ctypes.POINTER(ctypes.c_float)
|
||||
|
||||
@ -43,17 +46,55 @@ _lib = None
|
||||
class EncoderStruct(ctypes.Structure):
|
||||
pass
|
||||
|
||||
class DecoderStruct(ctypes.Structure):
|
||||
pass
|
||||
|
||||
EncoderStructPtr = ctypes.POINTER(EncoderStruct)
|
||||
DecoderStructPtr = ctypes.POINTER(DecoderStruct)
|
||||
|
||||
## Some constants from opus_defines.h
|
||||
# Error codes
|
||||
OK = 0
|
||||
BAD_ARG = -1
|
||||
|
||||
# Encoder CTLs
|
||||
APPLICATION_AUDIO = 2049
|
||||
APPLICATION_VOIP = 2048
|
||||
APPLICATION_LOWDELAY = 2051
|
||||
|
||||
CTL_SET_BITRATE = 4002
|
||||
CTL_SET_BANDWIDTH = 4008
|
||||
CTL_SET_FEC = 4012
|
||||
CTL_SET_PLP = 4014
|
||||
CTL_SET_SIGNAL = 4024
|
||||
|
||||
# Decoder CTLs
|
||||
CTL_SET_GAIN = 4034
|
||||
CTL_LAST_PACKET_DURATION = 4039
|
||||
|
||||
band_ctl = {
|
||||
'narrow': 1101,
|
||||
'medium': 1102,
|
||||
'wide': 1103,
|
||||
'superwide': 1104,
|
||||
'full': 1105,
|
||||
}
|
||||
|
||||
signal_ctl = {
|
||||
'auto': -1000,
|
||||
'voice': 3001,
|
||||
'music': 3002,
|
||||
}
|
||||
|
||||
def _err_lt(result, func, args):
|
||||
if result < 0:
|
||||
if result < OK:
|
||||
log.info('error has happened in %s', func.__name__)
|
||||
raise OpusError(result)
|
||||
return result
|
||||
|
||||
def _err_ne(result, func, args):
|
||||
ret = args[-1]._obj
|
||||
if ret.value != 0:
|
||||
if ret.value != OK:
|
||||
log.info('error has happened in %s', func.__name__)
|
||||
raise OpusError(ret.value)
|
||||
return result
|
||||
@ -64,18 +105,53 @@ def _err_ne(result, func, args):
|
||||
# The third is the result type.
|
||||
# The fourth is the error handler.
|
||||
exported_functions = [
|
||||
# Generic
|
||||
('opus_get_version_string',
|
||||
None, ctypes.c_char_p, None),
|
||||
('opus_strerror',
|
||||
[ctypes.c_int], ctypes.c_char_p, None),
|
||||
|
||||
# Encoder functions
|
||||
('opus_encoder_get_size',
|
||||
[ctypes.c_int], ctypes.c_int, None),
|
||||
('opus_encoder_create',
|
||||
[ctypes.c_int, ctypes.c_int, ctypes.c_int, c_int_ptr], EncoderStructPtr, _err_ne),
|
||||
('opus_encode',
|
||||
[EncoderStructPtr, c_int16_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int32, _err_lt),
|
||||
('opus_encode_float',
|
||||
[EncoderStructPtr, c_float_ptr, ctypes.c_int, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int32, _err_lt),
|
||||
('opus_encoder_ctl',
|
||||
None, ctypes.c_int32, _err_lt),
|
||||
('opus_encoder_destroy',
|
||||
[EncoderStructPtr], None, None),
|
||||
|
||||
# Decoder functions
|
||||
('opus_decoder_get_size',
|
||||
[ctypes.c_int], ctypes.c_int, None),
|
||||
('opus_decoder_create',
|
||||
[ctypes.c_int, ctypes.c_int, c_int_ptr], DecoderStructPtr, _err_ne),
|
||||
('opus_decode',
|
||||
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32, c_int16_ptr, ctypes.c_int, ctypes.c_int],
|
||||
ctypes.c_int, _err_lt),
|
||||
('opus_decode_float',
|
||||
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32, c_float_ptr, ctypes.c_int, ctypes.c_int],
|
||||
ctypes.c_int, _err_lt),
|
||||
('opus_decoder_ctl',
|
||||
None, ctypes.c_int32, _err_lt),
|
||||
('opus_decoder_destroy',
|
||||
[DecoderStructPtr], None, None),
|
||||
('opus_decoder_get_nb_samples',
|
||||
[DecoderStructPtr, ctypes.c_char_p, ctypes.c_int32], ctypes.c_int, _err_lt),
|
||||
|
||||
# Packet functions
|
||||
('opus_packet_get_bandwidth',
|
||||
[ctypes.c_char_p], ctypes.c_int, _err_lt),
|
||||
('opus_packet_get_nb_channels',
|
||||
[ctypes.c_char_p], ctypes.c_int, _err_lt),
|
||||
('opus_packet_get_nb_frames',
|
||||
[ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
|
||||
('opus_packet_get_samples_per_frame',
|
||||
[ctypes.c_char_p, ctypes.c_int], ctypes.c_int, _err_lt),
|
||||
]
|
||||
|
||||
def libopus_loader(name):
|
||||
@ -107,8 +183,9 @@ def _load_default():
|
||||
try:
|
||||
if sys.platform == 'win32':
|
||||
_basedir = os.path.dirname(os.path.abspath(__file__))
|
||||
_bitness = 'x64' if sys.maxsize > 2**32 else 'x86'
|
||||
_filename = os.path.join(_basedir, 'bin', 'libopus-0.{}.dll'.format(_bitness))
|
||||
_bitness = struct.calcsize('P') * 8
|
||||
_target = 'x64' if _bitness > 32 else 'x86'
|
||||
_filename = os.path.join(_basedir, 'bin', 'libopus-0.{}.dll'.format(_target))
|
||||
_lib = libopus_loader(_filename)
|
||||
else:
|
||||
_lib = libopus_loader(ctypes.util.find_library('opus'))
|
||||
@ -188,48 +265,30 @@ class OpusNotLoaded(DiscordException):
|
||||
"""An exception that is thrown for when libopus is not loaded."""
|
||||
pass
|
||||
|
||||
|
||||
# Some constants...
|
||||
OK = 0
|
||||
APPLICATION_AUDIO = 2049
|
||||
APPLICATION_VOIP = 2048
|
||||
APPLICATION_LOWDELAY = 2051
|
||||
CTL_SET_BITRATE = 4002
|
||||
CTL_SET_BANDWIDTH = 4008
|
||||
CTL_SET_FEC = 4012
|
||||
CTL_SET_PLP = 4014
|
||||
CTL_SET_SIGNAL = 4024
|
||||
|
||||
band_ctl = {
|
||||
'narrow': 1101,
|
||||
'medium': 1102,
|
||||
'wide': 1103,
|
||||
'superwide': 1104,
|
||||
'full': 1105,
|
||||
}
|
||||
|
||||
signal_ctl = {
|
||||
'auto': -1000,
|
||||
'voice': 3001,
|
||||
'music': 3002,
|
||||
}
|
||||
|
||||
class Encoder:
|
||||
class _OpusStruct:
|
||||
SAMPLING_RATE = 48000
|
||||
CHANNELS = 2
|
||||
FRAME_LENGTH = 20
|
||||
SAMPLE_SIZE = 4 # (bit_rate / 8) * CHANNELS (bit_rate == 16)
|
||||
FRAME_LENGTH = 20 # in milliseconds
|
||||
SAMPLE_SIZE = struct.calcsize('h') * CHANNELS
|
||||
SAMPLES_PER_FRAME = int(SAMPLING_RATE / 1000 * FRAME_LENGTH)
|
||||
|
||||
FRAME_SIZE = SAMPLES_PER_FRAME * SAMPLE_SIZE
|
||||
|
||||
def __init__(self, application=APPLICATION_AUDIO):
|
||||
self.application = application
|
||||
|
||||
@staticmethod
|
||||
def get_opus_version() -> str:
|
||||
if not is_loaded():
|
||||
if not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
|
||||
return _lib.opus_get_version_string().decode('utf-8')
|
||||
|
||||
class Encoder(_OpusStruct):
|
||||
def __init__(self, application=APPLICATION_AUDIO):
|
||||
if not is_loaded():
|
||||
if not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
|
||||
self.application = application
|
||||
self._state = self._create_state()
|
||||
self.set_bitrate(128)
|
||||
self.set_fec(True)
|
||||
@ -280,3 +339,84 @@ class Encoder:
|
||||
ret = _lib.opus_encode(self._state, pcm, frame_size, data, max_data_bytes)
|
||||
|
||||
return array.array('b', data[:ret]).tobytes()
|
||||
|
||||
class Decoder(_OpusStruct):
|
||||
def __init__(self):
|
||||
if not is_loaded():
|
||||
if not _load_default():
|
||||
raise OpusNotLoaded()
|
||||
|
||||
self._state = self._create_state()
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, '_state'):
|
||||
_lib.opus_decoder_destroy(self._state)
|
||||
self._state = None
|
||||
|
||||
def _create_state(self):
|
||||
ret = ctypes.c_int()
|
||||
return _lib.opus_decoder_create(self.SAMPLING_RATE, self.CHANNELS, ctypes.byref(ret))
|
||||
|
||||
@staticmethod
|
||||
def packet_get_nb_frames(data):
|
||||
"""Gets the number of frames in an Opus packet"""
|
||||
return _lib.opus_packet_get_nb_frames(data, len(data))
|
||||
|
||||
@staticmethod
|
||||
def packet_get_nb_channels(data):
|
||||
"""Gets the number of channels in an Opus packet"""
|
||||
return _lib.opus_packet_get_nb_channels(data)
|
||||
|
||||
@classmethod
|
||||
def packet_get_samples_per_frame(cls, data):
|
||||
"""Gets the number of samples per frame from an Opus packet"""
|
||||
return _lib.opus_packet_get_samples_per_frame(data, cls.SAMPLING_RATE)
|
||||
|
||||
def _set_gain(self, adjustment):
|
||||
"""Configures decoder gain adjustment.
|
||||
|
||||
Scales the decoded output by a factor specified in Q8 dB units.
|
||||
This has a maximum range of -32768 to 32767 inclusive, and returns
|
||||
OPUS_BAD_ARG (-1) otherwise. The default is zero indicating no adjustment.
|
||||
This setting survives decoder reset (irrelevant for now).
|
||||
gain = 10**x/(20.0*256)
|
||||
(from opus_defines.h)
|
||||
"""
|
||||
return _lib.opus_decoder_ctl(self._state, CTL_SET_GAIN, adjustment)
|
||||
|
||||
def set_gain(self, dB):
|
||||
"""Sets the decoder gain in dB, from -128 to 128."""
|
||||
|
||||
dB_Q8 = max(-32768, min(32767, round(dB * 256))) # dB * 2^n where n is 8 (Q8)
|
||||
return self._set_gain(dB_Q8)
|
||||
|
||||
def set_volume(self, mult):
|
||||
"""Sets the output volume as a float percent, i.e. 0.5 for 50%, 1.75 for 175%, etc."""
|
||||
return self.set_gain(20 * math.log10(mult)) # amplitude ratio
|
||||
|
||||
def _get_last_packet_duration(self):
|
||||
"""Gets the duration (in samples) of the last packet successfully decoded or concealed."""
|
||||
|
||||
ret = ctypes.c_int32()
|
||||
_lib.opus_decoder_ctl(self._state, CTL_LAST_PACKET_DURATION, ctypes.byref(ret))
|
||||
return ret.value
|
||||
|
||||
def decode(self, data, *, fec=False):
|
||||
if data is None and fec:
|
||||
raise OpusError("Invalid arguments: FEC cannot be used with null data")
|
||||
|
||||
if data is None:
|
||||
frame_size = self._get_last_packet_duration() or self.SAMPLES_PER_FRAME
|
||||
channel_count = self.CHANNELS
|
||||
else:
|
||||
frames = self.packet_get_nb_frames(data)
|
||||
channel_count = self.packet_get_nb_channels(data)
|
||||
samples_per_frame = self.packet_get_samples_per_frame(data)
|
||||
frame_size = frames * samples_per_frame
|
||||
|
||||
pcm = (ctypes.c_int16 * (frame_size * channel_count))()
|
||||
pcm_ptr = ctypes.cast(pcm, c_int16_ptr)
|
||||
|
||||
ret = _lib.opus_decode(self._state, data, len(data) if data else 0, pcm_ptr, frame_size, fec)
|
||||
|
||||
return array.array('h', pcm[:ret * channel_count]).tobytes()
|
||||
|
Loading…
x
Reference in New Issue
Block a user