def test_decode(self): decoder = Decoder(48000, 2) packet = chr((63 << 2) + 3) + chr(49) for j in range(2, 51): packet += chr(0) try: decoder.decode(packet, frame_size=960) except OpusError as e: self.assertEqual(e.code, constants.INVALID_PACKET) packet = chr(63 << 2) + chr(0) + chr(0) try: decoder.decode(packet, frame_size=60) except OpusError as e: self.assertEqual(e.code, constants.BUFFER_TOO_SMALL) try: decoder.decode(packet, frame_size=480) except OpusError as e: self.assertEqual(e.code, constants.BUFFER_TOO_SMALL) try: decoder.decode(packet, frame_size=960) except OpusError: self.fail('Decode failed')
def __init__(self, main_client, channel): """ :param main_client: The :class:`~.curious.core.client.Client` object associated with this VoiceClient. :param channel: The :class:`~.Channel` associated with this VoiceClient. """ #: The main client this voice client is associated with. self.client = main_client #: The voice channel this client is connected to. self.channel = channel #: The voice websocket that we are connected to. self.vs_ws = None # type: VoiceGateway #: The UDP socket that we are connected to. self._sock = None # type: socket.socket self.main_task = None # type: curio.Task # Header related stuff self.sequence = 0 # sequence is 2 bytes self.timestamp = 0 # timestamp is 4 bytes # Opus encoder/decoder # Do not use self.encoder = Encoder(48000, 2, 'audio') self.encoder.bitrate = 96000 self.decoder = Decoder(48000, 2)
def test_create(self): try: Decoder(1000, 3) except OpusError as e: self.assertEqual(e.code, constants.BAD_ARG) Decoder(48000, 2)
def test_decode_float(self): decoder = Decoder(48000, 2) packet = chr(63 << 2) + chr(0) + chr(0) try: decoder.decode_float(packet, frame_size=960) except OpusError: self.fail('Decode failed')
def test_gain(self): decoder = Decoder(48000, 2) self.assertEqual(decoder.gain, 0) try: decoder.gain = -32769 except OpusError as e: self.assertEqual(e.code, constants.BAD_ARG) try: decoder.gain = 32768 except OpusError as e: self.assertEqual(e.code, constants.BAD_ARG) decoder.gain = -15 self.assertEqual(decoder.gain, -15)
def get_encoder(encoding: str, rate: int): if encoding == "LINEAR16": return lambda audio_chunk: list(map(lambda x: int(x), audio_chunk)) elif encoding == "RAW_OPUS": from opuslib import Decoder decode = Decoder(rate, channels=1).decode return lambda audio_chunk: list(map(lambda x: int(x), decode(audio_chunk, frame_size=int(0.12 * rate)))) else: raise NotImplemented("Another encoding is not supported")
def __init__(self, bot): self.voice_states = {} self.tokenizer = gtts_token.Token() self.servers_recording = set() self.recording_data = {} if Decoder: self.opus_decoder = Decoder(48000, 2) else: self.opus_decoder = None super().__init__(bot) self.logger = self.logger.getChild('voice') self.disconnect_task = self.loop.create_task(self.disconnect_bg_task())
def test_get_pitch(self): decoder = Decoder(48000, 2) self.assertIn(decoder.pitch, (-1, 0)) packet = chr(63 << 2) + chr(0) + chr(0) decoder.decode(packet, frame_size=960) self.assertIn(decoder.pitch, (-1, 0)) packet = chr(1) + chr(0) + chr(0) decoder.decode(packet, frame_size=960) self.assertIn(decoder.pitch, (-1, 0))
def test_reset_state(self): decoder = Decoder(48000, 2) decoder.reset_state()
def test_get_bandwidth(self): decoder = Decoder(48000, 2) self.assertEqual(decoder.bandwidth, 0)
import asyncio import zlib import queue import threading import audioop from collections import namedtuple from google.cloud import speech from opuslib import Decoder from config.config import Config from config.config import Server from config.config import Opus dec = Decoder(Opus.rate, Opus.channels) class VoiceTranscription(): def __init__(self): self.buffer = queue.Queue() self.buffer_response = queue.Queue() self.activate_job() async def new_client(self): client = EchoServerProtocol() self._client.add(client) def put_buffer(self, data): self.buffer.put(data) def get_buffer(self): if self.buffer_response.empty():
import io import sys import os import time from opuslib import Encoder, Decoder file_name = os.path.join(os.path.dirname(__file__), '../resources', sys.argv[1]) rate = 48000 channels = 2 chunk = 960 enc = Encoder(rate, channels, 'audio') dec = Decoder(rate, channels) wf = wave.open(file_name, 'rb') p = pyaudio.PyAudio() def callback(in_data, frame_count, time_info, status): data = wf.readframes(frame_count) opus_audio = enc.encode(data, 960) print(len(data)) print(len(opus_audio)) decoded = dec.decode(opus_audio, 960) return (decoded, pyaudio.paContinue)
class VoiceClient(object): """ The voice client instance controls connecting to Discord's voice servers. This should ***not*** be created directly - instead use :class:`~.Channel.connect()` to connect to a voice channel, and use the instance returned from. """ def __new__(cls, *args, **kwargs): if not has_opus or not has_nacl: raise RuntimeError( "Cannot make new VoiceClients without libopus installed") return super().__new__(*args, **kwargs) def __init__(self, main_client, channel): """ :param main_client: The :class:`~.curious.core.client.Client` object associated with this VoiceClient. :param channel: The :class:`~.Channel` associated with this VoiceClient. """ #: The main client this voice client is associated with. self.client = main_client #: The voice channel this client is connected to. self.channel = channel #: The voice websocket that we are connected to. self.vs_ws = None # type: VoiceGateway #: The UDP socket that we are connected to. self._sock = None # type: socket.socket self.main_task = None # type: curio.Task # Header related stuff self.sequence = 0 # sequence is 2 bytes self.timestamp = 0 # timestamp is 4 bytes # Opus encoder/decoder # Do not use self.encoder = Encoder(48000, 2, 'audio') self.encoder.bitrate = 96000 self.decoder = Decoder(48000, 2) @property def open(self): return self.vs_ws._open # Voice encoder related things. def get_packet_header(self) -> bytes: """ Gets the voice packet header. :return: The bytes of the header. """ header = bytearray(12) # constant values, provided by the docs header[0:2] = b"\x80\x78" # dynamic values # offset 2 -> sequence struct.pack_into(">H", header, 2, self.sequence) # offset 4 -> timestamp struct.pack_into(">I", header, 4, self.timestamp) # offset 8 -> ssrc struct.pack_into(">I", header, 8, self.vs_ws.ssrc) return header def get_voice_packet(self, opus_body: bytes) -> bytes: """ Gets the voice packet to send to Discord, after encryption. :param opus_body: The body of the packet to encrypt. :return: The bytes of the packet. """ header = self.get_packet_header() nonce = bytearray(24) # copy the header into nonce nonce[:12] = header encryptor = SecretBox(bytes(self.vs_ws.secret_key)) encrypted_body = encryptor.encrypt(opus_body, nonce=bytes(nonce)) # pack_nonce is True, so the body can just be concatted return bytes(header + encrypted_body.ciphertext) def get_ip_discovery_packet(self) -> bytes: """ Gets the IP discovery packet to send to Discord. """ packet = bytearray(70) packet[0:4] = struct.pack(">I", self.vs_ws.ssrc) return packet def unpack_packet(self, data: bytes) -> Tuple[int, int, int, bytes]: """ Unpacks a voice packet received from Discord. :param data: The data to unpack. :return: A tuple of (ssrc, sequence, timestamp, data). """ header = data[:12] encrypted_data = data[12:] # unpack header data type_ = header[0] version = header[1] sequence = struct.unpack(">H", header[2:4])[0] timestamp = struct.unpack(">I", header[4:8])[0] ssrc = struct.unpack(">I", header[8:12])[0] # okay, for some reason discord sends malformed packets # 0x90 as type means we need to chop off the first 8 bytes from the decrypted data # because it's invalid opus # first, decrypt the data nonce = bytearray(24) nonce[:12] = header encryptor = SecretBox(bytes(self.vs_ws.secret_key)) decrypted = encryptor.decrypt(encrypted_data, nonce=bytes(nonce)) if type_ == 0x90: decrypted = decrypted[8:] pcm_frames = self.decoder.decode(decrypted, 960) return ssrc, sequence, timestamp, pcm_frames def _send_voice_packet(self, built_packet: bytes): """ Sends a voice packet. :param built_packet: The final built packet, as got by :meth:`.get_voice_packet`. """ # Overflow values as appropriate. sequence = self.sequence + 1 self.sequence = simulate_overflow(sequence, 2 * 8, False) timestamp = self.timestamp + (20 * 48) self.timestamp = simulate_overflow(timestamp, 2 * 16, False) try: self._sock.sendto(built_packet, (self.vs_ws.endpoint, self.vs_ws.port)) except BlockingIOError: # can't send rn, oh well # opus is built for this logger.error( "Failed to send voice packet! Sequence: {}, timestamp: {}". format(self.sequence, self.timestamp)) def send_opus_data(self, opus_data: bytes): """ Sends an opus-encoded voice packet. :param opus_data: The data to send. """ packet = self.get_voice_packet(opus_data) self._send_voice_packet(packet) def send_voice_packet(self, voice_data: bytes): """ Sends voice data from a PCM buffer. :param voice_data: The raw PCM voice data to send. """ data = self.encoder.encode(voice_data, 960) self.send_opus_data(data) def play_path(self, path: str): """ Plays a file using ffmpeg. """ player = vp.VoicePlayer(self, path) player.start() async def poll(self): """ Polls the voice websocket constantly for new events. """ while True: await self.vs_ws.next_event() async def close(self): """ Closes the websocket. """ await self.vs_ws._close() await self.main_task.cancel() async def connect(self, timeout: int = 10) -> None: """ Connects the voice client to the UDP server. :param timeout: The timeout before the connection is closed. """ logger.info("Opening UDP connection to Discord voice servers.") # Wait before the voice socket is ready. await self.vs_ws._ready.wait() # Open our UDP connection. # TODO: Make this IPv6 compatible, if appropriate. # Now, you may be wondering why we use raw UDP sockets instead of using "nice" sockets. # This is because we need a voice thread to be able to access it. # This prevents blocking actions from killing the entire voice connection. addrinfo = await getaddrinfo(self.vs_ws.endpoint, self.vs_ws.port, 0, socket.SOCK_DGRAM) for item in addrinfo: try: new_socket = socket.socket(item[0], item[1]) break except: logger.debug("Failed to make socket", exc_info=True) else: raise ConnectionError("Could not create voice socket") self._sock = new_socket # Send an IP discovery packet. logger.info("Connecting to {}:{}".format(self.vs_ws.endpoint, self.vs_ws.port)) packet = self.get_ip_discovery_packet() logger.debug("Sending IP discovery packet") self._sock.sendto(packet, (self.vs_ws.endpoint, self.vs_ws.port)) # Wait for our local IP to be received. try: packet_data, addr = await curio.timeout_after( timeout, curio.abide(self._sock.recvfrom, 70)) except curio.TaskTimeout: self._sock.close() self.vs_ws._close() raise logger.debug("Got IP discovery packet!") # IP is encoded in ASCII, from the forth byte to the first \x00 byte. # Find the index of the null byte, starting from 4th. ip_start = 4 ip_end = packet_data.index(0, ip_start) our_ip = packet_data[ip_start:ip_end].decode('ascii') # Also, we need our local port. # Unpack the little-endian (thanks discord!) port from the very end of the voice packet. our_port = struct.unpack_from('<H', packet_data, len(packet_data) - 2)[0] # Ask the voice websocket to send our SESSION DESCRIPTION packet. await self.vs_ws.send_select_protocol(our_ip, our_port) await self.vs_ws._got_secret_key.wait() logger.info("Established connection to Discord's voice servers.") self._sock.setblocking(False) # We are DONW! @classmethod async def create(cls, main_client, gateway, channel) -> 'VoiceClient': """ Creates a new VoiceClient from a channel and a gateway instance. :param main_client: The main :class:`.Client` to use. :param gateway: The gateway instance to use. :param channel: The :class:`~.Channel` to connect to. """ vs_ws = await VoiceGateway.from_gateway(gw=gateway, channel_id=channel.id, guild_id=channel.guild.id) obb = cls(main_client, channel=channel) obb.vs_ws = vs_ws obb.main_task = await curio.spawn(obb.poll()) return obb