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:
142
pymumble_py3/soundqueue.py
Normal file
142
pymumble_py3/soundqueue.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# -*- 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
|
Reference in New Issue
Block a user