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
1111from 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
178178async 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
331333sr = 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
334358async 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
344371def 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'):
355380def 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