More cleanup, merged pymumble into the project so I don't have to maintain 2 separate things, tested working version.
This commit is contained in:
215
pymumble_py3/soundoutput.py
Normal file
215
pymumble_py3/soundoutput.py
Normal file
@@ -0,0 +1,215 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from time import time
|
||||
import struct
|
||||
import threading
|
||||
import socket
|
||||
import opuslib
|
||||
|
||||
from .constants import *
|
||||
from .errors import CodecNotSupportedError
|
||||
from .tools import VarInt
|
||||
from .messages import VoiceTarget
|
||||
|
||||
|
||||
class SoundOutput:
|
||||
"""
|
||||
Class managing the sounds that must be sent to the server (best sent in a multiple of audio_per_packet samples)
|
||||
The buffering is the responsibility of the caller, any partial sound will be sent without delay
|
||||
"""
|
||||
|
||||
def __init__(self, mumble_object, audio_per_packet, bandwidth, stereo=False, opus_profile=PYMUMBLE_AUDIO_TYPE_OPUS_PROFILE):
|
||||
"""
|
||||
audio_per_packet=packet audio duration in sec
|
||||
bandwidth=maximum total outgoing bandwidth
|
||||
"""
|
||||
self.mumble_object = mumble_object
|
||||
|
||||
self.Log = self.mumble_object.Log
|
||||
|
||||
self.pcm = []
|
||||
self.lock = threading.Lock()
|
||||
|
||||
self.codec = None # codec currently requested by the server
|
||||
self.encoder = None # codec instance currently used to encode
|
||||
self.encoder_framesize = None # size of an audio frame for the current codec (OPUS=audio_per_packet, CELT=0.01s)
|
||||
self.opus_profile = opus_profile
|
||||
self.channels = 1 if not stereo else 2
|
||||
|
||||
self.set_audio_per_packet(audio_per_packet)
|
||||
self.set_bandwidth(bandwidth)
|
||||
|
||||
self.codec_type = None # codec type number to be used in audio packets
|
||||
self.target = 0 # target is not implemented yet, so always 0
|
||||
|
||||
self.sequence_start_time = 0 # time of sequence 1
|
||||
self.sequence_last_time = 0 # time of the last emitted packet
|
||||
self.sequence = 0 # current sequence
|
||||
|
||||
def send_audio(self):
|
||||
"""send the available audio to the server, taking care of the timing"""
|
||||
if not self.encoder or len(self.pcm) == 0: # no codec configured or no audio sent
|
||||
return ()
|
||||
|
||||
samples = int(self.encoder_framesize * PYMUMBLE_SAMPLERATE * 2 * self.channels) # number of samples in an encoder frame
|
||||
|
||||
while len(self.pcm) > 0 and self.sequence_last_time + self.audio_per_packet <= time(): # audio to send and time to send it (since last packet)
|
||||
current_time = time()
|
||||
if self.sequence_last_time + PYMUMBLE_SEQUENCE_RESET_INTERVAL <= current_time: # waited enough, resetting sequence to 0
|
||||
self.sequence = 0
|
||||
self.sequence_start_time = current_time
|
||||
self.sequence_last_time = current_time
|
||||
elif self.sequence_last_time + (self.audio_per_packet * 2) <= current_time: # give some slack (2*audio_per_frame) before interrupting a continuous sequence
|
||||
# calculating sequence after a pause
|
||||
self.sequence = int((current_time - self.sequence_start_time) / PYMUMBLE_SEQUENCE_DURATION)
|
||||
self.sequence_last_time = self.sequence_start_time + (self.sequence * PYMUMBLE_SEQUENCE_DURATION)
|
||||
else: # continuous sound
|
||||
self.sequence += int(self.audio_per_packet / PYMUMBLE_SEQUENCE_DURATION)
|
||||
self.sequence_last_time = self.sequence_start_time + (self.sequence * PYMUMBLE_SEQUENCE_DURATION)
|
||||
|
||||
payload = bytearray() # content of the whole packet, without tcptunnel header
|
||||
audio_encoded = 0 # audio time already in the packet
|
||||
|
||||
while len(self.pcm) > 0 and audio_encoded < self.audio_per_packet: # more audio to be sent and packet not full
|
||||
self.lock.acquire()
|
||||
to_encode = self.pcm.pop(0)
|
||||
self.lock.release()
|
||||
|
||||
if len(to_encode) != samples: # pad to_encode if needed to match sample length
|
||||
to_encode += b'\x00' * (samples - len(to_encode))
|
||||
|
||||
try:
|
||||
encoded = self.encoder.encode(to_encode, len(to_encode) // (2 * self.channels))
|
||||
except opuslib.exceptions.OpusError:
|
||||
encoded = b''
|
||||
|
||||
audio_encoded += self.encoder_framesize
|
||||
|
||||
# create the audio frame header
|
||||
if self.codec_type == PYMUMBLE_AUDIO_TYPE_OPUS:
|
||||
frameheader = VarInt(len(encoded)).encode()
|
||||
else:
|
||||
frameheader = len(encoded)
|
||||
if audio_encoded < self.audio_per_packet and len(self.pcm) > 0: # if not last frame for the packet, set the terminator bit
|
||||
frameheader += (1 << 7)
|
||||
frameheader = struct.pack('!B', frameheader)
|
||||
|
||||
payload += frameheader + encoded # add the frame to the packet
|
||||
|
||||
header = self.codec_type << 5 # encapsulate in audio packet
|
||||
sequence = VarInt(self.sequence).encode()
|
||||
|
||||
udppacket = struct.pack('!B', header | self.target) + sequence + payload
|
||||
if self.mumble_object.positional:
|
||||
udppacket += struct.pack("fff", self.mumble_object.positional[0], self.mumble_object.positional[1], self.mumble_object.positional[2])
|
||||
|
||||
self.Log.debug("audio packet to send: sequence:{sequence}, type:{type}, length:{len}".format(
|
||||
sequence=self.sequence,
|
||||
type=self.codec_type,
|
||||
len=len(udppacket)
|
||||
))
|
||||
|
||||
tcppacket = struct.pack("!HL", PYMUMBLE_MSG_TYPES_UDPTUNNEL, len(udppacket)) + udppacket # encapsulate in tcp tunnel
|
||||
|
||||
while len(tcppacket) > 0:
|
||||
sent = self.mumble_object.control_socket.send(tcppacket)
|
||||
if sent < 0:
|
||||
raise socket.error("Server socket error")
|
||||
tcppacket = tcppacket[sent:]
|
||||
|
||||
def get_audio_per_packet(self):
|
||||
"""return the configured length of a audio packet (in ms)"""
|
||||
return self.audio_per_packet
|
||||
|
||||
def set_audio_per_packet(self, audio_per_packet):
|
||||
"""set the length of an audio packet (in ms)"""
|
||||
self.audio_per_packet = audio_per_packet
|
||||
self.create_encoder()
|
||||
|
||||
def get_bandwidth(self):
|
||||
"""get the configured bandwidth for the audio output"""
|
||||
return self.bandwidth
|
||||
|
||||
def set_bandwidth(self, bandwidth):
|
||||
"""set the bandwidth for the audio output"""
|
||||
self.bandwidth = bandwidth
|
||||
self._set_bandwidth()
|
||||
|
||||
def _set_bandwidth(self):
|
||||
"""do the calculation of the overhead and configure the actual bitrate for the codec"""
|
||||
if self.encoder:
|
||||
overhead_per_packet = 20 # IP header in bytes
|
||||
overhead_per_packet += (3 * int(self.audio_per_packet / self.encoder_framesize)) # overhead per frame
|
||||
if self.mumble_object.udp_active:
|
||||
overhead_per_packet += 12 # UDP header
|
||||
else:
|
||||
overhead_per_packet += 20 # TCP header
|
||||
overhead_per_packet += 6 # TCPTunnel encapsulation
|
||||
|
||||
overhead_per_second = int(overhead_per_packet * 8 / self.audio_per_packet) # in bits
|
||||
|
||||
self.Log.debug(
|
||||
"Bandwidth is {bandwidth}, downgrading to {bitrate} due to the protocol overhead".format(bandwidth=self.bandwidth, bitrate=self.bandwidth - overhead_per_second))
|
||||
|
||||
self.encoder.bitrate = self.bandwidth - overhead_per_second
|
||||
|
||||
def add_sound(self, pcm):
|
||||
"""add sound to be sent (in PCM 16 bits signed format)"""
|
||||
if len(pcm) % 2 != 0: # check that the data is align on 16 bits
|
||||
raise Exception("pcm data must be 16 bits")
|
||||
|
||||
samples = int(self.encoder_framesize * PYMUMBLE_SAMPLERATE * 2 * self.channels) # number of samples in an encoder frame
|
||||
|
||||
self.lock.acquire()
|
||||
if len(self.pcm) and len(self.pcm[-1]) < samples:
|
||||
initial_offset = samples - len(self.pcm[-1])
|
||||
self.pcm[-1] += pcm[:initial_offset]
|
||||
else:
|
||||
initial_offset = 0
|
||||
for i in range(initial_offset, len(pcm), samples):
|
||||
self.pcm.append(pcm[i:i + samples])
|
||||
self.lock.release()
|
||||
|
||||
def clear_buffer(self):
|
||||
self.lock.acquire()
|
||||
self.pcm = []
|
||||
self.lock.release()
|
||||
|
||||
def get_buffer_size(self):
|
||||
"""return the size of the unsent buffer in sec"""
|
||||
return sum(len(chunk) for chunk in self.pcm) / 2. / PYMUMBLE_SAMPLERATE / self.channels
|
||||
|
||||
def set_default_codec(self, codecversion):
|
||||
"""Set the default codec to be used to send packets"""
|
||||
self.codec = codecversion
|
||||
self.create_encoder()
|
||||
|
||||
def create_encoder(self):
|
||||
"""create the encoder instance, and set related constants"""
|
||||
if not self.codec:
|
||||
return ()
|
||||
|
||||
if self.codec.opus:
|
||||
self.encoder = opuslib.Encoder(PYMUMBLE_SAMPLERATE, self.channels, self.opus_profile)
|
||||
self.encoder_framesize = self.audio_per_packet
|
||||
self.codec_type = PYMUMBLE_AUDIO_TYPE_OPUS
|
||||
else:
|
||||
raise CodecNotSupportedError('')
|
||||
|
||||
self._set_bandwidth()
|
||||
|
||||
def set_whisper(self, target_id, channel=False):
|
||||
if not target_id:
|
||||
return
|
||||
if type(target_id) is int:
|
||||
target_id = [target_id]
|
||||
self.target = 2
|
||||
if channel:
|
||||
self.target = 1
|
||||
cmd = VoiceTarget(self.target, target_id)
|
||||
self.mumble_object.execute_command(cmd)
|
||||
|
||||
def remove_whisper(self):
|
||||
self.target = 0
|
||||
cmd = VoiceTarget(self.target, [])
|
||||
self.mumble_object.execute_command(cmd)
|
Reference in New Issue
Block a user