Skip to content

Commit 0aff18f

Browse files
committed
add multi-speaker vc support
1 parent bdd4d37 commit 0aff18f

5 files changed

Lines changed: 226 additions & 147 deletions

File tree

cordslite/_modidx.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@
7676
'cordslite.core.VoiceClient.__init__': ('core.html#voiceclient.__init__', 'cordslite/core.py'),
7777
'cordslite.core.VoiceClient.__repr__': ('core.html#voiceclient.__repr__', 'cordslite/core.py'),
7878
'cordslite.core.VoiceClient._connect': ('core.html#voiceclient._connect', 'cordslite/core.py'),
79+
'cordslite.core.VoiceClient._get_proc': ('core.html#voiceclient._get_proc', 'cordslite/core.py'),
7980
'cordslite.core.VoiceClient._recv_audio': ('core.html#voiceclient._recv_audio', 'cordslite/core.py'),
8081
'cordslite.core.VoiceClient._udp': ('core.html#voiceclient._udp', 'cordslite/core.py'),
8182
'cordslite.core.VoiceClient._vhb': ('core.html#voiceclient._vhb', 'cordslite/core.py'),
83+
'cordslite.core.VoiceClient._write_audio': ('core.html#voiceclient._write_audio', 'cordslite/core.py'),
8284
'cordslite.core.VoiceClient.join': ('core.html#voiceclient.join', 'cordslite/core.py'),
8385
'cordslite.core.VoiceClient.leave': ('core.html#voiceclient.leave', 'cordslite/core.py'),
8486
'cordslite.core.VoiceClient.start_recording': ( 'core.html#voiceclient.start_recording',
@@ -89,5 +91,6 @@
8991
'cordslite.core.VoiceUDP.datagram_received': ('core.html#voiceudp.datagram_received', 'cordslite/core.py'),
9092
'cordslite.core.decrypt_pkt': ('core.html#decrypt_pkt', 'cordslite/core.py'),
9193
'cordslite.core.get_ip': ('core.html#get_ip', 'cordslite/core.py'),
94+
'cordslite.core.silence': ('core.html#silence', 'cordslite/core.py'),
9295
'cordslite.core.websockets.asyncio.client.ClientConnection.send': ( 'core.html#websockets.asyncio.client.clientconnection.send',
9396
'cordslite/core.py')}}}

cordslite/core.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.
44

55
# %% auto #0
6-
__all__ = ['evt_typs', 'sr', 'DiscordClient', 'DiscordObject', 'DiscordError', 'Guild', 'Channel', 'Channels', 'Message',
6+
__all__ = ['evt_typs', 'spf', 'sr', 'DiscordClient', 'DiscordObject', 'DiscordError', 'Guild', 'Channel', 'Channels', 'Message',
77
'Messages', 'User', 'Member', 'Members', 'GatewayClient', 'Op', 'Event', 'VoiceClient', 'get_ip', 'VoiceUDP',
8-
'decrypt_pkt', 'Bot']
8+
'decrypt_pkt', 'silence', 'Bot']
99

1010
# %% ../nbs/00_core.ipynb #9868997a
1111
from fastcore.utils import *
@@ -176,6 +176,7 @@ async def send(self:websockets.asyncio.client.ClientConnection, msg, **kw):
176176
# %% ../nbs/00_core.ipynb #130c739c
177177
@patch
178178
async def _connect(self:GatewayClient):
179+
if self.ws: await self.ws.close()
179180
self.ws = await websockets.connect(self.url)
180181
hello = json.loads(await self.ws.recv())
181182
self.hb_int = hello['d']['heartbeat_interval']
@@ -327,26 +328,50 @@ def decrypt_pkt(pkt, secret):
327328
ext_sz = int.from_bytes(pkt[14:16], 'big') * 4 # extension data size in bytes
328329
return decrypted[ext_sz:]
329330

