mirror of
https://github.com/azlux/pymumble
synced 2024-11-23 13:56:26 +00:00
501149284b
* Added the ability to set a position
216 lines
9.1 KiB
Python
216 lines
9.1 KiB
Python
# -*- 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)
|