pymumble/pymumble_py3/soundqueue.py

143 lines
5.0 KiB
Python

# -*- coding: utf-8 -*-
import time
from threading import Lock
from collections import deque
import opuslib
from .constants import *
class SoundQueue:
"""
Per user storage of received audio frames
Takes care of the decoding of the received audio
"""
def __init__(self, mumble_object):
self.mumble_object = mumble_object
self.queue = deque()
self.start_sequence = None
self.start_time = None
self.receive_sound = True
self.lock = Lock()
# to be sure, create every supported decoders for all users
# sometime, clients still use a codec for a while after server request another...
self.decoders = {
PYMUMBLE_AUDIO_TYPE_OPUS: opuslib.Decoder(PYMUMBLE_SAMPLERATE, 1)
}
def set_receive_sound(self, value):
"""Define if received sounds must be kept or discarded in this specific queue (user)"""
if value:
self.receive_sound = True
else:
self.receive_sound = False
def add(self, audio, sequence, type, target):
"""Add a new audio frame to the queue, after decoding"""
if not self.receive_sound:
return None
self.lock.acquire()
try:
pcm = self.decoders[type].decode(audio, PYMUMBLE_READ_BUFFER_SIZE)
if not self.start_sequence or sequence <= self.start_sequence:
# New sequence started
self.start_time = time.time()
self.start_sequence = sequence
calculated_time = self.start_time
else:
# calculating position in current sequence
calculated_time = self.start_time + (sequence - self.start_sequence) * PYMUMBLE_SEQUENCE_DURATION
newsound = SoundChunk(pcm, sequence, len(pcm), calculated_time, type, target)
if not self.mumble_object.callbacks.get_callback(PYMUMBLE_CLBK_SOUNDRECEIVED):
self.queue.appendleft(newsound)
if len(self.queue) > 1 and self.queue[0].time < self.queue[1].time:
# sort the audio chunk if it came out of order
cpt = 0
while cpt < len(self.queue) - 1 and self.queue[cpt].time < self.queue[cpt+1].time:
tmp = self.queue[cpt+1]
self.queue[cpt+1] = self.queue[cpt]
self.queue[cpt] = tmp
self.lock.release()
return newsound
except KeyError:
self.lock.release()
self.mumble_object.Log.error("Codec not supported (audio packet type {0})".format(type))
except Exception as e:
self.lock.release()
self.mumble_object.Log.error("error while decoding audio. sequence:{seq}, type:{type}. {error}".format(seq=sequence, type=type, error=str(e)))
def is_sound(self):
"""Boolean to check if there is a sound frame in the queue"""
if len(self.queue) > 0:
return True
else:
return False
def get_sound(self, duration=None):
"""Return the first sound of the queue and discard it"""
self.lock.acquire()
if len(self.queue) > 0:
if duration is None or self.first_sound().duration <= duration:
result = self.queue.pop()
else:
result = self.first_sound().extract_sound(duration)
else:
result = None
self.lock.release()
return result
def first_sound(self):
"""Return the first sound of the queue, but keep it"""
if len(self.queue) > 0:
return self.queue[-1]
else:
return None
class SoundChunk:
"""
Object that contains the actual audio frame, in PCM format"""
def __init__(self, pcm, sequence, size, calculated_time, type, target, timestamp=time.time()):
self.timestamp = timestamp # measured time of arrival of the sound
self.time = calculated_time # calculated time of arrival of the sound (based on sequence)
self.pcm = pcm # audio data
self.sequence = sequence # sequence of the packet
self.size = size # size
self.duration = float(size) / 2 / PYMUMBLE_SAMPLERATE # duration in sec
self.type = type # type of the audio (codec)
self.target = target # target of the audio
def extract_sound(self, duration):
"""Extract part of the chunk, leaving a valid chunk for the remaining part"""
size = int(duration*2*PYMUMBLE_SAMPLERATE)
result = SoundChunk(
self.pcm[:size],
self.sequence,
size,
self.time,
self.type,
self.target,
self.timestamp
)
self.pcm = self.pcm[size:]
self.duration -= duration
self.time += duration
self.size -= size
return result