330-
# %% ../nbs/00_core.ipynb #96f699af
331+
# %% ../nbs/00_core.ipynb #c06edb1d
332+
spf = 960
331333
sr = 48_000
334+
def silence(n_smpls:int): return b'\x00' * (n_smpls * 2) # 2 bytes per sample (s16le)
335+
336+
@patch
337+
def _get_proc(self:VoiceClient, ssrc):
338+
if ssrc not in self._procs:
339+
pth = str(self._rec_path.with_stem(f"{self._rec_path.stem}_{ssrc}"))
340+
proc = (ffmpeg.input('pipe:', f='s16le', ar=sr, ac=1)
341+
.output(pth).overwrite_output()
342+
.run_async(pipe_stdin=True, quiet=True))
343+
proc.stdin.write(silence(int((time.time() - self._rec_start) * sr)))
344+
self._procs[ssrc] = proc
345+
return self._procs[ssrc]
346+
347+
@patch
348+
def _write_audio(self:VoiceClient, ssrc, ts, data):
349+
proc = self._get_proc(ssrc)
350+
# Fill silence gaps
351+
if ssrc in self._last_ts:
352+
gap = ts - self._last_ts[ssrc] - spf
353+
if gap > 0: proc.stdin.write(silence(gap))
354+
self._last_ts[ssrc] = ts
355+
proc.stdin.write(self._decoders[ssrc].decode(data, spf))
332356

333357
@patch
334358
async def _recv_audio(self:VoiceClient):
335359
while self._recording:
336360
try:
337361
pkt = await asyncio.wait_for(self.proto.packets.get(), timeout=0.1)
338-
if pkt[1] != 0x78: continue
339-
opus_data = decrypt_pkt(pkt, self.secret)
340-
if opus_data: self._proc.stdin.write(self._decoder.decode(opus_data, 960))
362+
if pkt[1] != 0x78: continue # filter out RTP non-voice packets
363+
ssrc = int.from_bytes(pkt[8:12], 'big')
364+
ts = int.from_bytes(pkt[4:8], 'big')
365+
if ssrc not in self._decoders: self._decoders[ssrc] = opuslib_next.Decoder(sr, 1)
366+
if data := decrypt_pkt(pkt, self.secret): self._write_audio(ssrc, ts, data)
341367
except asyncio.TimeoutError: continue
342368

369+
# %% ../nbs/00_core.ipynb #b17c0541
343370
@patch
344371
def start_recording(self:VoiceClient, path='recording.mp3'):
345-
self._rec_path = path
346-
self._decoder = opuslib_next.Decoder(sr, 1)
347-
self._proc = (ffmpeg.input('pipe:', f='s16le', ar=sr, ac=1)
348-
.output(path).overwrite_output()
349-
.run_async(pipe_stdin=True, quiet=True))
372+
while not self.proto.packets.empty(): self.proto.packets.get_nowait()
373+
self._rec_path,self._decoders,self._procs,self._last_ts = Path(path),{},{},{}
374+
self._rec_start = time.time()
350375
self._recording = True
351376
self._rec_task = asyncio.create_task(self._recv_audio())
352377
return path
@@ -355,8 +380,8 @@ def start_recording(self:VoiceClient, path='recording.mp3'):
355380
def stop_recording(self:VoiceClient):
356381
self._recording = False
357382
self._rec_task.cancel()
358-
self._proc.communicate()
359-
return self._rec_path
383+
for p in self._procs.values(): p.communicate()
384+
return [str(self._rec_path.with_stem(f"{self._rec_path.stem}_{ssrc}")) for ssrc in self._procs]
360385

361386
# %% ../nbs/00_core.ipynb #a79084f1
362387
@patch
@@ -387,7 +412,9 @@ async def _on_msg(self, msg):
387412
if not parts or not parts[0].startswith('!'): return
388413
name = parts[0][1:]
389414
if name not in self.cmds: return
390-
try: await self.cmds[name](msg, parts[1] if len(parts) > 1 else '')
415+
try:
416+
res = self.cmds[name](msg, parts[1] if len(parts) > 1 else '')
417+
if asyncio.iscoroutine(res): await res
391418
except Exception as e:
392419
self.errors.append(e)
393420
if self._on_err: await self._on_err(msg, e)

0 commit comments

Comments
 (0)