From af5af8133ff973c9c1ab7fce43d616c9c6098c42 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Wed, 27 May 2026 14:42:27 +0000 Subject: [PATCH] =?UTF-8?q?feat(voice):=20Pas=202=20=E2=80=94=20install=20?= =?UTF-8?q?voice=20deps,=20vendor=20discord-ext-voice-recv,=20setup=20asse?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation pentru Discord voice-to-voice pipeline. - requirements.txt: faster-whisper, silero-vad, num2words, numpy, PyNaCl - vendor/discord-ext-voice-recv/: vendored la commit ac04ea7b09 (bump version 0.5.3a) — Discord voice protocol fragil, upstream hobby fork. Adapter layer in src/voice/_discord_voice_adapter.py izolează churn (swap la py-cord = doar acel fișier rescris). VENDOR_INFO.md documentează update procedure. - tools/voice_setup.py: idempotent setup script — libopus check, ffmpeg check, Supertonic reachable, faster-whisper/silero-vad warm, assets generation. Exit 0 = green, 1 = needs human (currently libopus missing needs `sudo apt install -y libopus0`). - assets/voice/: thinking.wav (filler "Stai puțin să-mi adun gândurile", ~2.8s), mhm.wav (listener noise), beep_200ms.wav (wake-up tone 880Hz). - src/voice/__init__.py: package stub. Co-Authored-By: Claude Opus 4.7 (1M context) --- assets/voice/beep_200ms.wav | Bin 0 -> 38478 bytes assets/voice/mhm.wav | Bin 0 -> 110636 bytes assets/voice/thinking.wav | Bin 0 -> 245804 bytes requirements.txt | 11 + src/voice/__init__.py | 1 + tools/voice_setup.py | 273 ++++++++ vendor/discord-ext-voice-recv/.gitignore | 132 ++++ vendor/discord-ext-voice-recv/LICENSE | 21 + vendor/discord-ext-voice-recv/README.md | 230 +++++++ vendor/discord-ext-voice-recv/VENDOR_INFO.md | 22 + .../discord/ext/voice_recv/__init__.py | 20 + .../discord/ext/voice_recv/buffer.py | 249 +++++++ .../discord/ext/voice_recv/enums.py | 30 + .../discord/ext/voice_recv/extras/__init__.py | 2 + .../ext/voice_recv/extras/localplayback.py | 132 ++++ .../voice_recv/extras/speechrecognition.py | 237 +++++++ .../discord/ext/voice_recv/gateway.py | 122 ++++ .../discord/ext/voice_recv/opus.py | 174 +++++ .../discord/ext/voice_recv/reader.py | 422 ++++++++++++ .../discord/ext/voice_recv/router.py | 203 ++++++ .../discord/ext/voice_recv/rtp.py | 471 +++++++++++++ .../discord/ext/voice_recv/silence.py | 152 +++++ .../discord/ext/voice_recv/sinks.py | 634 ++++++++++++++++++ .../discord/ext/voice_recv/types.py | 59 ++ .../discord/ext/voice_recv/utils.py | 205 ++++++ .../discord/ext/voice_recv/video.py | 95 +++ .../discord/ext/voice_recv/voice_client.py | 196 ++++++ .../discord-ext-voice-recv/examples/recv.py | 47 ++ vendor/discord-ext-voice-recv/pyproject.toml | 27 + .../discord-ext-voice-recv/requirements.txt | 1 + vendor/discord-ext-voice-recv/setup.py | 70 ++ vendor/discord-ext-voice-recv/update_notes.md | 21 + 32 files changed, 4259 insertions(+) create mode 100644 assets/voice/beep_200ms.wav create mode 100644 assets/voice/mhm.wav create mode 100644 assets/voice/thinking.wav create mode 100644 src/voice/__init__.py create mode 100644 tools/voice_setup.py create mode 100644 vendor/discord-ext-voice-recv/.gitignore create mode 100644 vendor/discord-ext-voice-recv/LICENSE create mode 100644 vendor/discord-ext-voice-recv/README.md create mode 100644 vendor/discord-ext-voice-recv/VENDOR_INFO.md create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/__init__.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/buffer.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/enums.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/__init__.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/localplayback.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/speechrecognition.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/gateway.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/opus.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/reader.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/router.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/rtp.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/silence.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/sinks.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/types.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/utils.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/video.py create mode 100644 vendor/discord-ext-voice-recv/discord/ext/voice_recv/voice_client.py create mode 100644 vendor/discord-ext-voice-recv/examples/recv.py create mode 100644 vendor/discord-ext-voice-recv/pyproject.toml create mode 100644 vendor/discord-ext-voice-recv/requirements.txt create mode 100644 vendor/discord-ext-voice-recv/setup.py create mode 100644 vendor/discord-ext-voice-recv/update_notes.md diff --git a/assets/voice/beep_200ms.wav b/assets/voice/beep_200ms.wav new file mode 100644 index 0000000000000000000000000000000000000000..e359e51e86cbb514f74f15e57a156bbddc001fad GIT binary patch literal 38478 zcmeI*d%RXt{=o6I_fzS9QIcx9C`l@!JB1J_-KU0x)HKMwD2-9>*T%>oiJFKB)l5^l zgmO(nRE+2{MYU8WInT4#THp0um*+Wq_3hTRYuCp@ z7DUl9gmI{Ym(!Qi{@9)DP98&N>?rOiPQzd{!p|i?mwbeOU>cspbj-sF z>__ur^Wx>0h5hK6^h};d>9lk@981w6vH^wtD9Ch(w^kH-Zj==mdKlH^9=@04sXo{uD(&QS{!qVc>;%zts6|lZ! zeaV}69#0)Kvt(w;Vtj+D#j3?~a1WNFeo{ZV37?@=+A4hndoU;r3U8oZR4*Ef&(J7t z6kmNmnuU(&heK^Ps4%E-4w@o~lXxi}#M#&r?TMbjx!4xAg?rH$i_^vFV3Z^! z$y9X3uHvrZ^B9Vz__gHMlI56-zri;1v;Xh+{r6*w$7Vn8@A>;aj*sW#`uIMN&*$^{ z{9ea&cwJuK1MqsiZm-{RSRUKV@>x#HYq>4I^TbJzAgEtMzL= zTi@2Z^=~`a9=40^V>{VqQ(*hqj<%=mYWt4I2e93*hH+p#7#GHeabmm}H^z@~R41;J z#g*}8oEdM%o$+TJS~lbIJQ%0Ot8r`m8pmmrMlZtnHqMRrmN5S92lfa1h5f^RVt=^; z_8B{t4RK}cSPBH)n`@G`3;x#x4c9zfaGM<6unu^(2iXTv~Sg$w`lkqLuCT){HV>`O1 z-P74PJRBZw!k1_lwTmWV3);o);&E7nU(m47u+Rk;VCX>u3j+(M<2TqD?~Gr?&1i)U z(T3=LoPgzFdAJ(oFe9Cjo{5dg#^i3CfVIW7#YfN=M`2sZwvzWT3y0clM#+qlSMep{ zVq9#8QCNs_Nx5V&UO~;YW;zCIaC$gBJcCkEsb~mZL#4P;aj*sW#`uIMN&*$^{{9eb!@VdOd8{zeO-Cn=tuspVz<+GfY z*K%8a>%sc4URowCvwE_=tT*e=dbB>RSL@e$w!W=*>)&>;J!}`-$9A&K>}$54?Pz=2 zuD0)Zw1@5f2#f>c!MHF!j1%L(0ps6(V1KY**gxzi_Ls+D|FIw0pX^umFWb!iX1}xl*$>yn>#}}n z|FoalU+uS*U_Z7$+pq24_H+BY{oej>9xxx67t9al3G;<{<4~KKPs}Uk7xRqy#=K+x zF%OxK%u5~84rHk*srf3OU*+>F=P&vEDxY8F^Q(M*mCvv8`Bgr@%I8=4{#CwzmG58W z`&aq?Rla|f?_cHnSNZ-`{`z44`ry#)b;bPk!Tj|>U(?NBAIx7L%wHeOUmwh0AI#r> zmB0TgfB#kf{;T}`SNZ#|^7mim@4w35f0e)gDt~`_{{Hs-{q6bt+w=Fg=kIUN-`}3U zzde6{d;a%>`QH!be?OT2{b2t0gZbYN`Zrto-w)=0KbZghV19pw{QeC2{TcH6GvxPY z$nVdP-=87BKSO?hhW!2v`Tei*`(Nevzsm1_mEZp=zyDQ!|Ev7|SNZ*~^81VC_ZQ9Y zFPh(9G{3)Set*&Y{-XK)Mf3ZM=J#*U@86!^zdgTydw&1+{Qm9v{oC{Vx99h7&(Fun z&&SEn$H~vf$Zi~J|)3|ATJ^qQU@JzQ3 z=mXC~^Qa7Rj<^Z0qI6O^8Hm{^pO#Or#4EU6MDh- zw?Bjvu_{@W+=6CUS6o+|gp1%geU@z#%rovv6-yOQz%We5X0%RPC--1I zJacUlJU4G(7?_=9S2d~{T@J6QY+N?(g$Z~YKcZTpT6Wf8+d|tydz^qe@KCJJ;TdX| zqY2hTYoZ6x7Hh(qFdj$a-Spk`52%27$-LxJ9FAqhWyJ^3AIBhq=Q8~pzOTp6TTmz# zva$~-4k-Q^pP*(^GZ~5n@Eoek@g96WV`Vj3MXjQ{unM00I2e!Neb{cE5mpzTYv;LI zHBb_l#9v_!#=^GV8|{r|VK5HE>@Yj@$KG^rc1Bc}v`e}z*_NHFcTRFnQsl$eA3Qg* z7n-09ek%E?Wb;9O?e{GA`4|Ud@c_J{~hreZ6a z$IY`gm=aHkm%#oO3ZYO5RZ$Lm;yv-#Sb%%b8`ZEZS{99m=aGFCz6!VFI4n<>r#Hbf zww5K!l0V`^c!uB{+zijoIu2EkmZT;7V0>0AR?NzEZt>jWNO)eP=h%6l^y{z)ho^^U zZMK**BWs0P;W~ImV4bK=Gy)4yf{t;=ERRf!r^WBVGZxJi#@_)Lqn`8T*=Y~MbHj|q z@1pOr`so+-i(=Sj#=IXh)0t^Mq)D2cV`d!v2A0-o(>o!D1L!gCeB2w#Ld;F*!D)79x+I0>FR_b5C!vJzGlR}`Oy?a>>a zYv*~M4bd2_V0(F{#l!F{HP4&ukaWn-MEfB5AUQHUGHbJi>B6*1s6y5sn&ptvQR%2J zo`5-^R$MDP+tM@bo`z*G2bwS3hx0Z1`FH}J73tYpRj@Kz8T|$3+LBO`o!fX}xG)@s z`RV+0C~9CyvLqRYQ;`-^PH0_Pd=j23XpZOt%hv{-&=Z!+T(baM(I{z@<&T-k% zhuK(zYDu-E2PR-8>ZWzmE8w_kJr2MO>_msCL-Z%Sj$h%J?HJ{?dp_y2n01g}`#t}y zKaPXvp)G>><5cVpyTeRegvzjQkHoQX{F#gk(E#5Uzb`I^?RG1!gynOb^Gw+k`z{la^te>^xoWt$u_H-J4i%PI>+zaddNNg!?$;vjhI5j(mc64!c z@j6(hTksGp*T>j{rb*M}5=_B5G)fz#BVf#%ue!q=;+WbnY8YLHr{TEfwYG%${7T${ zyWqL4x8fQMKpS`l=K5%Tc8>D3(Y4Wu_&$6ep2Nkcfo05lZijP)Uz1;x`51#U;2Dda zA8BrQ4in)yyLZ4Cd<-w)L;Q$3Nu4bJcwXo`@GRSIu+7X>4MW3l1^xkJr%lu*J6G3p zp3Ni9M_S-KnAbfE*nDvvhQWT=7S-W6_$th=-B1pWC%54g>`Hf~FTpc)kH&^%L-H)_ zYsOUtShn}!_-5NV@3QYXmzoRbE8F4R<#ZT}FTt_DS=ubU2LHl-c($lHWGRftb6`K2 z2haR07njTWP?xw%)}Nf4T#VnL8%{<|INy99f5pXUgm1&QVLEKHqp&JnmCYS{q&?D# zSe`7;a{rKINOBU&!rpqV8x4T-KF?SE7Kg=$WoI^fHmSM2J1o;_FjkMiFVQd2Czy`wU@l5R5*A||I>CDM zeBv9?7UoCi4)@~%cm}QQW-jsk&LuFXEP`MAJ^#(}`?LPeeRu=Lw&UdWn2X(L8`@?$ zWI8e&VbC7Fxc;Q6YaA6prY0UKdi%!7XZ4}OMaKOVi|dcjyUu8&HO%5sxuV|zAh zwNNee!9!RE&%f=6tKgWu7{;V?i>h(eY|LsLH;#`+ZMbIH743??z`x+RwS#aRJhS>0 z+=9;Vg;UyvB}t^FPukKM^a3(JmES;ww3c==TokIy5j~s4P&}g zT8i25DcQW#Hv18-(aa&vi`SxhR6RNy&X1*ksBa5)}?>pWx8dH&h>Bg~DCosBso-W>7>KEQq$OZ_nxPr~c|7>)y$#WHTf z*Ko~Z9-V~I=ndz>d%~XZ9y~+c^Whs|N4g`Ohg)$TYGG@#HF*<{!?Kwdx}X)Bpdp+e z+P5sTb7;%uI@&(=IXJdGJ9mCqIxKwxpTK%_thpY}v7DPbH+GzL9%0#>Q_O+q8k_&j z74qx1;J^7Z{;a>_8rE3!T<7O83bt8!n2)F7YS=H#{nq(wuxvNOwYKvT&wX!)Hn2?Y zo0w-D{~Vv6gX_HAXplBYd&4u9tvlB-bwl0I8?H6xVIw?~*)iMmo^6MR@f_yEd}(?7 zY~Okw=26RK?r<$s4X%q`#eMi4j=|6A&*>sez+jvJ`~I3_O?FoJq-0XICb3O^2iwO! zW&HJlXL!5Lw7neTjbq2?>S=XWW}UP8GWXhM=Btz7TE=*E{Miokh}UQvm`m=1acTL? z?XCy@4*TJQu>2Rn{i@n<{BZ0y=U)SJVu*004&#A_y%Ruve~|oefM5CW`2j7p=OptMuw4L8qEFX>m#Ehv$EOW90SH- z0?ZSWVZU-6b_dKGw%2KBfMO_SaXl-{%H~$?gEWDAAxq(VZ?A)Wz8>}^`;srQ5Yyp( z{#?7+@2)>+bTT@*9TVZbAT5{kVDoEZIInRpVmjQHV5=c@kHWQ#^Sc+}n#%rfesllH z`TsDCg5`4`NbXOXKV8!~Z?Rm?sS5Z{_)mBZ6LC3CMOA#8ew(eIj7R%>CpgBs2kBYi z=I|HboM{5=Pj|xjdkD7aEI0>Vivw_7b~@Y_b3XhU9A8{ln?rsN_a5zQ_WgaZ-OLBB zkL`1=C!NRN0OwGilYR*-`zf$o?pN8z%!|f=Ip2P4n^}*}4;<6%>&8zzc$S%SEw9zJ zwz=H=;QoevPkwD%xju0HV|&@w{;rSVV~xk(;dr(Wj-l>hnI9cjt?RF0*<52CkIpzB z_B;36obwug?&COqnQx3k%Vj_P2ruAn3_(YDHtV)@Tlx;{=cCadC*uh0Pxfd1#yN#; zXBpi4HMjfO@;cA--`z*CT<$xxLLZER`>1ci*f1Bjg8NH<#M5{auGfsoDp8fJEl!S3 z&f?9rKzF!KISaOxb4~ZY%=M1-pJ6T>%dUm>W^VN3>-6h%0Vcw}W*%`a>;8;;TJAv^ zmmlL@SVsBvN3cIS-*gRaPHYKZ2XWu~0nCPR@hfVF+F90=OlXR3xDwuT&N17v+23pf zCwPu=<`iSj{$zRTp(b3{xfi%nQIX8B{+OoBQkM0o!LY9Jef!ZRKkdb>Mi|6_;Qv9*6PZeBc+jj&F^=a9{O7xW<12*1z+=-3U?0 z*2$Hl%0y@tN?IxFSFXbxCmiFK!*aRaw2rQZ`NJ{O=kfWzz^gF#ThHb+*NpBVG=+06 z=OzU>*4h6oj}`3LWemC|x1DV-$9&g(=0#)S1DG4F#|Ch1Xxum#GDkZ1cMf4LFfMHm zuYWIWANlo9Fc-QO`w+&V@ofD#M*5hJ)4qn_TyAT+HH$~r8us^bu-)7{cE8Yk;P_;0 zS_aoTwqrHe7u`EQ9@c~X%e9`l!#?&b%pa@a+S9z>3}?YL;!W_n%-R3KJ8(VXwc9S= z!E*Z9|M&a0mHS`*yuat~`#6`ND_X#QW-eY2UoV-3$1omO;Q}~MJ{6YlI9Nv8$9*aH z^6Xcxe|+7;e0Uiw*L|1@W6`9J^gVT0Z9|<~{da<=1}C zf3to4*(rDcqv4$Ge3(D#!9MMp@ZVUB7hu`k&vKnV1dao)OC5V{7t5jcu*_$|erY`z z$BrGA%W>qNSOVv8wwZa~G0i=}9+CV zFLQ@|%=yQ$a6EIYwm!|vZ@{t{llNc@%$1kI*G3%=j5pg+mZvW+gt_oqIRAVQmTL~o zP4;8QP0~>6b2>+GJhhIEN$0VD!u{}i9p}uMj=z>uer>KX{v6|tL0<>F0hhsj2=~T} zMaLHRq^-*hFlJtd^J8<#pJ6_6?&liEy*t~`eq@d?{~3GsNyovPVQzJ=$~d%Kt|Od( zn19VxKA-VuUvr-2-s**LzGpshy=|Uzuf#pqNq7Q&w#$=8^d*#Nd>`%s??c{4Z)`jhD-Y^a=m-Xdie-B;; zNnKyKPIFJhb~_i&&y7pRIAhZ>*D^YmyFV%8)i!eOV&62!+qYbg+4i=V`QEYY2v~>C z_l*_jB3{Q^aKGBPbWQ5D+79MA*CdufcfwwLoNW5W9L z!+5l>t%0%gK8#D(!d~YLSVm)XGAyg*@N4^%^Ni!l%QNAe&;DW=jZybZEUWz5?>XmmoHzeiCdV)Hq5aGDvX8ljF*lk+euDM7 z4$c>hOUt(ij^UQk_;L>D^;=#)8@G;wmdCY<`PDMnR^|;K&pI>zI=(QqU}$S$VJ`7noC}#Ry}qyT zB`mA$>1zRp`2Uu}IfnJI9_B&g&wBFld|bh;OG|6hl`@6Y(Nmcie%-}*TA uD<9Lxc0<|c@x$k|?Rn*Zb_}eU2LPaPQv52gRb{ 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 E-`&o$?*IS* literal 0 HcmV?d00001 diff --git a/assets/voice/thinking.wav b/assets/voice/thinking.wav new file mode 100644 index 0000000000000000000000000000000000000000..9f43da972e6e09983a2e6a355097e1e1d2e9c5ca GIT binary patch literal 245804 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�DiS~urx@lZ*;X9B!VIhym@%Szt|epVHh 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 F{|{Q!Pn-Y% literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index d41c02e..30685b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,14 @@ httpx>=0.27 pytest>=8.0 supertonic[serve]>=1.3.1 trafilatura>=1.8 + +# Voice pipeline (Pas 2 setup) +faster-whisper>=1.0 +silero-vad>=5.1 +num2words>=0.5 +numpy>=1.24 +PyNaCl>=1.5 +# discord-ext-voice-recv vendored at vendor/discord-ext-voice-recv/ +# pinned commit: ac04ea7b0941112e83767cf1c1469b408fa06748 +# install: pip install -e vendor/discord-ext-voice-recv +# System deps (NOT pip): libopus0 (apt), ffmpeg diff --git a/src/voice/__init__.py b/src/voice/__init__.py new file mode 100644 index 0000000..a440a10 --- /dev/null +++ b/src/voice/__init__.py @@ -0,0 +1 @@ +"""Discord voice pipeline modules — Pas 3-7 in voice plan.""" diff --git a/tools/voice_setup.py b/tools/voice_setup.py new file mode 100644 index 0000000..37f02c7 --- /dev/null +++ b/tools/voice_setup.py @@ -0,0 +1,273 @@ +""" +voice_setup.py — One-shot setup for Discord voice pipeline. + +Run after `pip install -r requirements.txt`. Idempotent. + +Steps: +1. Verify libopus0 loaded by discord.py (apt install libopus0 if missing) +2. Verify ffmpeg in PATH +3. Verify Supertonic TTS reachable at :7788 +4. Warm faster-whisper small int8 (downloads to ~/.cache/huggingface/ if cold) +5. Warm silero-vad +6. Generate assets/voice/{beep_200ms,mhm,thinking}.wav via Supertonic + ffmpeg + +Exit code: 0 = all green, 1 = something needs human intervention. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +import time +import urllib.request +import urllib.error +import json +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parent.parent +ASSETS_DIR = REPO_ROOT / "assets" / "voice" +SUPERTONIC_URL = "http://127.0.0.1:7788/v1/audio/speech" +SUPERTONIC_VOICE = "M2" + +GREEN = "\033[32m" +RED = "\033[31m" +YELLOW = "\033[33m" +RESET = "\033[0m" + + +def _ok(msg: str) -> None: + print(f"{GREEN}[ OK ]{RESET} {msg}") + + +def _fail(msg: str) -> None: + print(f"{RED}[FAIL]{RESET} {msg}") + + +def _warn(msg: str) -> None: + print(f"{YELLOW}[WARN]{RESET} {msg}") + + +def check_libopus() -> bool: + try: + import discord + except ImportError: + _fail("discord.py not installed — run `pip install -r requirements.txt`") + return False + + if discord.opus.is_loaded(): + _ok("libopus loaded (discord.py)") + return True + + try: + discord.opus._load_default() + except Exception: + pass + + if discord.opus.is_loaded(): + _ok("libopus loaded after fallback") + return True + + _fail( + "libopus NOT loaded — Discord voice will fail silent. " + "Run: sudo apt install -y libopus0" + ) + return False + + +def check_ffmpeg() -> bool: + if not shutil.which("ffmpeg"): + _fail("ffmpeg not in PATH — required for audio asset generation") + return False + _ok(f"ffmpeg at {shutil.which('ffmpeg')}") + return True + + +def check_supertonic() -> bool: + try: + req = urllib.request.Request( + SUPERTONIC_URL, + data=json.dumps( + { + "model": "supertonic-3", + "input": "test", + "voice": SUPERTONIC_VOICE, + "response_format": "wav", + "lang": "ro", + } + ).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=5) as resp: + if resp.status == 200: + _ok(f"Supertonic up at {SUPERTONIC_URL}") + return True + except (urllib.error.URLError, ConnectionError) as e: + _fail(f"Supertonic unreachable at :7788 — {e}. Start: systemctl --user start supertonic-tts") + return False + _fail(f"Supertonic returned non-200") + return False + + +def warm_whisper() -> bool: + try: + from faster_whisper import WhisperModel + except ImportError: + _fail("faster-whisper not installed") + return False + + print(" Warming faster-whisper small int8 (downloads if cold)...") + t0 = time.perf_counter() + try: + WhisperModel("small", device="cpu", compute_type="int8", cpu_threads=4) + elapsed = time.perf_counter() - t0 + _ok(f"faster-whisper small int8 warm ({elapsed:.1f}s)") + return True + except Exception as e: + _fail(f"faster-whisper warm failed: {e}") + return False + + +def warm_silero() -> bool: + try: + from silero_vad import load_silero_vad + except ImportError: + _fail("silero-vad not installed") + return False + + print(" Warming silero-vad...") + t0 = time.perf_counter() + try: + load_silero_vad() + elapsed = time.perf_counter() - t0 + _ok(f"silero-vad warm ({elapsed:.1f}s)") + return True + except Exception as e: + _fail(f"silero-vad warm failed: {e}") + return False + + +def _supertonic_synth(text: str, out_path: Path) -> bool: + payload = { + "model": "supertonic-3", + "input": text, + "voice": SUPERTONIC_VOICE, + "response_format": "wav", + "lang": "ro", + } + req = urllib.request.Request( + SUPERTONIC_URL, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=30) as resp: + wav_bytes = resp.read() + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_bytes(wav_bytes) + return True + except Exception as e: + _fail(f"Supertonic synth failed for {out_path.name}: {e}") + return False + + +def gen_thinking_wav() -> bool: + path = ASSETS_DIR / "thinking.wav" + if path.exists() and path.stat().st_size > 1024: + _ok(f"thinking.wav exists ({path.stat().st_size} bytes)") + return True + print(" Generating thinking.wav via Supertonic...") + if _supertonic_synth("Stai puțin să-mi adun gândurile.", path): + _ok(f"thinking.wav generated ({path.stat().st_size} bytes)") + return True + return False + + +def gen_mhm_wav() -> bool: + path = ASSETS_DIR / "mhm.wav" + if path.exists() and path.stat().st_size > 512: + _ok(f"mhm.wav exists ({path.stat().st_size} bytes)") + return True + print(" Generating mhm.wav via Supertonic...") + if _supertonic_synth("Mhm.", path): + _ok(f"mhm.wav generated ({path.stat().st_size} bytes)") + return True + return False + + +def gen_beep_wav() -> bool: + path = ASSETS_DIR / "beep_200ms.wav" + if path.exists() and path.stat().st_size > 512: + _ok(f"beep_200ms.wav exists ({path.stat().st_size} bytes)") + return True + print(" Generating beep_200ms.wav via ffmpeg (880Hz sine, 200ms)...") + path.parent.mkdir(parents=True, exist_ok=True) + try: + subprocess.run( + [ + "ffmpeg", + "-y", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + "sine=frequency=880:duration=0.2:sample_rate=48000", + "-af", + "afade=t=out:st=0.15:d=0.05,volume=0.3", + "-ac", + "2", + str(path), + ], + check=True, + ) + _ok(f"beep_200ms.wav generated ({path.stat().st_size} bytes)") + return True + except subprocess.CalledProcessError as e: + _fail(f"ffmpeg beep gen failed: {e}") + return False + + +def main() -> int: + print(f"voice_setup.py — Discord voice pipeline setup\n") + + checks: list[tuple[str, bool]] = [] + + checks.append(("libopus", check_libopus())) + checks.append(("ffmpeg", check_ffmpeg())) + checks.append(("Supertonic", check_supertonic())) + checks.append(("faster-whisper", warm_whisper())) + checks.append(("silero-vad", warm_silero())) + + if checks[2][1]: # Supertonic OK + checks.append(("thinking.wav", gen_thinking_wav())) + checks.append(("mhm.wav", gen_mhm_wav())) + else: + _warn("Skipping thinking.wav / mhm.wav generation — Supertonic down") + checks.append(("thinking.wav", False)) + checks.append(("mhm.wav", False)) + + if checks[1][1]: # ffmpeg OK + checks.append(("beep_200ms.wav", gen_beep_wav())) + else: + _warn("Skipping beep_200ms.wav — ffmpeg missing") + checks.append(("beep_200ms.wav", False)) + + print() + failed = [name for name, ok in checks if not ok] + if failed: + print(f"{RED}FAILED:{RESET} {len(failed)}/{len(checks)} — fix above before /voice join works:") + for name in failed: + print(f" - {name}") + return 1 + + print(f"{GREEN}ALL GREEN{RESET} ({len(checks)} checks). Voice pipeline ready.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/vendor/discord-ext-voice-recv/.gitignore b/vendor/discord-ext-voice-recv/.gitignore new file mode 100644 index 0000000..ee011fb --- /dev/null +++ b/vendor/discord-ext-voice-recv/.gitignore @@ -0,0 +1,132 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +.vscode/ +*.code-* diff --git a/vendor/discord-ext-voice-recv/LICENSE b/vendor/discord-ext-voice-recv/LICENSE new file mode 100644 index 0000000..8cbe445 --- /dev/null +++ b/vendor/discord-ext-voice-recv/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-present Imayhaveborkedit + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/vendor/discord-ext-voice-recv/README.md b/vendor/discord-ext-voice-recv/README.md new file mode 100644 index 0000000..fad7c4b --- /dev/null +++ b/vendor/discord-ext-voice-recv/README.md @@ -0,0 +1,230 @@ +![PyPI - Version](https://img.shields.io/pypi/v/discord-ext-voice-recv?color=dodgerblue&link=https%3A%2F%2Fpypi.org%2Fproject%2Fdiscord-ext-voice-recv%2F) + +# discord-ext-voice-recv +Voice receive extension package for discord.py + +## Warning +**This extension should be more or less functional, but the code is not yet feature complete. No guarantees are given for stability or random breaking changes.** + +See the [update notes](update_notes.md) for a poor excuse for a changelog. + +## Installing +**Python 3.8 or higher is required**, preferably at least 3.11 or whatever is latest + +``` +python -m pip install discord-ext-voice-recv +``` + +To install directly from github: +``` +python -m pip install git+https://github.com/imayhaveborkedit/discord-ext-voice-recv +``` + +Naturally, this extension depends on `discord.py` being installed with voice support (`pynacl`). + +## Example +See the [example script](examples/recv.py). + +## Feature overview +### Custom VoiceProtocol client +No monkey patching or bizarre hacks required. Simply use the library feature to use `VoiceRecvClient` as the voice client class. See [Usage](#usage). + +### New events +This extension adds the unimplemented voice websocket events and three virtual events. See [New Events](#new-events). + +### Speaking state +It is now possible to determine if a member is speaking or not, using `VoiceRecvClient.get_speaking()`, or using the speaking events inside an `AudioSink`. + +### Simple and familiar API +The overall API is designed to mirror the discord.py voice send API, with `AudioSink` being the counterpart to the existing `AudioSource`. See [Sinks](#sinks). + +### Convenient included utilities +Batteries included in the form of useful built in `AudioSinks`. Some to match their `AudioSource` counterpart, some I merely considered useful. See... uh... TODO. + +### Optional extras +Slightly more complex included batteries that depend on external packages. These live in `voice_recv.extras`. They can be installed by adding their optional dependency during install, ex: `pip install discord-ext-voice-recv[extras_thing]`, or all of them can be installed by specifying `extras` instead. See [Extras](#extras). + +### More or less typed +It's probably fine. + +## Usage +### VoiceRecvClient +The class `voice_recv.VoiceRecvClient` must be used in `VoiceChannel.connect()` to enable voice receive functionality. +```python +from discord.ext import voice_recv + +voice_client = await voice_channel.connect(cls=voice_recv.VoiceRecvClient) +``` + +### New voice client functions +```python +def listen(sink: voice_recv.AudioSink, *, after=None) -> None +``` +Receives audio data into an `AudioSink`. A sink is similar to the `AudioSource` class, where most of the logic is done in a single callback function, but in reverse. Sinks are explained in detail in the [Sinks](#sinks) section below. + +The finalizer, `after` is called after the sink has been exhausted or an error occurred. The callback signature is the same as the after callback for `play()`, one parameter for an optional Exception object. + +```python +def is_listening() -> bool +``` +Returns `True` if the voice client is currently receiving audio. Specifically, if the bot is reading from the voice socket. + +```python +def stop() -> None +``` +This function now stops both receiving and sending of audio. + +```python +def stop_listening() -> None +``` +Stops receiving audio. + +```python +def stop_playing() -> None +``` +Stops playing audio. This function is identical to `discord.VoiceClient.stop()`. + +```python +def get_speaking(member: discord.Member | discord.User) -> bool | None +``` +Gets the speaking state (voice activity, the green circle) of a member. User is typed in for convenience. Returns None if the member was not found. + +## Sinks +The API of this extension is designed to mirror the discord.py voice send API. Sending audio uses the `AudioSource` class, while receiving audio uses the `AudioSink` class. A sink is designed to be the inverse of a source. Essentially, a source is a callback called by discord.py to produce a chunk of audio data. Conversely, a sink is a callback called by the library to handle a chunk of audio. Sinks can be composed in the same fashion as sources, creating an audio processing pipeline. Sources and sinks can even combined into one object to handle both tasks, such as creating a feedback loop. + +Special care should be taken not to write excessively computationally expensive code, as python is not particularly well suited to real-time audio processing. + +Due to voice receive being somewhat more complex than voice sending, sinks have additional functionality compared to sources. However, the core sink functions should look relatively familiar. + +```python +class MySink(voice_recv.AudioSink): + def __init__(self): + super().__init__() + + def wants_opus(self) -> bool: + return False + + def write(self, user: User | Member | None, data: VoiceData): + ... + + def cleanup(self): + ... +``` + +These are the main functions of a sink, names and purpose reflecting that of their source counterparts. It is important to note that `super().__init__()` must be called when inheriting from `AudioSink`, in contrast to `AudioSource` which does not have a default `__init__` function. + +- The `wants_opus()` function determines if the sink should receive opus packets or decoded PCM packets. Care should be taken not to unintentionally mix sinks that want different types. +- The `write()` function is the main callback, where the sink logic takes place. In a sink pipeline, this could alter, inspect, or log a packet, and then write it to a child sink. `VoiceData` is a simple container class with attributes for the origin member, opus data, optionally pcm data, and raw audio packet. +- The `cleanup()` function is identical to `AudioSource.cleanup()`, a finalizer to cleanup any loose ends when the sink has finished its job. + +Additionally, sinks also have properties for their `client` and `voice_client`, as well as `parent` and `child`/`children` sinks. + +### Built in Sinks +This extension comes with several useful built in sinks, as well as a few [extras](#extras) mentioned later. For a more information, you will have to [source dive](discord/ext/voice_recv/sinks.py) for now. + +- `AudioSink` - The base class for most sinks, similar in purpose to the discord.py `AudioSource`. + - `MultiAudioSink` - A sink that supports writing to multiple destination sinks. Has no subclass implementations currently. Generally intended to be extended by the user. + - `BasicSink` - A simple sink that operates based on a user provided callback. Useful for testing or simple tasks not performed by other sinks. + - `WaveSink` - Writes audio data to a .wav file. It does not fill in silence or mix audio from multiple users on its own. `WavSink` is an alias for this sink. + - `FFmpegSink` - Uses ffmpeg to convert the audio stream to an arbitrary format, or whatever else ffmpeg can do to it. Requires ffmpeg, but you should already have it working for discord.py. + - `PCMVolumeTransformer` - The AudioSink analog to the discord.py AudioSource version. Does exactly the same thing: controls the volume. + - `ConditionalFilter` - Filters audio data based on a given predicate. If the predicate fails for a packet, it is not written to the destination sink. + - `UserFilter` - A conditional filter to check if data is from a given user. + - `TimedFilter` - A conditional filter with a timer for how long it should operate. + - `SilenceGeneratorSink` - Generates silence to fill in audio transmission downtime for a continuous data stream. **Note: This sink is pretty broken and buggy right now and slated for rewrite. Usage is not advised.** + +### Sink event listeners +With AudioSinks being potentially more complex and stateful than AudioSources and the addition of new events, it is sometimes necessary to handle events in the context of a sink. It would be rather awkward to have to register a sink function with `commands.Bot.add_listener()` while dealing with thread safety, and even more so using `discord.Client`. To remedy this, listeners can be defined within sinks, similarly to how they work in Cogs. + +```python +class MySink(AudioSink): + @AudioSink.listener() + def on_voice_member_disconnect(self, member: discord.Member, ssrc: int | None): + print(f"{member} has disconnected") + self.do_something_like_handle_disconnect(ssrc) +``` + +Note that these functions must be sync functions, as they are dispatched from a thread. Trying to use an async function will result in an error. This restriction only applies to sink listeners, and normal async event listeners will function as per usual. The event listener dispatch thread is different from the one used to dispatch the `write()` callback so potential thread safety issues should be considered. A decorator argument to run the event callback in the other thread *may* be added later. + +## New events +```python +async def on_voice_member_speaking_state(member: discord.Member, ssrc: int, state: SpeakingState | int) +``` +First and foremost, this event does **NOT** refer to the speaking indicator in discord (the green circle). For voice activity, see `on_voice_member_speaking_start`. +This event is fired when the speaking state (speaking mode) of a member changes. This happens when: +- A member first speaks (transmits audio) in a voice, but only once per session +- A member activates or deactivates priority speaker mode + +This event is fired once initially to reveal the ssrc of a member, an identifier to map packets to their originating member. Any packets received from this member before this event fires can (probably) be safely ignored since they are likely just silence packets. + +```python +async def on_voice_member_connect(member: discord.Member) +``` + +Called when a member connects to a voice channel. Also called on initial connection for every member in the channel. + +```python +async def on_voice_member_disconnect(member: discord.Member, ssrc: int | None) +``` +Called when a member disconnects from a voice channel. The `ssrc` parameter is the unique id a member has to identify which packets belong to them. This is useful when using custom sinks, particularly those that handle packets from multiple members. + +```python +async def on_voice_member_video(member: discord.Member, data: voice_recv.VoiceVideoStreams) +``` +Called when a member in voice channel toggles their webcam on or off, NOT screenshare. Screenshare status is only indicated in the `self_video` attribute of `discord.VoiceState`. + +```python +async def on_voice_member_flags(member: discord.Member, flags: voice_recv.VoiceFlags) +``` +An undocumented event dispatched when a member joins a voice channel containing a flags bitfield. Also called on initial connection for every member in the channel. + +Flags: +- `VoiceFlags.clips_enabled`: User has [clips](https://support.discord.com/hc/en-us/articles/16861982215703-Clips) enabled +- `VoiceFlags.allow_voice_recording`: User has consented to their voice being clipped +- `VoiceFlags.allow_any_viewer_clips`: User has consented to stream viewers clipping them + +```python +async def on_voice_member_platform(member: discord.Member, platform: voice_recv.VoicePlatform | None) +``` +An undocumented event dispatched when a member joins a voice channel containing the member's platform. Also called on initial connection for every member in the channel. + +Values: +- `VoicePlatform.desktop` +- `VoicePlatform.mobile` +- `VoicePlatform.xbox` +- `VoicePlatform.playstation` + +```python +def on_rtcp_packet(packet: RTCPPacket, guild: discord.Guild) +``` +A virtual event for when an RTCP packet is received. This event only works inside of sinks, so it cannot be async. + +```python +def on_voice_member_speaking_start(member: discord.Member) +def on_voice_member_speaking_stop(member: discord.Member) +``` +Virtual events for the state of the speaking indicator (the green circle). These events are synthesized from packet activity and may not exactly match what is displayed in the discord client. Due to performance issues with asyncio, this event is sink only and cannot be async. + +## Extras + +### `voice_recv.extras.speechrecognition` +- Optional dependency: `extras_speech` +- Requires package: `SpeechRecognition` +- Provides: `SpeechRecognitionSink` + +A helper sink for using `SpeechRecognition` to perform speech-to-text conversion. Generally depends on third party services for reasonable quality. Results may vary. + +### `voice_recv.extras.localplayback` +- Optional dependency: `extras_local` +- Requires package: `pyaudio` +- Provides: `LocalPlaybackSink`, `SimpleLocalPlaybackSink` + +Helper sinks for playing audio through an audio output device the local system. Defaults to the system default device, but other output devices can also be specified. + +## Currently missing or WIP features +- Silence generation (WIP, pending rewrite) + +## Future plans +- Muxer AudioSink (mixes multiple audio streams into a single stream) +- Rust implementations of some components for improved performance +- Alternative voice client implementation with a minimal interface intended for use with external data processing diff --git a/vendor/discord-ext-voice-recv/VENDOR_INFO.md b/vendor/discord-ext-voice-recv/VENDOR_INFO.md new file mode 100644 index 0000000..1c45e35 --- /dev/null +++ b/vendor/discord-ext-voice-recv/VENDOR_INFO.md @@ -0,0 +1,22 @@ +# Vendored: discord-ext-voice-recv + +**Upstream:** https://github.com/imayhaveborkedit/discord-ext-voice-recv +**Pinned commit:** `ac04ea7b0941112e83767cf1c1469b408fa06748` (bump version 0.5.3a) +**Vendored at:** 2026-05-27 +**Reason:** Discord voice protocol is fragile, upstream is hobby fork. Adapter +layer in `src/voice/_discord_voice_adapter.py` isolates upstream churn — if this +package breaks, swap to py-cord by rewriting only that file. + +## Update procedure + +```bash +cd vendor/discord-ext-voice-recv +git fetch origin master +git log HEAD..origin/master --oneline # review what changed +git checkout +cd ../.. +source .venv/bin/activate && pip install -e vendor/discord-ext-voice-recv --force-reinstall +pytest tests/test_voice_adapter_contract.py -v # MUST PASS — contract guard +``` + +Update this file's `Pinned commit` after a successful upgrade. diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/__init__.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/__init__.py new file mode 100644 index 0000000..4df6e26 --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +from .voice_client import * +from .reader import * +from .sinks import * +from .video import * +from .opus import * +from .rtp import * +from .enums import * + +from . import ( + rtp as rtp, + extras as extras, +) + +__title__ = 'discord.ext.voice_recv' +__author__ = 'Imayhaveborkedit' +__license__ = 'MIT' +__copyright__ = 'Copyright 2021-present Imayhaveborkedit' +__version__ = '0.5.3a' diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/buffer.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/buffer.py new file mode 100644 index 0000000..a7e2e11 --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/buffer.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import heapq +import logging +import threading + +from .utils import gap_wrapped, add_wrapped + + +from typing import ( + TYPE_CHECKING, + Protocol, + TypeVar, +) + +from .rtp import _PacketCmpMixin + +if TYPE_CHECKING: + from typing import Optional, List + from .rtp import AudioPacket + +__all__ = [ + 'HeapJitterBuffer', +] + + +_T = TypeVar('_T') +PacketT = TypeVar('PacketT', bound=_PacketCmpMixin) + + +log = logging.getLogger(__name__) + + +class Buffer(Protocol[_T]): + """The base class representing a simple buffer with no extra features.""" + + # fmt: off + def __len__(self) -> int: ... + def push(self, item: _T) -> None: ... + def pop(self) -> Optional[_T]: ... + def peek(self) -> Optional[_T]: ... + def flush(self) -> List[_T]: ... + def reset(self) -> None: ... + # fmt: on + + +class BaseBuffer(Buffer[PacketT]): + """A basic buffer.""" + + def __init__(self): + self._buffer: List[PacketT] = [] + + def __len__(self) -> int: + return len(self._buffer) + + def push(self, item: PacketT) -> None: + self._buffer.append(item) + + def pop(self) -> Optional[PacketT]: + return self._buffer.pop() + + def peek(self) -> Optional[PacketT]: + return self._buffer[-1] if self._buffer else None + + def flush(self) -> List[PacketT]: + buf = self._buffer.copy() + self._buffer.clear() + return buf + + def reset(self) -> None: + self._buffer.clear() + + +class HeapJitterBuffer(BaseBuffer[PacketT]): + """Push item in, pop items out""" + + _threshold: int = 10000 + + def __init__(self, maxsize: int = 10, *, prefsize: int = 1, prefill: int = 1): + if maxsize < 1: + raise ValueError(f'maxsize ({maxsize}) must be greater than 0') + + if not 0 <= prefsize <= maxsize: + raise ValueError(f'prefsize must be between 0 and maxsize ({maxsize})') + + self.maxsize: int = maxsize + self.prefsize: int = prefsize + self.prefill: int = prefill + self._prefill: int = prefill + + self._last_tx_seq: int = -1 + + self._has_item: threading.Event = threading.Event() + # I sure hope I dont need to add a lock to this + self._buffer: List[AudioPacket] = [] + + def _push(self, packet: AudioPacket) -> None: + heapq.heappush(self._buffer, packet) + + def _pop(self) -> AudioPacket: + return heapq.heappop(self._buffer) + + def _get_packet_if_ready(self) -> Optional[AudioPacket]: + return self._buffer[0] if len(self._buffer) > self.prefsize else None + + def _pop_if_ready(self) -> Optional[AudioPacket]: + return self._pop() if len(self._buffer) > self.prefsize else None + + def _update_has_item(self) -> None: + prefilled = self._prefill == 0 + packet_ready = len(self._buffer) > self.prefsize + + if not prefilled or not packet_ready: + self._has_item.clear() + return + + next_packet = self._buffer[0] + sequential = add_wrapped(self._last_tx_seq, 1) == next_packet.sequence + positive_seq = self._last_tx_seq >= 0 + + # We have the next packet ready + # OR we havent sent a packet out yet + # OR the buffer is full + if (sequential and positive_seq) or not positive_seq or len(self._buffer) >= self.maxsize: + self._has_item.set() + else: + self._has_item.clear() + + def _cleanup(self) -> None: + # Logging this is pointless until I fix the stale remote buffer issue + # if len(self._buffer) > self.maxsize: + # log.debug("Buffer overfilled: %s > %s", len(self._buffer), self.maxsize) + + # drop oldest packets if buffer overfilled + while len(self._buffer) > self.maxsize: + packet = heapq.heappop(self._buffer) + # log.debug("Dropped extra packet %s", packet) + + def push(self, packet: AudioPacket) -> bool: + """ + Push a packet into the buffer. If the packet would make the buffer + exceed its maxsize, the oldest packet will be dropped. + """ + + seq = packet.sequence + + # for the gap between _last_tx_seq and the current one, a large gap is old, a small gap is new + # the gap for old packets will generally be very large since they wrap all the way around + if gap_wrapped(self._last_tx_seq, seq) > self._threshold and self._last_tx_seq != -1: + log.debug("Dropping old packet %s", packet) + return False + + self._push(packet) + + if self._prefill > 0: + self._prefill -= 1 + + self._cleanup() + self._update_has_item() + + return True + + def pop(self, *, timeout: float | None = 0) -> Optional[AudioPacket]: + """ + If timeout is a positive number, wait as long as timeout for a packet + to be ready and return that packet, otherwise return None. + """ + + ok = self._has_item.wait(timeout) + if not ok: + return None + + if self._prefill > 0: + return None + + # This function should actually be redundant but i'll leave it for now + packet = self._pop_if_ready() + + if packet is not None: + self._last_tx_seq = packet.sequence + + self._update_has_item() + return packet + + def peek(self, *, all: bool = False) -> Optional[AudioPacket]: + """ + Returns the next packet in the buffer only if it is ready, meaning it can + be popped. When `all` is set to True, it returns the next packet, if any. + """ + + if not self._buffer: + return None + + if all: + return self._buffer[0] + else: + return self._get_packet_if_ready() + + def peek_next(self) -> Optional[AudioPacket]: + """ + Returns the next packet in the buffer only if it is sequential. + """ + + packet = self.peek(all=True) + + if packet is None: + return + + if packet.sequence == add_wrapped(self._last_tx_seq, 1) or self._last_tx_seq < 0: + return packet + + def gap(self) -> int: + """ + Returns the number of missing packets between the last packet to be + popped and the currently held next packet. Returns 0 otherwise. + """ + + if self._buffer and self._last_tx_seq > 0: + return gap_wrapped(self._last_tx_seq, self._buffer[0].sequence) + + return 0 + + def flush(self) -> List[AudioPacket]: + """ + Return all remaining packets. + """ + + packets = sorted(self._buffer) + self._buffer.clear() + + if packets: + self._last_tx_seq = packets[-1].sequence + + self._prefill = self.prefill + self._has_item.clear() + + return packets + + def reset(self) -> None: + """ + Clear buffer and reset internal counters. + """ + + self._buffer.clear() + self._has_item.clear() + self._prefill = self.prefill + self._last_tx_seq = -1 diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/enums.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/enums.py new file mode 100644 index 0000000..cac5ee5 --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/enums.py @@ -0,0 +1,30 @@ +from discord.flags import BaseFlags, fill_with_flags, flag_value +from discord.enums import Enum + +__all__ = ( + 'VoiceFlags', + 'VoicePlatform', +) + +@fill_with_flags() +class VoiceFlags(BaseFlags): + __slots__ = () + + @flag_value + def clips_enabled(self): + return 1 << 0 + + @flag_value + def allow_voice_recording(self): + return 1 << 1 + + @flag_value + def allow_any_viewer_clips(self): + return 1 << 2 + + +class VoicePlatform(Enum): + desktop = 0 + mobile = 1 + xbox = 2 + playstation = 3 diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/__init__.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/__init__.py new file mode 100644 index 0000000..d21bd83 --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/__init__.py @@ -0,0 +1,2 @@ +from . import speechrecognition +from . import localplayback diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/localplayback.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/localplayback.py new file mode 100644 index 0000000..49fb0d6 --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/localplayback.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from ..sinks import AudioSink + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..opus import VoiceData + from ..types import MemberOrUser + + +__all__ = [ + 'LocalPlaybackSink', + 'SimpleLocalPlaybackSink', +] + +log = logging.getLogger(__name__) + +try: + import pyaudio +except ImportError: + + def __getattr__(name: str): + if name in __all__: + raise RuntimeError('The pyaudio module is required to use this sink.') + +else: + if TYPE_CHECKING: + from typing import Optional, Dict + + from discord import Member + + PyAudioStream = pyaudio._Stream + + class _BaseLocalPlaybackSink(AudioSink): + pa: pyaudio.PyAudio = None # type: ignore + + def __init__(self, output_device_id: Optional[int] = None, *, py_audio: Optional[pyaudio.PyAudio] = None): + self._init_pa(py_audio) + + if output_device_id is None: + output_device_id = self.pa.get_default_output_device_info().get("index") # type: ignore + self.output_device_id = output_device_id + + @classmethod + def _init_pa(cls, pa: Optional[pyaudio.PyAudio]) -> None: + if pa is None: + if cls.pa is None: + cls.pa = pyaudio.PyAudio() + else: + if cls.pa is None: + cls.pa = pa + elif cls.pa is not pa: + raise RuntimeError("Conflicting PyAudio objects") + + def write(self, user: Optional[MemberOrUser], data: VoiceData) -> None: + raise NotImplementedError + + def wants_opus(self) -> bool: + return False + + @classmethod + def terminate_pyaudio(cls): + """Call this when you are completely done using all instances of LocalPlayback sinks.""" + + cls.pa.terminate() + cls.pa = None # type: ignore + + class SimpleLocalPlaybackSink(_BaseLocalPlaybackSink): + """ + A simplified version of LocalPlaybackSink that only supports one stream of audio. + Convenient for when you have already isolated a single member's audio. + """ + + def __init__(self, output_device_id: Optional[int] = None, *, py_audio: Optional[pyaudio.PyAudio] = None): + super().__init__(output_device_id, py_audio=py_audio) + self._stream: PyAudioStream = self.pa.open( + rate=48000, + channels=2, + format=pyaudio.paInt16, + output=True, + output_device_index=output_device_id, + ) + + def write(self, user: Optional[MemberOrUser], data: VoiceData) -> None: + self._stream.write(data.pcm) + + def cleanup(self) -> None: + self._stream.close() + + class LocalPlaybackSink(_BaseLocalPlaybackSink): + """ + An AudioSink for playing received audio directly to one of the system's audio output devices using PyAudio. + This sink can handle playback of multiple users' audio without additional stream mixing beforehand. + + The `output_device_id` parameter defaults to the system's default audio device, and can otherwise be + acquired via PyAudio functions. A specific `PyAudio` instance can also be passed to use a specific instance. + """ + + def __init__(self, output_device_id: Optional[int] = None, *, py_audio: Optional[pyaudio.PyAudio] = None): + super().__init__(output_device_id, py_audio=py_audio) + self._streams: Dict[int, PyAudioStream] = {} + + def _get_stream(self, user: MemberOrUser) -> PyAudioStream: + stream = self._streams.get(user.id) + if stream is None: + stream = self._streams[user.id] = self.pa.open( + rate=48000, + channels=2, + format=pyaudio.paInt16, + output=True, + output_device_index=self.output_device_id, + ) + return stream + + def write(self, user: Optional[MemberOrUser], data: VoiceData) -> None: + if user: + self._get_stream(user).write(data.pcm) + + def cleanup(self) -> None: + for stream in tuple(self._streams.values()): + stream.close() + + @AudioSink.listener() + def on_voice_member_disconnect(self, member: Member, ssrc: Optional[int]) -> None: + stream = self._streams.pop(member.id, None) + if stream: + stream.close() diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/speechrecognition.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/speechrecognition.py new file mode 100644 index 0000000..2a74b6a --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/extras/speechrecognition.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from ..sinks import AudioSink + +log = logging.getLogger(__name__) + +__all__ = [ + 'SpeechRecognitionSink', +] + +try: + import speech_recognition as sr +except ImportError: + + def __getattr__(name: str): + if name in __all__: + raise RuntimeError('The SpeechRecognition module is required to use this sink.') + +else: + import time + import array + import asyncio + import audioop + + from collections import defaultdict + + from ..rtp import SilencePacket + + from typing import TYPE_CHECKING, TypedDict + + if TYPE_CHECKING: + from concurrent.futures import Future as CFuture + from typing import Literal, Callable, Optional, Any, Final, Protocol, Awaitable, TypeVar + + from discord import Member + + from ..opus import VoiceData + from ..types import MemberOrUser as User + + T = TypeVar('T') + + # [r.split('_', 1)[1] for r in dir(sr.Recognizer()) if r.startswith("recognize")] + SRRecognizerMethod = Literal[ + 'amazon', + 'api', + 'assemblyai', + 'azure', + 'bing', + 'faster_whisper', + 'google', + 'google_cloud', + 'groq', + 'houndify', + 'ibm', + 'lex', + 'openai', + 'sphinx', + 'tensorflow', + 'vosk', + 'whisper', + 'wit', + ] + + class SRStopper(Protocol): + def __call__(self, wait: bool = True, /) -> None: ... + + SRProcessDataCB = Callable[[sr.Recognizer, sr.AudioData, User], Optional[str]] + SRTextCB = Callable[[User, str], Any] + + class _StreamData(TypedDict): + stopper: Optional[SRStopper] + recognizer: sr.Recognizer + buffer: array.array[int] + + class SpeechRecognitionSink(AudioSink): + def __init__( + self, + *, + process_cb: Optional[SRProcessDataCB] = None, + text_cb: Optional[SRTextCB] = None, + default_recognizer: SRRecognizerMethod = 'google', + phrase_time_limit: int = 10, + ignore_silence_packets: bool = True, + ): + super().__init__(None) + self.process_cb: Optional[SRProcessDataCB] = process_cb + self.text_cb: Optional[SRTextCB] = text_cb + self.phrase_time_limmit: int = phrase_time_limit + self.ignore_silence_packets: bool = ignore_silence_packets + + self.default_recognizer: SRRecognizerMethod = default_recognizer + self._stream_data: defaultdict[int, _StreamData] = defaultdict( + lambda: _StreamData(stopper=None, recognizer=sr.Recognizer(), buffer=array.array('B')) + ) + + def _await(self, coro: Awaitable[T]) -> CFuture[T]: + assert self.client is not None + return asyncio.run_coroutine_threadsafe(coro, self.client.loop) # type: ignore + + def wants_opus(self) -> bool: + return False + + def write(self, user: Optional[User], data: VoiceData) -> None: + if self.ignore_silence_packets and isinstance(data.packet, SilencePacket): + return + + if user is None: + return + + sdata = self._stream_data[user.id] + sdata['buffer'].extend(data.pcm) + + if not sdata['stopper']: + sdata['stopper'] = sdata['recognizer'].listen_in_background( + DiscordSRAudioSource(sdata['buffer']), self.background_listener(user), self.phrase_time_limmit + ) + + def background_listener(self, user: User): + process_cb = self.process_cb or self.get_default_process_callback() + text_cb = self.text_cb or self.get_default_text_callback() + + def callback(_recognizer: sr.Recognizer, _audio: sr.AudioData): + output = process_cb(_recognizer, _audio, user) + if output is not None: + text_cb(user, output) + + return callback + + def get_default_process_callback(self) -> SRProcessDataCB: + def cb(recognizer: sr.Recognizer, audio: sr.AudioData, user: Optional[User]) -> Optional[str]: + log.debug("Got %s, %s, %s", audio, audio.sample_rate, audio.sample_width) + text: Optional[str] = None + try: + # they changed recognize_google to be optionally assigned at runtime... + func = getattr(recognizer, 'recognize_' + self.default_recognizer, recognizer.recognize_google) # type: ignore + text = func(audio) + except sr.UnknownValueError: + log.debug("Bad speech chunk") + # self._debug_audio_chunk(audio) + + return text + + return cb + + def get_default_text_callback(self) -> SRTextCB: + def cb(user: Optional[User], text: Optional[str]) -> Any: + log.info("%s said: %s", user.display_name if user else 'Someone', text) + + return cb + + @AudioSink.listener() + def on_voice_member_disconnect(self, member: Member, ssrc: Optional[int]) -> None: + if member is not None: + self._drop(member.id) + + def cleanup(self) -> None: + for user_id in tuple(self._stream_data.keys()): + self._drop(user_id) + + def _drop(self, user_id: int) -> None: + data = self._stream_data.pop(user_id, None) + if data is None: + log.debug("Cannot drop user id: %s, no data", user_id) + return + + stopper = data.get('stopper') + if stopper: + stopper() + + buffer = data.get('buffer') + if buffer: + # arrays don't have a clear function + del buffer[:] + + def _debug_audio_chunk(self, audio: sr.AudioData, filename: str = 'sound.wav') -> None: + import io, wave, discord + + with io.BytesIO() as b: + with wave.open(b, 'wb') as writer: + writer.setframerate(48000) + writer.setsampwidth(2) + writer.setnchannels(2) + writer.writeframes(audio.get_wav_data()) + + b.seek(0) + f = discord.File(b, filename) + self._await(self.voice_client.channel.send(file=f)) # type: ignore + + class DiscordSRAudioSource(sr.AudioSource): + little_endian: Final[bool] = True + SAMPLE_RATE: Final[int] = 48_000 + SAMPLE_WIDTH: Final[int] = 2 + CHANNELS: Final[int] = 2 + CHUNK: Final[int] = 960 + + def __init__(self, buffer: array.array[int]): + self.buffer = buffer + self._entered: bool = False + + @property + def stream(self): + return self + + def __enter__(self): + if self._entered: + log.warning('Already entered sr audio source') + self._entered = True + return self + + def __exit__(self, *exc) -> None: + self._entered = False + if any(exc): + log.exception('Error closing sr audio source') + + def read(self, size: int) -> bytes: + # TODO: make this timeout configurable + for _ in range(10): + if len(self.buffer) < size * self.CHANNELS: + time.sleep(0.1) + else: + break + else: + if len(self.buffer) == 0: + return b'' + + chunksize = size * self.CHANNELS + audiochunk = self.buffer[:chunksize].tobytes() + del self.buffer[: min(chunksize, len(audiochunk))] + audiochunk = audioop.tomono(audiochunk, 2, 1, 1) + return audiochunk + + def close(self) -> None: + self.buffer.clear() diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/gateway.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/gateway.py new file mode 100644 index 0000000..450b3fb --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/gateway.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from discord.enums import SpeakingState, try_enum + +from .enums import VoiceFlags, VoicePlatform +from .video import VoiceVideoStreams + +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from typing import Dict, Any + + from discord.gateway import DiscordVoiceWebSocket + from .voice_client import VoiceRecvClient + from .video import VoiceVideoPayload + +log = logging.getLogger(__name__) + + +# https://cdn.discordapp.com/attachments/381887113391505410/1094473412623204533/image.png +# fmt: off +IDENTIFY = 0 +SELECT_PROTOCOL = 1 +READY = 2 +HEARTBEAT = 3 +SESSION_DESCRIPTION = 4 # (aka SELECT_PROTOCOL_ACK) +SPEAKING = 5 +HEARTBEAT_ACK = 6 +RESUME = 7 +HELLO = 8 +RESUMED = 9 +CLIENT_CONNECT = 11 +VIDEO = 12 +CLIENT_DISCONNECT = 13 +SESSION_UPDATE = 14 # (useless) +MEDIA_SINK_WANTS = 15 # (useless) +VOICE_BACKEND_VERSION = 16 # (useless) +CHANNEL_OPTIONS_UPDATE = 17 # (dead) +FLAGS = 18 +SPEED_TEST = 19 # (dead) +PLATFORM = 20 +# fmt: on + + +async def hook(self: DiscordVoiceWebSocket, msg: Dict[str, Any]): + op: int = msg['op'] + data: Dict[str, Any] = msg.get('d', {}) + vc: VoiceRecvClient = self._connection.voice_client # type: ignore + + if op not in (3, 6): + from pprint import pformat + + log.debug("Received op %s: \n%s", op, pformat(data, compact=True)) + + if len(msg.keys()) > 2: + m = msg.copy() + m.pop('op') + m.pop('d') + log.info("WS payload has extra keys: %s", m) + + if op == self.READY: + vc._add_ssrc(vc.guild.me.id, data['ssrc']) + + elif op == self.SESSION_DESCRIPTION: + if vc._reader: + # TODO: remove bytes cast once type is fixed in dpy + vc._reader.update_secret_key(bytes(self.secret_key)) # type: ignore + + elif op == self.SPEAKING: + # this event refers to the speaking MODE, e.g. priority speaker + # it also sends the user's ssrc + uid = int(data['user_id']) + ssrc = data['ssrc'] + vc._add_ssrc(uid, ssrc) + member = vc.guild.get_member(uid) + state = try_enum(SpeakingState, data['speaking']) + vc.dispatch("voice_member_speaking_state", member, ssrc, state) + + elif op == CLIENT_CONNECT: + uids = [int(uid) for uid in data['user_ids']] + + # Multiple user IDs means this is the initial member list + for uid in uids: + member = vc.guild.get_member(uid) + vc.dispatch("voice_member_connect", member) + + elif op == VIDEO: + uid = int(data['user_id']) + vc._add_ssrc(uid, data['audio_ssrc']) + member = vc.guild.get_member(uid) + streams = VoiceVideoStreams(data=cast('VoiceVideoPayload', data), vc=vc) + vc.dispatch("voice_member_video", member, streams) + + elif op == CLIENT_DISCONNECT: + uid = int(data['user_id']) + ssrc = vc._get_ssrc_from_id(uid) + + if vc._reader and ssrc is not None: + log.debug("Destroying decoder for %s, ssrc=%s", uid, ssrc) + vc._reader.packet_router.destroy_decoder(ssrc) + + vc._remove_ssrc(user_id=uid) + member = vc.guild.get_member(uid) + vc.dispatch("voice_member_disconnect", member, ssrc) + + elif op == FLAGS: + uid = int(data['user_id']) + member = vc.guild.get_member(uid) + vc.dispatch("voice_member_flags", member, VoiceFlags._from_value(data['flags'] or 0)) + + elif op == PLATFORM: + uid = int(data['user_id']) + member = vc.guild.get_member(uid) + vc.dispatch( + "voice_member_platform", + member, + try_enum(VoicePlatform, data['platform']) if data['platform'] is not None else None, + ) diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/opus.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/opus.py new file mode 100644 index 0000000..a435b19 --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/opus.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging + +from typing import TYPE_CHECKING, Final + +from .buffer import HeapJitterBuffer as JitterBuffer +from .rtp import FakePacket +from .utils import add_wrapped + +from discord.opus import Decoder + +if TYPE_CHECKING: + from typing import Optional, Tuple, Dict, Callable, Any + from .rtp import AudioPacket + from .sinks import AudioSink + from .router import PacketRouter + from .voice_client import VoiceRecvClient + from .types import MemberOrUser as User + + EventCB = Callable[..., Any] + EventData = Tuple[str, Tuple[Any, ...], Dict[str, Any]] + +log = logging.getLogger(__name__) + +__all__ = [ + 'VoiceData', +] + + +class VoiceData: + """Container object for audio data and source user.""" + + __slots__ = ('packet', 'source', 'pcm') + + def __init__(self, packet: AudioPacket, source: Optional[User], *, pcm: Optional[bytes] = None): + self.packet: AudioPacket = packet + self.source: Optional[User] = source + self.pcm: bytes = pcm if pcm else b'' + + @property + def opus(self) -> Optional[bytes]: + return self.packet.decrypted_data + + +class PacketDecoder: + def __init__(self, router: PacketRouter, ssrc: int): + self.router: PacketRouter = router + self.ssrc: int = ssrc + + self._decoder: Optional[Decoder] = None if self.sink.wants_opus() else Decoder() + self._buffer: JitterBuffer = JitterBuffer() + self._cached_id: Optional[int] = None + + self._last_seq: int = -1 + self._last_ts: int = -1 + + @property + def sink(self) -> AudioSink: + return self.router.sink + + def _get_user(self, user_id: int) -> Optional[User]: + vc: VoiceRecvClient = self.sink.voice_client # type: ignore + return vc.guild.get_member(user_id) or vc.client.get_user(user_id) + + def _get_cached_member(self) -> Optional[User]: + return self._get_user(self._cached_id) if self._cached_id else None + + def _flag_ready_state(self): + if self._buffer.peek(): + self.router.waiter.register(self) + else: + self.router.waiter.unregister(self) + + def push_packet(self, packet: AudioPacket) -> None: + self._buffer.push(packet) + self._flag_ready_state() + + def pop_data(self, *, timeout: float = 0) -> Optional[VoiceData]: + packet = self._get_next_packet(timeout) + self._flag_ready_state() + + if packet is None: + return + + return self._process_packet(packet) + + def set_user_id(self, user_id: int) -> None: + self._cached_id = user_id + + def reset(self) -> None: + self._buffer.reset() + self._decoder = None if self.sink.wants_opus() else Decoder() + self._last_seq = self._last_ts = -1 + self._flag_ready_state() + + def destroy(self) -> None: + self._buffer.reset() + self._decoder = None + self._flag_ready_state() + + def _get_next_packet(self, timeout: float) -> Optional[AudioPacket]: + packet = self._buffer.pop(timeout=timeout) + + if packet is None: + # Gets the last (buffered) packet out (i think) + # TODO: revist this, might be an issue + if self._buffer: + packets = self._buffer.flush() + if any(packets[1:]): + log.warning( + "%s packets were lost being flushed in decoder-%s\n --> (last=%s) %s", + len(packets) - 1, + self.ssrc, + self._last_seq, + [p.sequence for p in packets], + ) + return packets[0] + return + elif not packet: + packet = self._make_fakepacket() + + return packet + + def _make_fakepacket(self) -> FakePacket: + seq = add_wrapped(self._last_seq, 1) + ts = add_wrapped(self._last_ts, Decoder.SAMPLES_PER_FRAME, wrap=2**32) + return FakePacket(self.ssrc, seq, ts) + + def _process_packet(self, packet: AudioPacket) -> VoiceData: + pcm = None + if not self.sink.wants_opus(): + packet, pcm = self._decode_packet(packet) + + member = self._get_cached_member() + + if member is None: + self._cached_id = self.sink.voice_client._get_id_from_ssrc(self.ssrc) # type: ignore + member = self._get_cached_member() + + data = VoiceData(packet, member, pcm=pcm) + self._last_seq = packet.sequence + self._last_ts = packet.timestamp + + return data + + def _decode_packet(self, packet: AudioPacket) -> Tuple[AudioPacket, bytes]: + assert self._decoder is not None + + # Decode as per usual + if packet: + pcm = self._decoder.decode(packet.decrypted_data, fec=False) + return packet, pcm + + # Fake packet, need to check next one to use fec + next_packet = self._buffer.peek_next() + + if next_packet is not None: + nextdata: bytes = next_packet.decrypted_data # type: ignore + + log.debug( + "Generating fec packet: fake=%s, fec=%s", + packet.sequence, + next_packet.sequence, + ) + pcm = self._decoder.decode(nextdata, fec=True) + + # Need to drop a packet + else: + pcm = self._decoder.decode(None, fec=False) + + return packet, pcm diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/reader.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/reader.py new file mode 100644 index 0000000..596e26c --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/reader.py @@ -0,0 +1,422 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import time +import logging +import threading + +from operator import itemgetter +from typing import TYPE_CHECKING + +from . import rtp +from .sinks import AudioSink +from .router import PacketRouter, SinkEventRouter + +try: + import nacl.secret + from nacl.exceptions import CryptoError +except ImportError as e: + raise RuntimeError("pynacl is required") from e + +if TYPE_CHECKING: + from typing import Optional, Callable, Any, Dict, Literal, Union + + from discord import Member + from discord.types.voice import SupportedModes + from .voice_client import VoiceRecvClient + from .rtp import RTPPacket + + DecryptRTP = Callable[[RTPPacket], bytes] + DecryptRTCP = Callable[[bytes], bytes] + AfterCB = Callable[[Optional[Exception]], Any] + SpeakingEvent = Literal['voice_member_speaking_start', 'voice_member_speaking_stop'] + EncryptionBox = Union[nacl.secret.SecretBox, nacl.secret.Aead] + +log = logging.getLogger(__name__) + +__all__ = [ + 'AudioReader', +] + + +class AudioReader: + def __init__(self, sink: AudioSink, voice_client: VoiceRecvClient, *, after: Optional[AfterCB] = None): + if after is not None and not callable(after): + raise TypeError('Expected a callable for the "after" parameter.') + + self.sink: AudioSink = sink + self.voice_client: VoiceRecvClient = voice_client + self.after: Optional[AfterCB] = after + + # No need for the whole set_sink() call + self.sink._voice_client = voice_client + + self.active: bool = False + self.error: Optional[Exception] = None + self.packet_router: PacketRouter = PacketRouter(sink, self) + self.event_router: SinkEventRouter = SinkEventRouter(sink, self) + self.decryptor: PacketDecryptor = PacketDecryptor(voice_client.mode, bytes(voice_client.secret_key)) + self.speaking_timer: SpeakingTimer = SpeakingTimer(self) + self.keepalive: UDPKeepAlive = UDPKeepAlive(voice_client) + + def is_listening(self) -> bool: + return self.active + + def update_secret_key(self, secret_key: bytes) -> None: + self.decryptor.update_secret_key(secret_key) + + def start(self) -> None: + if self.active: + log.debug('Reader is already started', exc_info=True) + return + + self.speaking_timer.start() + self.event_router.start() + self.packet_router.start() + self.voice_client._connection.add_socket_listener(self.callback) + self.keepalive.start() + self.active = True + + def stop(self) -> None: + if not self.active: + log.debug('Tried to stop an inactive reader', exc_info=True) + return + + self.voice_client._connection.remove_socket_listener(self.callback) + self.active = False + self.speaking_timer.notify() + + threading.Thread(target=self._stop, name=f'audioreader-stopper-{id(self):x}').start() + + def _stop(self) -> None: + try: + self.packet_router.stop() + except Exception as e: + self.error = e + log.exception('Error stopping packet router') + + try: + self.event_router.stop() + except Exception as e: + self.error = e + log.exception('Error stopping event router') + + self.speaking_timer.stop() + self.keepalive.stop() + + if self.after: + try: + self.after(self.error) + except Exception: + log.exception('Error calling listener after function') + + for sink in self.sink.root.walk_children(with_self=True): + try: + sink.cleanup() + except Exception: + log.exception('Error calling cleanup() for %s', sink) + + def set_sink(self, sink: AudioSink) -> AudioSink: + """Sets the new sink for the reader and returns the old one. + Does not call cleanup() + """ + # This whole function is potentially very racy + old_sink = self.sink + old_sink._voice_client = None + sink._voice_client = self.voice_client + self.packet_router.set_sink(sink) + self.sink = sink + + return old_sink + + def _is_ip_discovery_packet(self, data: bytes) -> bool: + return len(data) == 74 and data[1] == 0x02 + + def callback(self, packet_data: bytes) -> None: + packet = rtp_packet = rtcp_packet = None + try: + if not rtp.is_rtcp(packet_data): + packet = rtp_packet = rtp.decode_rtp(packet_data) + packet.decrypted_data = self.decryptor.decrypt_rtp(packet) + else: + packet = rtcp_packet = rtp.decode_rtcp(self.decryptor.decrypt_rtcp(packet_data)) + + if not isinstance(packet, rtp.ReceiverReportPacket): + log.info("Received unexpected rtcp packet: type=%s, %s", packet.type, type(packet)) + log.debug("Packet info:\n packet=%s\n data=%s", packet, packet_data) + except CryptoError as e: + log.error("CryptoError decoding packet data") + log.debug("CryptoError details:\n data=%s\n secret_key=%s", packet_data, self.voice_client.secret_key) + return + except Exception as e: + if self._is_ip_discovery_packet(packet_data): + log.debug("Ignoring ip discovery packet") + return + + log.exception("Error unpacking packet") + log.debug("Packet data: len=%s data=%s", len(packet_data), packet_data) + finally: + if self.error: + self.stop() + return + if not packet: + return + + if rtcp_packet: + self.packet_router.feed_rtcp(rtcp_packet) + elif rtp_packet: + ssrc = rtp_packet.ssrc + + if ssrc not in self.voice_client._ssrc_to_id: + if rtp_packet.is_silence(): + # TODO: buffer packets from unknown ssrcs, 50 max? + # also remove this log later its pointless + log.debug("Skipping silence packet for unknown ssrc %s", ssrc) + return + else: + log.info("Received packet for unknown ssrc %s:\n%s", ssrc, rtp_packet) + + self.speaking_timer.notify(ssrc) + try: + self.packet_router.feed_rtp(rtp_packet) + except Exception as e: + log.exception('Error processing rtp packet') + self.error = e + self.stop() + + +class PacketDecryptor: + supported_modes: list[SupportedModes] = [ + 'aead_xchacha20_poly1305_rtpsize', + 'xsalsa20_poly1305_lite', + 'xsalsa20_poly1305_suffix', + 'xsalsa20_poly1305', + ] + + def __init__(self, mode: SupportedModes, secret_key: bytes) -> None: + self.mode: SupportedModes = mode + try: + self.decrypt_rtp: DecryptRTP = getattr(self, '_decrypt_rtp_' + mode) + self.decrypt_rtcp: DecryptRTCP = getattr(self, '_decrypt_rtcp_' + mode) + except AttributeError as e: + raise NotImplementedError(mode) from e + + self.box: EncryptionBox = self._make_box(secret_key) + + def _make_box(self, secret_key: bytes) -> EncryptionBox: + if self.mode.startswith("aead"): + return nacl.secret.Aead(secret_key) + else: + return nacl.secret.SecretBox(secret_key) + + def update_secret_key(self, secret_key: bytes) -> None: + self.box = self._make_box(secret_key) + + def _decrypt_rtp_xsalsa20_poly1305(self, packet: RTPPacket) -> bytes: + nonce = bytearray(24) + nonce[:12] = packet.header + result = self.box.decrypt(bytes(packet.data), bytes(nonce)) + + if packet.extended: + offset = packet.update_ext_headers(result) + result = result[offset:] + + return result + + def _decrypt_rtcp_xsalsa20_poly1305(self, data: bytes) -> bytes: + nonce = bytearray(24) + nonce[:8] = data[:8] + result = self.box.decrypt(data[8:], bytes(nonce)) + + return data[:8] + result + + def _decrypt_rtp_xsalsa20_poly1305_suffix(self, packet: RTPPacket) -> bytes: + nonce = packet.data[-24:] + voice_data = packet.data[:-24] + result = self.box.decrypt(bytes(voice_data), bytes(nonce)) + + if packet.extended: + offset = packet.update_ext_headers(result) + result = result[offset:] + + return result + + def _decrypt_rtcp_xsalsa20_poly1305_suffix(self, data: bytes) -> bytes: + nonce = data[-24:] + header = data[:8] + result = self.box.decrypt(data[8:-24], nonce) + + return header + result + + def _decrypt_rtp_xsalsa20_poly1305_lite(self, packet: RTPPacket) -> bytes: + nonce = bytearray(24) + nonce[:4] = packet.data[-4:] + voice_data = packet.data[:-4] + result = self.box.decrypt(bytes(voice_data), bytes(nonce)) + + if packet.extended: + offset = packet.update_ext_headers(result) + result = result[offset:] + + return result + + def _decrypt_rtcp_xsalsa20_poly1305_lite(self, data: bytes) -> bytes: + nonce = bytearray(24) + nonce[:4] = data[-4:] + header = data[:8] + result = self.box.decrypt(data[8:-4], bytes(nonce)) + + return header + result + + def _decrypt_rtp_aead_xchacha20_poly1305_rtpsize(self, packet: RTPPacket) -> bytes: + packet.adjust_rtpsize() + + nonce = bytearray(24) + nonce[:4] = packet.nonce + voice_data = packet.data + + # Blob vomit + assert isinstance(self.box, nacl.secret.Aead) + result = self.box.decrypt(bytes(voice_data), bytes(packet.header), bytes(nonce)) + + if packet.extended: + offset = packet.update_ext_headers(result) + result = result[offset:] + + return result + + def _decrypt_rtcp_aead_xchacha20_poly1305_rtpsize(self, data: bytes) -> bytes: + nonce = bytearray(24) + nonce[:4] = data[-4:] + header = data[:8] + + assert isinstance(self.box, nacl.secret.Aead) + result = self.box.decrypt(data[8:-4], bytes(header), bytes(nonce)) + + return header + result + + +class SpeakingTimer(threading.Thread): + def __init__(self, reader: AudioReader): + super().__init__(daemon=True, name=f'speaking-timer-{id(self):x}') + + self.reader: AudioReader = reader + self.voice_client = reader.voice_client + self.speaking_timeout_delay: float = 0.2 + self.last_speaking_state: Dict[int, bool] = {} + self.speaking_cache: Dict[int, float] = {} + self.speaking_timer_event: threading.Event = threading.Event() + self._end_thread: threading.Event = threading.Event() + + def _lookup_member(self, ssrc: int) -> Optional[Member]: + whoid = self.voice_client._get_id_from_ssrc(ssrc) + return self.voice_client.guild.get_member(whoid) if whoid else None + + def maybe_dispatch_speaking_start(self, ssrc: int) -> None: + tlast = self.speaking_cache.get(ssrc) + if tlast is None or tlast + self.speaking_timeout_delay < time.perf_counter(): + self.dispatch('voice_member_speaking_start', ssrc) + + def dispatch(self, event: SpeakingEvent, ssrc: int) -> None: + who = self._lookup_member(ssrc) + if not who: + return + self.voice_client.dispatch_sink(event, who) + + def notify(self, ssrc: Optional[int] = None) -> None: + if ssrc is not None: + self.last_speaking_state[ssrc] = True + self.maybe_dispatch_speaking_start(ssrc) + self.speaking_cache[ssrc] = time.perf_counter() + + self.speaking_timer_event.set() + self.speaking_timer_event.clear() + + def drop_ssrc(self, ssrc: int) -> None: + self.speaking_cache.pop(ssrc, None) + state = self.last_speaking_state.pop(ssrc, None) + if state: + self.dispatch('voice_member_speaking_stop', ssrc) + self.notify() + + def get_speaking(self, ssrc: int) -> Optional[bool]: + return self.last_speaking_state.get(ssrc) + + def stop(self) -> None: + self._end_thread.set() + self.notify() + + def run(self) -> None: + _i1 = itemgetter(1) + + def get_next_entry(): + cache = sorted(self.speaking_cache.items(), key=_i1) + for ssrc, tlast in cache: + # only return pair if speaking + if self.last_speaking_state.get(ssrc): + return ssrc, tlast + + return None, None + + self.speaking_timer_event.wait() + while not self._end_thread.is_set(): + if not self.speaking_cache: + self.speaking_timer_event.wait() + + tnow = time.perf_counter() + ssrc, tlast = get_next_entry() + + # no ssrc has been speaking, nothing to timeout + if ssrc is None or tlast is None: + self.speaking_timer_event.wait() + continue + + self.speaking_timer_event.wait(tlast + self.speaking_timeout_delay - tnow) + + if time.perf_counter() < tlast + self.speaking_timeout_delay: + continue + + self.dispatch('voice_member_speaking_stop', ssrc) + self.last_speaking_state[ssrc] = False + + +# TODO: unify into a single thread that does all keepalives +class UDPKeepAlive(threading.Thread): + delay: int = 5000 + + def __init__(self, voice_client: VoiceRecvClient): + super().__init__(daemon=True, name=f"voice-udp-keepalive-{id(self):x}") + + self.voice_client: VoiceRecvClient = voice_client + + self.last_time: float = 0 + self.counter: int = 0 + self._end_thread: threading.Event = threading.Event() + + def run(self) -> None: + self.voice_client.wait_until_connected() + + while not self._end_thread.is_set(): + vc = self.voice_client + try: + packet = self.counter.to_bytes(8, 'big') + except OverflowError: + self.counter = 0 + continue + + try: + vc._connection.socket.sendto(packet, (vc._connection.endpoint_ip, vc._connection.voice_port)) + except Exception as e: + log.debug("Error sending keepalive to socket: %s: %s", e.__class__.__name__, e) + # TODO: test connection interruptions + vc.wait_until_connected() + if vc.is_connected(): + continue + break + else: + self.counter += 1 + time.sleep(self.delay) + + def stop(self) -> None: + self._end_thread.set() diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/router.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/router.py new file mode 100644 index 0000000..d52a733 --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/router.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import queue +import logging +import threading + +from collections import deque + +from .utils import MultiDataEvent +from .opus import PacketDecoder + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Tuple, Dict, List, Callable, Any, Optional + from .rtp import RTPPacket, RTCPPacket + from .sinks import AudioSink + from .reader import AudioReader + + EventCB = Callable[..., Any] + EventData = Tuple[str, Tuple[Any, ...], Dict[str, Any]] + +log = logging.getLogger(__name__) + + +class PacketRouter(threading.Thread): + def __init__(self, sink: AudioSink, reader: AudioReader): + super().__init__(daemon=True, name=f"packet-router-{id(self):x}") + + self.sink: AudioSink = sink + self.decoders: Dict[int, PacketDecoder] = {} + self.reader: AudioReader = reader + self.waiter: MultiDataEvent[PacketDecoder] = MultiDataEvent() + + self._lock: threading.RLock = threading.RLock() + self._end_thread: threading.Event = threading.Event() + self._dropped_ssrcs: deque[int] = deque(maxlen=16) + + def feed_rtp(self, packet: RTPPacket) -> None: + # TODO: stale packet check + + if packet.ssrc in self._dropped_ssrcs: + log.debug("Ignoring packet from dropped ssrc %s", packet.ssrc) + return + + with self._lock: + decoder = self.get_decoder(packet.ssrc) + if decoder is not None: + decoder.push_packet(packet) + + def feed_rtcp(self, packet: RTCPPacket) -> None: + guild = self.sink.voice_client.guild if self.sink.voice_client else None + event_router = self.reader.event_router + event_router.dispatch('rtcp_packet', packet, guild) + + def get_decoder(self, ssrc: int) -> Optional[PacketDecoder]: + with self._lock: + decoder = self.decoders.get(ssrc) + if decoder is None: + decoder = self.decoders[ssrc] = PacketDecoder(self, ssrc) + + return decoder + + def set_sink(self, sink: AudioSink) -> None: + with self._lock: + self.sink = sink + + def set_user_id(self, ssrc: int, user_id: int) -> None: + with self._lock: + if ssrc in self._dropped_ssrcs: + self._dropped_ssrcs.remove(ssrc) + + decoder = self.decoders.get(ssrc) + + if decoder is not None: + decoder.set_user_id(user_id) + + def destroy_decoder(self, ssrc: int) -> None: + with self._lock: + decoder = self.decoders.pop(ssrc, None) + if decoder is not None: + self._dropped_ssrcs.append(ssrc) + decoder.destroy() + + def destroy_all_decoders(self) -> None: + with self._lock: + for ssrc in list(self.decoders.keys()): + self.destroy_decoder(ssrc) + + def stop(self) -> None: + self._end_thread.set() + self.waiter.notify() + + def run(self) -> None: + try: + self._do_run() + except Exception as e: + log.exception("Error in %s loop", self) + self.reader.error = e + finally: + self.reader.voice_client.stop_listening() + self.waiter.clear() + + def _do_run(self) -> None: + while not self._end_thread.is_set(): + self.waiter.wait() + with self._lock: + for decoder in self.waiter.items: + data = decoder.pop_data() + if data is not None: + self.sink.write(data.source, data) + + +class SinkEventRouter(threading.Thread): + def __init__(self, sink: AudioSink, reader: AudioReader): + super().__init__(daemon=True, name=f"sink-event-router-{id(self):x}") + + self.sink: AudioSink = sink + self.reader: AudioReader = reader + + self._event_listeners: Dict[str, List[EventCB]] = {} + self._buffer: queue.SimpleQueue[EventData] = queue.SimpleQueue() + self._lock = threading.RLock() + self._end_thread: threading.Event = threading.Event() + + self.register_events() + + def dispatch(self, event: str, /, *args: Any, **kwargs: Any) -> None: + log.debug("Dispatching voice_client event %s", event) + self._buffer.put_nowait((event, args, kwargs)) + + def set_sink(self, sink: AudioSink) -> None: + with self._lock: + self.unregister_events() + self.sink = sink + self.register_events() + + def register_events(self) -> None: + with self._lock: + self._register_listeners(self.sink) + for child in self.sink.walk_children(): + self._register_listeners(child) + + def unregister_events(self) -> None: + with self._lock: + self._unregister_listeners(self.sink) + for child in self.sink.walk_children(): + self._unregister_listeners(child) + + def _register_listeners(self, sink: AudioSink) -> None: + log.debug("Registering events for %s: %s ", sink, sink.__sink_listeners__) + + for name, method_name in sink.__sink_listeners__: + func = getattr(sink, method_name) + + log.debug("Registering event: %r, func: %r", name, method_name) + if name in self._event_listeners: + self._event_listeners[name].append(func) + else: + self._event_listeners[name] = [func] + + def _unregister_listeners(self, sink: AudioSink): + for name, method_name in sink.__sink_listeners__: + func = getattr(sink, method_name) + + if name in self._event_listeners: + try: + self._event_listeners[name].remove(func) + except ValueError: + pass + + def _dispatch_to_listeners(self, event: str, *args: Any, **kwargs: Any) -> None: + for listener in self._event_listeners.get(f'on_{event}', []): + try: + listener(*args, **kwargs) + except Exception: + log.exception("Unhandled exception dispatching voice listener event %r", event) + log.debug("event=%r, args=%r, kwargs=%r, listener=%r", event, args, kwargs, listener) + + def stop(self) -> None: + self._end_thread.set() + + def run(self) -> None: + try: + self._do_run() + except Exception as e: + log.exception("Error in %s", self.name) + self.reader.error = e + self.reader.voice_client.stop_listening() + + def _do_run(self) -> None: + while not self._end_thread.is_set(): + try: + event, args, kwargs = self._buffer.get(timeout=0.5) + except queue.Empty: + continue + else: + with self._lock: + # this looks dumb + with self.reader.packet_router._lock: + self._dispatch_to_listeners(event, *args, **kwargs) diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/rtp.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/rtp.py new file mode 100644 index 0000000..3f3c71b --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/rtp.py @@ -0,0 +1,471 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import struct +import logging + +from math import ceil +from collections import namedtuple + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Optional, Literal, Union, Final, Dict, Any, Tuple + + AudioPacket = Union['RTPPacket', 'FakePacket', 'SilencePacket'] + RealPacket = Union['RTPPacket', 'RTCPPacket'] + Packet = Union[RealPacket, 'FakePacket', 'SilencePacket'] + + PacketTypes = Union[ + 'SenderReportPacket', + 'ReceiverReportPacket', + 'SDESPacket', + 'BYEPacket', + 'APPPacket', + ] + +log = logging.getLogger(__name__) + +__all__ = [ + 'RTPPacket', + 'RTCPPacket', + 'FakePacket', + 'SilencePacket', + 'ExtensionID', +] + +OPUS_SILENCE: Final = b'\xf8\xff\xfe' + + +class ExtensionID: + audio_power: Final = 1 + speaking_state: Final = 9 + + +def decode(data: bytes) -> RealPacket: + """Creates an :class:`RTPPacket` or an :class:`RTCPPacket`. + + Parameters + ----------- + data : bytes + The raw packet data. + """ + + # While technically unreliable, discord RTP packets (should) + # always be distinguishable from RTCP packets. RTCP packets + # should always have 200-204 as their second byte, while RTP + # packet are (probably) always 73 (or at least not 200-204). + + # check version bits + if not data[0] >> 6 == 2: + raise ValueError(f'Invalid packet header 0b{data[0]:0>8b}') + return _rtcp_map.get(data[1], RTPPacket)(data) + + +def decode_rtp(data: bytes) -> RTPPacket: + return decode(data) # type: ignore + + +def decode_rtcp(data: bytes) -> RTCPPacket: + return decode(data) # type: ignore + + +def is_rtcp(data: bytes) -> bool: + return 200 <= data[1] <= 204 + + +def _parse_low(x: int, bitlen: int = 32) -> float: + return x / 2.0**bitlen + + +def _into_low(x: float, bitlen: int = 32) -> int: + return int(x * 2.0**bitlen) + + +class _PacketCmpMixin: + __slots__ = ('ssrc', 'sequence', 'timestamp') + + def __lt__(self, other: _PacketCmpMixin) -> bool: + if self.ssrc != other.ssrc: + raise TypeError("packet ssrc mismatch (%s, %s)" % (self.ssrc, other.ssrc)) + return self.sequence < other.sequence and self.timestamp < other.timestamp + + def __gt__(self, other: _PacketCmpMixin) -> bool: + if self.ssrc != other.ssrc: + raise TypeError("packet ssrc mismatch (%s, %s)" % (self.ssrc, other.ssrc)) + return self.sequence > other.sequence or self.timestamp > other.timestamp + + def __eq__(self, other: _PacketCmpMixin) -> bool: + if self.ssrc != other.ssrc: + return False + return self.sequence == other.sequence and self.timestamp == other.timestamp + + def is_silence(self) -> bool: + data = getattr(self, 'decrypted_data', None) + return data == OPUS_SILENCE + + +class FakePacket(_PacketCmpMixin): + __slots__ = ('ssrc', 'sequence', 'timestamp') + decrypted_data: bytes = b'' + extension_data: dict = {} + + def __init__(self, ssrc: int, sequence: int, timestamp: int): + self.ssrc: int = ssrc + self.sequence: int = sequence + self.timestamp: int = timestamp + + def __repr__(self) -> str: + return ''.format(self) + + def __bool__(self) -> Literal[False]: + return False + + +class SilencePacket(_PacketCmpMixin): + __slots__ = ('ssrc', 'timestamp') + decrypted_data: Final = OPUS_SILENCE + extension_data: Final[Dict[int, Any]] = {} + sequence: int = -1 + + def __init__(self, ssrc: int, timestamp: int): + self.ssrc: int = ssrc + self.timestamp: int = timestamp + + def __repr__(self) -> str: + return ''.format(self) + + def is_silence(self) -> bool: + return True + + +class RTPPacket(_PacketCmpMixin): + __slots__ = ( + 'version', + 'padding', + 'extended', + 'cc', + 'marker', + 'payload', + 'sequence', + 'timestamp', + 'ssrc', + 'csrcs', + 'header', + 'data', + 'decrypted_data', + 'nonce', + 'extension', + 'extension_data', + '_rtpsize', + ) + + _hstruct = struct.Struct('>xxHII') + _ext_header = namedtuple("Extension", 'profile length values') + _ext_magic = b'\xbe\xde' + + def __init__(self, data: bytes): + data = bytearray(data) # type: ignore + + # fmt: off + self.version: int = data[0] >> 6 + self.padding: bool = bool(data[0] & 0b00100000) + self.extended: bool = bool(data[0] & 0b00010000) + self.cc: int = data[0] & 0b00001111 + + self.marker: bool = bool(data[1] & 0b10000000) + self.payload: int = data[1] & 0b01111111 + # fmt: on + + sequence, timestamp, ssrc = self._hstruct.unpack_from(data) + self.sequence: int = sequence + self.timestamp: int = timestamp + self.ssrc: int = ssrc + + self.csrcs: Tuple[int, ...] = () + self.extension = None + self.extension_data: Dict[int, bytes] = {} + + self.header = data[:12] + self.data = data[12:] + self.decrypted_data: Optional[bytes] = None + + self.nonce: bytes = b'' + self._rtpsize: bool = False + + if self.cc: + fmt = '>%sI' % self.cc + offset = struct.calcsize(fmt) + 12 + self.csrcs = struct.unpack(fmt, data[12:offset]) + self.data = data[offset:] + + # TODO?: impl padding calculations (though discord doesn't seem to use that bit) + + def adjust_rtpsize(self): + """Adjusts the packet header and data based on the rtpsize format.""" + + self._rtpsize = True + self.nonce = self.data[-4:] + + if not self.extended: + self.data = self.data[:-4] + return + + # rtpsize based formats are laid out similarly to SRTP packets, which includes the ext header now + # the nonce also needs to be removed from the end + self.header += self.data[:4] + self.data = self.data[4:-4] + + def update_ext_headers(self, data: bytes) -> int: + """Adds extended header data to this packet, returns payload offset""" + + if not self.extended: + return 0 + + # rtpsize formats have the extension header in the rtp header instead of payload + if self._rtpsize: + data = self.header[-4:] + data + + # data is the decrypted packet payload containing the extension header and opus data + profile, length = struct.unpack_from('>2sH', data) + + if profile == self._ext_magic: + self._parse_bede_header(data, length) + + values = struct.unpack('>%sI' % length, data[4 : 4 + length * 4]) + self.extension = self._ext_header(profile, length, values) + + offset = 4 + length * 4 + if self._rtpsize: + # remove the extra offset from adding the header in + offset -= 4 + + return offset + + # https://www.rfcreader.com/#rfc5285_line186 + def _parse_bede_header(self, data: bytes, length: int) -> None: + offset = 4 + n = 0 + + while n < length: + next_byte = data[offset : offset + 1] + + if next_byte == b'\x00': + offset += 1 + continue + + header = struct.unpack('>B', next_byte)[0] + + element_id = header >> 4 + element_len = 1 + (header & 0b0000_1111) + + self.extension_data[element_id] = data[offset + 1 : offset + 1 + element_len] + offset += 1 + element_len + n += 1 + + def _dump_info(self) -> str: + attrs = {name: getattr(self, name) for name in self.__slots__} + return ''.join(("')) + + def __repr__(self) -> str: + return ( + ''.format(self, len(self.data), set(self.extension_data)) + ) + + +# http://www.rfcreader.com/#rfc3550_line855 +class RTCPPacket: + __slots__ = ('version', 'padding', 'length') + _header = struct.Struct('>BBH') + _ssrc_fmt = struct.Struct('>I') + type = None + + def __init__(self, data: bytes): + self.length: int + head, _, self.length = self._header.unpack_from(data) + self.version: int = head >> 6 + self.padding: bool = bool(head & 0b00100000) + # dubious, yet devious + setattr(self, self.__slots__[0], head & 0b00011111) + + def __repr__(self) -> str: + content = ', '.join("{}: {}".format(k, getattr(self, k, None)) for k in self.__slots__) + return "<{} {}>".format(self.__class__.__name__, content) + + @classmethod + def from_data(cls, data: bytes) -> PacketTypes: + _, ptype, _ = cls._header.unpack_from(data) + return _rtcp_map[ptype](data) + + +# TODO?: consider moving repeated code to a ReportPacket type +# http://www.rfcreader.com/#rfc3550_line1614 +class SenderReportPacket(RTCPPacket): + __slots__ = ('report_count', 'ssrc', 'info', 'reports', 'extension') + _info_fmt = struct.Struct('>5I') + _report_fmt = struct.Struct('>IB3x4I') + _24bit_int_fmt = struct.Struct('>4xI') + _info = namedtuple('RRSenderInfo', 'ntp_ts rtp_ts packet_count octet_count') + _report = namedtuple("RReport", 'ssrc perc_loss total_lost last_seq jitter lsr dlsr') + type = 200 + + def __init__(self, data): + super().__init__(data) + self.ssrc = self._ssrc_fmt.unpack_from(data, 4)[0] + self.info = self._read_sender_info(data, 8) + + reports = [] + for x in range(self.report_count): + offset = 28 + 24 * x + reports.append(self._read_report(data, offset)) + + self.reports = tuple(reports) + + self.extension = None + if len(data) > 28 + 24 * self.report_count: + self.extension = data[28 + 24 * self.report_count :] + + def _read_sender_info(self, data, offset): + nhigh, nlow, rtp_ts, pcount, ocount = self._info_fmt.unpack_from(data, offset) + ntotal = nhigh + _parse_low(nlow) + return self._info(ntotal, rtp_ts, pcount, ocount) + + def _read_report(self, data, offset): + ssrc, flost, seq, jit, lsr, dlsr = self._report_fmt.unpack_from(data, offset) + clost = self._24bit_int_fmt.unpack_from(data, offset)[0] & 0xFFFFFF + return self._report(ssrc, flost, clost, seq, jit, lsr, dlsr) + + +# http://www.rfcreader.com/#rfc3550_line1879 +class ReceiverReportPacket(RTCPPacket): + __slots__ = ('report_count', 'ssrc', 'reports', 'extension') + _report_fmt = struct.Struct('>IB3x4I') + _24bit_int_fmt = struct.Struct('>4xI') + _report = namedtuple("RReport", 'ssrc perc_loss total_lost last_seq jitter lsr dlsr') + type = 201 + + reports: Tuple[_report, ...] + + def __init__(self, data: bytes): + super().__init__(data) + self.ssrc: int = self._ssrc_fmt.unpack_from(data, 4)[0] + + reports = [] + for x in range(self.report_count): + offset = 8 + 24 * x + reports.append(self._read_report(data, offset)) + + self.reports = tuple(reports) + + self.extension: Optional[bytes] = None + if len(data) > 8 + 24 * self.report_count: + self.extension = data[8 + 24 * self.report_count :] + + def _read_report(self, data: bytes, offset: int) -> _report: + ssrc, flost, seq, jit, lsr, dlsr = self._report_fmt.unpack_from(data, offset) + clost = self._24bit_int_fmt.unpack_from(data, offset)[0] & 0xFFFFFF + return self._report(ssrc, flost, clost, seq, jit, lsr, dlsr) + + +# UNFORTUNATELY it seems discord only uses the above ~~two packet types~~ packet type. +# Good thing I knew that when I made the rest of these. Haha yes. + + +# http://www.rfcreader.com/#rfc3550_line2024 +class SDESPacket(RTCPPacket): + __slots__ = ('source_count', 'chunks', '_pos') + _item_header = struct.Struct('>BB') + _chunk = namedtuple("SDESChunk", 'ssrc items') + _item = namedtuple("SDESItem", 'type size length text') + type = 202 + + def __init__(self, data): + super().__init__(data) + _chunks = [] + self._pos = 4 + + for _ in range(self.source_count): + _chunks.append(self._read_chunk(data)) + + self.chunks = tuple(_chunks) + + def _read_chunk(self, data): + ssrc = self._ssrc_fmt.unpack_from(data, self._pos)[0] + self._pos += 4 + + # check for chunk with no items + if data[self._pos : self._pos + 4] == b'\x00\x00\x00\x00': + self._pos += 4 + return self._chunk(ssrc, ()) + + items = [self._read_item(data)] + + # Read items until END type is found + while items[-1].type != 0: + items.append(self._read_item(data)) + + # pad chunk to 4 bytes + if self._pos % 4: + self._pos = ceil(self._pos / 4) * 4 + + return self._chunk(ssrc, items) + + def _read_item(self, data): + itype, ilen = self._item_header.unpack_from(data, self._pos) + self._pos += 2 + text = None + + if ilen: + text = data[self._pos : self._pos + ilen].decode() + self._pos += ilen + + return self._item(itype, ilen + 2, ilen, text) + + def _get_chunk_size(self, chunk): + return 4 + max(4, sum(i.size for i in chunk.items)) # + padding? + + +# http://www.rfcreader.com/#rfc3550_line2311 +class BYEPacket(RTCPPacket): + __slots__ = ('source_count', 'ssrcs', 'reason') + type = 203 + + def __init__(self, data): + super().__init__(data) + self.ssrcs = struct.unpack_from('>%sI' % self.source_count, data, 4) + self.reason = None + + body_length = 4 + len(self.ssrcs) * 4 + if len(data) > body_length: + extra_len = struct.unpack_from('B', data, body_length)[0] + reason = struct.unpack_from('%ss' % extra_len, data, body_length + 1) + self.reason = reason.decode() + + +# http://www.rfcreader.com/#rfc3550_line2353 +class APPPacket(RTCPPacket): + __slots__ = ('subtype', 'ssrc', 'name', 'data') + _packet_info = struct.Struct('>I4s') + type = 204 + + def __init__(self, data): + super().__init__(data) + self.ssrc, name = self._packet_info.unpack_from(data, 4) + self.name = name.decode('ascii') + self.data = data[12:] # should be a multiple of 32 bits but idc + + +_rtcp_map = { + 200: SenderReportPacket, + 201: ReceiverReportPacket, + 202: SDESPacket, + 203: BYEPacket, + 204: APPPacket, +} diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/silence.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/silence.py new file mode 100644 index 0000000..1cdaa06 --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/silence.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import time +import logging +import threading + +from .opus import VoiceData +from .rtp import SilencePacket + +from discord.utils import MISSING +from discord.opus import Decoder + +from typing import TYPE_CHECKING, Tuple + +if TYPE_CHECKING: + from typing import Callable, Any, Dict, Optional, Final, Union + from .rtp import AudioPacket + from .types import MemberOrUser as User + + SilenceGenFN = Callable[[Optional[User], VoiceData], Any] + SSRCData = Tuple[float, Optional[User], AudioPacket] + +log = logging.getLogger(__name__) + +__all__ = [ + 'SilenceGenerator', +] + +SILENCE_PCM: Final = b'\0' * Decoder.FRAME_SIZE +PACKET_INTERVAL: Final = 0.02 + + +class SilenceGenerator(threading.Thread): + """Generates and sends silence packets.""" + + def __init__(self, callback: SilenceGenFN, *, grace_period: float = 0.015): + super().__init__(daemon=True, name=f'silencegen-{id(self):x}') + self.callback: SilenceGenFN = callback + self.grace_period: float = grace_period + + self._ssrc_data: Dict[int, SSRCData] = {} # {ssrc: (time, _, _)} + self._last_timestamp: Dict[int, int] = {} # {ssrc: timestamp} + self._user_map_backup: Dict[int, int] = {} # {id: ssrc} + self._end: threading.Event = threading.Event() + self._has_data: threading.Event = threading.Event() + self._lock: threading.Lock = threading.Lock() + + def push(self, user: Optional[User], packet: AudioPacket) -> None: + """Updates the last time a packet was received and from whom. + Calling this function will start generating silence packets for `packet.ssrc` + until `drop(ssrc)` or `stop()` is called. + """ + + with self._lock: + self._ssrc_data[packet.ssrc] = (time.perf_counter(), user, packet) + self._last_timestamp[packet.ssrc] = packet.timestamp + + if user: + self._user_map_backup[user.id] = packet.ssrc + + self._has_data.set() + + def _get_next_info(self) -> SSRCData: + return min(self._ssrc_data.values()) + + def drop(self, *, ssrc: Optional[int] = None, user: User = MISSING) -> None: + """Stop generating silence packets for `ssrc`, or whatever is cached for `user` + if `ssrc` is None, if any. + """ + + with self._lock: + if ssrc is None: + ssrc = self._user_map_backup.pop(user.id, None) + if ssrc is None: + return # weird but ok + + self._last_timestamp.pop(ssrc, None) + last_data = self._ssrc_data.pop(ssrc, None) + if last_data is None and user is not MISSING: + ssrc = self._user_map_backup.pop(user.id) + self._ssrc_data.pop(ssrc, None) + + if not self._ssrc_data: + self._has_data.clear() + + def stop(self) -> None: + """Stops generating silence for everything and clears the cache.""" + + self._end.set() + self._has_data.set() + + with self._lock: + self._ssrc_data.clear() + self._user_map_backup.clear() + self._last_timestamp.clear() + self._has_data.clear() + + self.join(1) + + def start(self) -> None: + self._end.clear() + super().start() + + def run(self) -> None: + try: + self._do_run() + except Exception as e: + log.exception("Error in %s", self) + + def _do_run(self) -> None: + while not self._end.is_set(): + self._has_data.wait() + if self._end.is_set(): + return + + with self._lock: + tlast, user, packet = self._get_next_info() + ssrc = packet.ssrc + + # prepare the object before the sleep as a little micro optimization + next_packet = SilencePacket( + ssrc, self._last_timestamp.get(ssrc, packet.timestamp) + Decoder.SAMPLES_PER_FRAME + ) + # TODO: check if destination wants opus or not + next_data = VoiceData(next_packet, user, pcm=SILENCE_PCM) + + tnext = tlast + PACKET_INTERVAL + tnow = time.perf_counter() + # wait a little bit longer than when the next one should be + # so we don't have to race with the next packet + delay = tnext + self.grace_period - tnow + + if delay > 0: + time.sleep(delay) + + with self._lock: + tlast2, luser, lpacket = self._ssrc_data.get(ssrc, (-1, None, packet)) + + if next_packet.ssrc != lpacket.ssrc or tlast != tlast2 or self._end.is_set(): + continue # another packet came in and bumped up the time + + next_data.source = luser # is there any point in doing this? + self.callback(luser, next_data) + + with self._lock: + # If there was no packet update during the sleep... + if tlast == tlast2 and ssrc in self._ssrc_data: + # update the existing packet time for the next window + self._ssrc_data[ssrc] = (tlast + PACKET_INTERVAL, user, packet) + self._last_timestamp[ssrc] += Decoder.SAMPLES_PER_FRAME diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/sinks.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/sinks.py new file mode 100644 index 0000000..ddaa278 --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/sinks.py @@ -0,0 +1,634 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import io +import abc +import time +import wave +import shlex +import inspect +import audioop +import logging +import threading +import subprocess + +from .opus import VoiceData +from .silence import SilenceGenerator + +import discord + +from discord.utils import MISSING, SequenceProxy +from discord.opus import Decoder as OpusDecoder + +from typing import TYPE_CHECKING, overload + +if TYPE_CHECKING: + from typing import Callable, Optional, Any, IO, Sequence, Tuple, Generator, Union, Dict, List + + from .rtp import AudioPacket, RTCPPacket + from .voice_client import VoiceRecvClient + from .opus import VoiceData + from .types import MemberOrUser as User + + BasicSinkWriteCB = Callable[[Optional[User], VoiceData], Any] + BasicSinkWriteRTCPCB = Callable[[RTCPPacket], Any] + ConditionalFilterFn = Callable[[Optional[User], VoiceData], bool] + FFmpegErrorCB = Callable[['FFmpegSink', Exception, Optional[VoiceData]], Any] + + +log = logging.getLogger(__name__) + +__all__ = [ + 'AudioSink', + 'MultiAudioSink', + 'BasicSink', + 'WaveSink', + 'FFmpegSink', + 'PCMVolumeTransformer', + 'ConditionalFilter', + 'TimedFilter', + 'UserFilter', + 'SilenceGeneratorSink', +] + + +# TODO: use this in more places +class VoiceRecvException(discord.DiscordException): + """Generic exception for voice recv related errors""" + + def __init__(self, message: str): + self.message: str = message + + +class SinkMeta(abc.ABCMeta): + __sink_listeners__: List[Tuple[str, str]] + + def __new__(cls, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any], **kwargs): + listeners: Dict[str, Any] = {} + new_cls = super().__new__(cls, name, bases, attrs, **kwargs) + + for base in reversed(new_cls.__mro__): + for elem, value in base.__dict__.items(): + # If it exists in a subclass, delete the higher level one + if elem in listeners: + del listeners[elem] + + is_static_method = isinstance(value, staticmethod) + if is_static_method: + value = value.__func__ + + if not hasattr(value, '__sink_listener__'): + continue + + listeners[elem] = value + + listener_list = [] + for listener in listeners.values(): + for listener_name in listener.__sink_listener_names__: + listener_list.append((listener_name, listener.__name__)) + + new_cls.__sink_listeners__ = listener_list + return new_cls + + +class SinkABC(metaclass=SinkMeta): + __sink_listeners__: List[Tuple[str, str]] + + @property + @abc.abstractmethod + def root(self) -> AudioSink: + raise NotImplementedError + + @property + @abc.abstractmethod + def parent(self) -> Optional[AudioSink]: + raise NotImplementedError + + @property + @abc.abstractmethod + def child(self) -> Optional[AudioSink]: + raise NotImplementedError + + @property + @abc.abstractmethod + def children(self) -> Sequence[AudioSink]: + raise NotImplementedError + + @property + @abc.abstractmethod + def voice_client(self) -> Optional[VoiceRecvClient]: + raise NotImplementedError + + # handling opus vs pcm is not strictly mutually exclusive + # a sink could handle both but idk about that pattern + @abc.abstractmethod + def wants_opus(self) -> bool: + """If sink handles opus data""" + raise NotImplementedError + + @abc.abstractmethod + def write(self, user: Optional[User], data: VoiceData): + """Callback for when the sink receives data""" + raise NotImplementedError + + @abc.abstractmethod + def cleanup(self): + raise NotImplementedError + + @abc.abstractmethod + def _register_child(self, child: AudioSink) -> None: + raise NotImplementedError + + +class AudioSink(SinkABC): + _voice_client: Optional[VoiceRecvClient] + _parent: Optional[AudioSink] = None + _child: Optional[AudioSink] = None + + def __init__(self, destination: Optional[AudioSink] = None, /): + if destination is not None: + self._register_child(destination) + else: + self._child = None + + def __del__(self): + self.cleanup() + + def _register_child(self, child: AudioSink) -> None: + if child in self.root.walk_children(): + raise RuntimeError('Sink is already registered.') + + self._child = child + child._parent = self + + @property + def root(self) -> AudioSink: + if self.parent is None: + return self + + return self.parent.root + + @property + def parent(self) -> Optional[AudioSink]: + return self._parent + + @property + def child(self) -> Optional[AudioSink]: + return self._child + + @property + def children(self) -> Sequence[AudioSink]: + return [self._child] if self._child else [] + + @property + def voice_client(self) -> Optional[VoiceRecvClient]: + """Guaranteed to not be None inside write()""" + + if self.parent is not None: + return self.parent.voice_client + else: + return self._voice_client + + @property + def client(self) -> Optional[discord.Client]: + """Guaranteed to not be None inside write()""" + return self.voice_client and self.voice_client.client + + def walk_children(self, *, with_self: bool = False) -> Generator[AudioSink, None, None]: + """Returns a generator of all the children of this sink, recursively, depth first.""" + + if with_self: + yield self + + for child in self.children: + yield child + yield from child.walk_children() + + @classmethod + def listener(cls, name: str = MISSING): + """Marks a function as an event listener.""" + + if name is not MISSING and not isinstance(name, str): + raise TypeError(f'AudioSink.listener expected str but received {type(name).__name__} instead.') + + def decorator(func): + actual = func + + if isinstance(actual, staticmethod): + actual = actual.__func__ + + if inspect.iscoroutinefunction(actual): + raise TypeError('Listener function must not be a coroutine function.') + + actual.__sink_listener__ = True + to_assign = name or actual.__name__ + + try: + actual.__sink_listener_names__.append(to_assign) + except AttributeError: + actual.__sink_listener_names__ = [to_assign] + + return func + + return decorator + + +class MultiAudioSink(AudioSink): + def __init__(self, destinations: Sequence[AudioSink], /): + # Intentionally not calling super().__init__ here + if destinations is not None: + for dest in destinations: + self._register_child(dest) + + self._children: List[AudioSink] = list(destinations) + + def _register_child(self, child: AudioSink) -> None: + if child in self.root.walk_children(): + raise RuntimeError('Sink is already registered.') + + child._parent = self + + @property + def child(self) -> Optional[AudioSink]: + return self._children[0] if self._children else None + + @property + def children(self) -> Sequence[AudioSink]: + return SequenceProxy(self._children) + + # TODO: add functions to add/remove children? + + +class BasicSink(AudioSink): + """Simple callback based sink.""" + + def __init__( + self, + event: BasicSinkWriteCB, + *, + rtcp_event: Optional[BasicSinkWriteRTCPCB] = None, + decode: bool = True, + ): + super().__init__() + + self.cb = event + self.cb_rtcp = rtcp_event + self.decode = decode + + def wants_opus(self) -> bool: + return not self.decode + + def write(self, user: Optional[User], data: VoiceData) -> None: + self.cb(user, data) + + @AudioSink.listener() + def on_rtcp_packet(self, packet: RTCPPacket, guild: discord.Guild) -> None: + self.cb_rtcp(packet) if self.cb_rtcp else None + + def cleanup(self) -> None: + pass + + +class WaveSink(AudioSink): + """Endpoint AudioSink that generates a wav file. + Best used in conjunction with a silence generating sink. (TBD) + """ + + CHANNELS = OpusDecoder.CHANNELS + SAMPLE_WIDTH = OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS + SAMPLING_RATE = OpusDecoder.SAMPLING_RATE + + def __init__(self, destination: wave._File): + super().__init__() + + self._file: wave.Wave_write = wave.open(destination, 'wb') + self._file.setnchannels(self.CHANNELS) + self._file.setsampwidth(self.SAMPLE_WIDTH) + self._file.setframerate(self.SAMPLING_RATE) + + def wants_opus(self) -> bool: + return False + + def write(self, user: Optional[User], data: VoiceData) -> None: + self._file.writeframes(data.pcm) + + def cleanup(self) -> None: + try: + self._file.close() + except Exception: + log.warning("WaveSink got error closing file on cleanup", exc_info=True) + + +WavSink = WaveSink + + +class FFmpegSink(AudioSink): + @overload + def __init__( + self, + *, + filename: str, + executable: str = 'ffmpeg', + stderr: Optional[IO[bytes]] = None, + before_options: Optional[str] = None, + options: Optional[str] = None, + on_error: Optional[FFmpegErrorCB] = None, + ): ... + + @overload + def __init__( + self, + *, + buffer: IO[bytes], + executable: str = 'ffmpeg', + stderr: Optional[IO[bytes]] = None, + before_options: Optional[str] = None, + options: Optional[str] = None, + on_error: Optional[FFmpegErrorCB] = None, + ): ... + + def __init__( + self, + *, + filename: str = MISSING, + buffer: IO[bytes] = MISSING, + executable: str = 'ffmpeg', + stderr: Optional[IO[bytes]] = None, + before_options: Optional[str] = None, + options: Optional[str] = None, + on_error: Optional[FFmpegErrorCB] = None, + ): + super().__init__() + + self.filename: str = filename or 'pipe:1' + self.buffer: IO[bytes] = buffer + self.on_error: FFmpegErrorCB = on_error or self._on_error + + args = [executable, '-hide_banner'] + subprocess_kwargs: Dict[str, Any] = {'stdin': subprocess.PIPE} + if self.buffer is not MISSING: + subprocess_kwargs['stdout'] = subprocess.PIPE + + piping_stderr = False + if stderr is not None: + try: + stderr.fileno() + except Exception: + piping_stderr = True + subprocess_kwargs['stderr'] = subprocess.PIPE + + if isinstance(before_options, str): + args.extend(shlex.split(before_options)) + + # fmt: off + args.extend(( + '-f', 's16le', + '-ar', '48000', + '-ac', '2', + '-i', 'pipe:0', + '-loglevel', 'warning', + '-blocksize', str(discord.FFmpegAudio.BLOCKSIZE) + )) + # fmt: on + + if isinstance(options, str): + args.extend(shlex.split(options)) + + args.append(self.filename) + + self._process: subprocess.Popen = MISSING + self._process = self._spawn_process(args, **subprocess_kwargs) + + self._stdin: IO[bytes] = self._process.stdin # type: ignore + self._stdout: Optional[IO[bytes]] = None + self._stderr: Optional[IO[bytes]] = None + self._stdout_reader_thread: Optional[threading.Thread] = None + self._stderr_reader_thread: Optional[threading.Thread] = None + + if self.buffer: + n = f'popen-stout-reader:pid-{self._process.pid}' + self._stdout = self._process.stdout + _args = (self._stdout, self.buffer) + self._stdout_reader_thread = threading.Thread(target=self._pipe_reader, args=_args, daemon=True, name=n) + self._stdout_reader_thread.start() + + if piping_stderr: + n = f'popen-stderr-reader:pid-{self._process.pid}' + self._stderr = self._process.stderr + _args = (self._stderr, stderr) + self._stderr_reader_thread = threading.Thread(target=self._pipe_reader, args=_args, daemon=True, name=n) + self._stderr_reader_thread.start() + + @staticmethod + def _on_error(_self: FFmpegSink, error: Exception, data: Optional[VoiceData]) -> None: + _self.voice_client.stop_listening() # type: ignore + + def wants_opus(self) -> bool: + return False + + def cleanup(self): + self._kill_process() + self._process = self._stdout = self._stdin = self._stderr = MISSING + + def write(self, user: Optional[User], data: VoiceData): + if self._process and not self._stdin.closed: + audio = data.opus if self.wants_opus() else data.pcm + assert audio is not None + try: + self._stdin.write(audio) + except Exception as e: + log.exception('Error writing data to ffmpeg') + self._kill_process() + self.on_error(self, e, data) + + def _spawn_process(self, args: Any, **subprocess_kwargs: Any) -> subprocess.Popen: + log.debug('Spawning ffmpeg process with command: %s, kwargs: %s', args, subprocess_kwargs) + process = None + try: + process = subprocess.Popen(args, creationflags=discord.player.CREATE_NO_WINDOW, **subprocess_kwargs) + except FileNotFoundError: + executable = args.partition(' ')[0] if isinstance(args, str) else args[0] + raise Exception(executable + ' was not found.') from None + except subprocess.SubprocessError as exc: + raise Exception(f'Popen failed: {exc.__class__.__name__}: {exc}') from exc + else: + return process + + def _kill_process(self) -> None: + # this function gets called in __del__ so instance attributes might not even exist + proc: subprocess.Popen = getattr(self, '_process', MISSING) + if proc is MISSING: + return + + log.debug('Terminating ffmpeg process %s.', proc.pid) + + try: + self._stdin.close() + except Exception: + pass + + # TODO: extract wait time + log.debug('Waiting for ffmpeg process %s for up to 5 seconds.', proc.pid) + try: + proc.wait(5) + except Exception: + pass + + try: + proc.kill() + except Exception: + log.exception('Ignoring error attempting to kill ffmpeg process %s', proc.pid) + + if proc.poll() is None: + log.info('ffmpeg process %s has not terminated. Waiting to terminate...', proc.pid) + proc.communicate() + log.info('ffmpeg process %s should have terminated with a return code of %s.', proc.pid, proc.returncode) + else: + log.info('ffmpeg process %s successfully terminated with return code of %s.', proc.pid, proc.returncode) + + self._process = MISSING + + def _pipe_reader(self, source: IO[bytes], dest: IO[bytes]) -> None: + while self._process: + if source.closed: + return + try: + data = source.read(discord.FFmpegAudio.BLOCKSIZE) + except (OSError, ValueError) as e: + log.debug('FFmpeg stdin pipe closed: %s', e) + return + except Exception: + log.debug('Read error for %s, this is probably not a problem', self, exc_info=True) + return + if data is None: + return + try: + dest.write(data) + except Exception as e: + log.exception('Write error for %s', self) + self._kill_process() + self.on_error(self, e, None) + return + + +class PCMVolumeTransformer(AudioSink): + """AudioSink used to change the volume of PCM data, just like + :class:`discord.PCMVolumeTransformer`. + """ + + def __init__(self, destination: AudioSink, volume: float = 1.0): + if not isinstance(destination, AudioSink): + raise TypeError(f'expected AudioSink not {type(destination).__name__}') + + if destination.wants_opus(): + raise VoiceRecvException('AudioSink must not request Opus encoding.') + + super().__init__(destination) + + self.destination: AudioSink = destination + self._volume: float = volume + + def wants_opus(self) -> bool: + return False + + @property + def volume(self) -> float: + """Retrieves or sets the volume as a floating point percentage (e.g. 1.0 for 100%).""" + return self._volume + + @volume.setter + def volume(self, value: float): + self._volume = max(value, 0.0) + + def write(self, user: Optional[User], data: VoiceData) -> None: + data.pcm = audioop.mul(data.pcm, 2, min(self._volume, 2.0)) + self.destination.write(user, data) + + def cleanup(self) -> None: + pass + + +class ConditionalFilter(AudioSink): + """AudioSink for filtering packets based on an arbitrary predicate function.""" + + def __init__(self, destination: AudioSink, predicate: ConditionalFilterFn): + super().__init__(destination) + + self.destination: AudioSink = destination + self.predicate: ConditionalFilterFn = predicate + + def wants_opus(self) -> bool: + return self.destination.wants_opus() + + def write(self, user: Optional[User], data: VoiceData) -> None: + if self.predicate(user, data): + self.destination.write(user, data) + + def cleanup(self) -> None: + del self.predicate + + +class UserFilter(ConditionalFilter): + """A convenience class for a User based ConditionalFilter.""" + + def __init__(self, destination: AudioSink, user: User): + super().__init__(destination, self._predicate) + self.user: User = user + + def _predicate(self, user: Optional[User], data: VoiceData) -> bool: + return user == self.user + + +class TimedFilter(ConditionalFilter): + """A convenience class for a timed ConditionalFilter.""" + + def __init__(self, destination: AudioSink, duration: float, *, start_on_init: bool = False): + super().__init__(destination, self.predicate) + self.duration: float = duration + self.start_time: Optional[float] + + if start_on_init: + self.start_time = self.get_time() + else: + self.start_time = None + self.write = self._write_once + + def _write_once(self, user: Optional[User], data: VoiceData): + self.start_time = self.get_time() + super().write(user, data) + self.write = super().write + + def predicate(self, user: Optional[User], data: VoiceData) -> bool: + return self.start_time is not None and self.get_time() - self.start_time < self.duration + + def get_time(self) -> float: + """Function to generate a timestamp. Defaults to `time.perf_counter()`. + Can be overridden. + """ + return time.perf_counter() + + +class SilenceGeneratorSink(AudioSink): + """Generates intermittent silence packets during transmission downtime.""" + + def __init__(self, destination: AudioSink): + super().__init__(destination) + + self.destination: AudioSink = destination + self.silencegen: SilenceGenerator = SilenceGenerator(self.destination.write) + self.silencegen.start() + + def wants_opus(self) -> bool: + return self.destination.wants_opus() + + def write(self, user: Optional[User], data: VoiceData) -> None: + self.silencegen.push(user, data.packet) + self.destination.write(user, data) + + @AudioSink.listener() + def on_voice_member_disconnect(self, member: discord.Member, ssrc: Optional[int]) -> None: + self.silencegen.drop(ssrc=ssrc, user=member) + + def cleanup(self) -> None: + self.silencegen.stop() diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/types.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/types.py new file mode 100644 index 0000000..ef409dc --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/types.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Literal, Optional, TypedDict + +from discord.types.snowflake import Snowflake + +if TYPE_CHECKING: + from typing import Union + import discord + + MemberOrUser = Union[discord.Member, discord.User] + +ResolutionTypes = Literal['fixed', 'source'] +StreamTypes = Literal['audio', 'video', 'screen', 'test'] # only video appears to be used + + +class VideoResolution(TypedDict): + height: int + width: int + type: ResolutionTypes + + +class VideoStream(TypedDict): + type: StreamTypes + active: bool + max_bitrate: int + max_framerate: int + max_resolution: VideoResolution + quality: int + rid: int + rtx_ssrc: int + ssrc: int + + +class VoiceVideoPayload(TypedDict): + audio_ssrc: int + video_ssrc: int + user_id: Snowflake + streams: list[VideoStream] + + +class VoiceClientConnectPayload(TypedDict): + user_ids: List[Snowflake] + + +class VoiceClientDisconnectPayload(TypedDict): + user_id: Snowflake + + +class VoiceFlagsPayload(TypedDict): + flags: Optional[int] + user_id: Snowflake + + +class VoicePlatformPayload(TypedDict): + platform: Optional[Literal[0, 1, 2, 3]] + user_id: Snowflake diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/utils.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/utils.py new file mode 100644 index 0000000..01f0259 --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/utils.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import time +import threading + +from collections import defaultdict + +from typing import TYPE_CHECKING, Generic, TypeVar + +if TYPE_CHECKING: + from typing import Callable, Sequence + + TimeFunc = Callable[[], float] + +_dataT = TypeVar("_dataT") + + +def gap_wrapped(a: int, b: int, *, wrap: int = 65536) -> int: + """ + Returns the gap between two numbers, acounting for unsigned integer wraparound. + """ + return (b - (a + 1) + wrap) % wrap + + +def add_wrapped(a: int, b: int, *, wrap: int = 65536) -> int: + """ + Returns the sum of two numbers, accounting for unsigned integer wraparound. + """ + return (a + b) % wrap + + +# May not even be needed if i dont use the dict subclasses +class Bidict(dict): + """A bi-directional dict""" + + _None = object() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + super().update({v: k for k, v in self.items()}) + + def __setitem__(self, key, value): + # Delete related mappings + # if we have 1 <-> 2 and we set 2 <-> 3, 2 is now unrelated to 1 + + if key in self: + del self[key] + if value in self: + del self[value] + + super().__setitem__(key, value) + super().__setitem__(value, key) + + def __delitem__(self, key): + value = super().__getitem__(key) + super().__delitem__(value) + + if key == value: + return + + super().__delitem__(key) + + def to_dict(self): + return super().copy() + + def pop(self, k, d=_None): + try: + v = super().pop(k) + super().pop(v, d) + return v + except KeyError: + if d is not self._None: + return d + raise + + def popitem(self): + item = super().popitem() + super().__delitem__(item[1]) + return item + + def setdefault(self, k, d=None): + try: + return self[k] + except KeyError: + if d in self: + return d + + self[k] = d + return d + + def update(self, *args, **F): + try: + E = args[0] + if callable(getattr(E, 'keys', None)): + for k in E: + self[k] = E[k] + else: + for k, v in E: + self[k] = v + except IndexError: + pass + finally: + for k in F: + self[k] = F[k] + + def copy(self): + return self.__class__(super().copy()) + + # incompatible + # https://docs.python.org/3/library/exceptions.html#NotImplementedError, Note 1 + fromkeys = None # type: ignore + + +class Defaultdict(defaultdict): + def __missing__(self, key): + if self.default_factory is None: + raise KeyError((key,)) + + self[key] = value = self.default_factory(key) # type: ignore + return value + + +class LoopTimer: + def __init__(self, delay: float, *, timefunc: TimeFunc = time.perf_counter): + self._delay: float = delay + self._time: TimeFunc = timefunc + self._start: float = 0 + self._loops: int = 0 + + @property + def delay(self) -> float: + return self._delay + + @property + def loops(self) -> int: + return self._loops + + @property + def start_time(self) -> float: + return self._start + + @property + def remaining_time(self) -> float: + next_time = self._start + self._delay * self._loops + return self._delay + (next_time - self._time()) + + def start(self) -> None: + self._loops = 0 + self._start = self._time() + + def mark(self) -> None: + self._loops += 1 + + def sleep(self) -> None: + time.sleep(max(0, self.remaining_time)) + + +class MultiDataEvent(Generic[_dataT]): + """ + Something like the inverse of a Condition. A 1-waiting-on-N type of object, + with accompanying data object for convenience. + """ + + def __init__(self): + self._items: list[_dataT] = [] + self._ready: threading.Event = threading.Event() + + @property + def items(self) -> list[_dataT]: + """A shallow copy of the currently ready objects.""" + return self._items.copy() + + def is_ready(self) -> bool: + return self._ready.is_set() + + def _check_ready(self) -> None: + if self._items: + self._ready.set() + else: + self._ready.clear() + + def notify(self) -> None: + self._ready.set() + self._check_ready() + + def wait(self, timeout: float | None = None) -> bool: + self._check_ready() + return self._ready.wait(timeout) + + def register(self, item: _dataT) -> None: + self._items.append(item) + self._ready.set() + + def unregister(self, item: _dataT) -> None: + try: + self._items.remove(item) + except ValueError: + pass + self._check_ready() + + def clear(self) -> None: + self._items.clear() + self._ready.clear() diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/video.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/video.py new file mode 100644 index 0000000..984ad3f --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/video.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .types import ( + VoiceVideoPayload, + VideoStream as VideoStreamPayload, + VideoResolution as VideoResolutionPayload, + ) + from .voice_client import VoiceRecvClient + +__all__ = [ + 'VoiceVideoStreams', +] + + +class VoiceVideoStreams: + __slots__ = ( + 'audio_ssrc', + 'video_ssrc', + 'member', + 'streams', + ) + + def __init__(self, *, data: VoiceVideoPayload, vc: VoiceRecvClient): + self.audio_ssrc = data['audio_ssrc'] + self.video_ssrc = data['video_ssrc'] + self.member = vc.guild.get_member(int(data['user_id'])) + self.streams = self._get_streams(data['streams']) + + def __repr__(self) -> str: + return f"" + + def _get_streams(self, data: list[VideoStreamPayload]) -> list[VideoStreamInfo]: + return [VideoStreamInfo(data=stream) for stream in data] + + def _minify_streams(self) -> str: + streams = [f"" for s in self.streams] + return f"[{', '.join(streams)}]" + + +class VideoStreamInfo: + __slots__ = ( + 'type', + 'active', + 'max_bitrate', + 'max_framerate', + 'max_resolution', + 'quality', + 'rid', + 'rtx_ssrc', + 'ssrc', + ) + + def __init__(self, *, data: VideoStreamPayload): + self.type: str = data.get('type', 'video') + self.active = data['active'] + self.max_bitrate = data.get('max_bitrate', 0) + self.max_framerate = data['max_framerate'] + self.max_resolution = VideoStreamResolution(data['max_resolution']) + self.quality = data['quality'] + self.rid = data['rid'] + self.rtx_ssrc = data['rtx_ssrc'] + self.ssrc = data['ssrc'] + + def __repr__(self) -> str: + attrs = [ + ('ssrc', self.ssrc), + ('active', self.active), + ('quality', self.quality), + ('max_bitrate', self.max_bitrate), + ('max_framerate', self.max_framerate), + ('max_resolution', self.max_resolution), + ] + inner = ' '.join('%s=%r' % t for t in attrs) + return f'<{self.__class__.__name__} {inner}>' + + +class VideoStreamResolution: + __slots__ = ( + 'height', + 'width', + 'type', + ) + + def __init__(self, data: VideoResolutionPayload): + self.height = data['height'] + self.width = data['width'] + self.type = data['type'] + + def __repr__(self) -> str: + return f"" diff --git a/vendor/discord-ext-voice-recv/discord/ext/voice_recv/voice_client.py b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/voice_client.py new file mode 100644 index 0000000..a646712 --- /dev/null +++ b/vendor/discord-ext-voice-recv/discord/ext/voice_recv/voice_client.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import time +import asyncio +import logging + +import discord +from discord.voice_state import VoiceConnectionState +from discord.utils import MISSING + +from typing import TYPE_CHECKING + +from .gateway import hook +from .reader import AudioReader +from .sinks import AudioSink + +if TYPE_CHECKING: + from typing import Optional, Dict, Any, Union + from discord.ext.commands._types import CoroFunc + from .reader import AfterCB + +from pprint import pformat + +__all__ = [ + 'VoiceRecvClient', +] + +log = logging.getLogger(__name__) + + +class VoiceRecvClient(discord.VoiceClient): + endpoint_ip: str + voice_port: int + + def __init__(self, client: discord.Client, channel: discord.abc.Connectable): + super().__init__(client, channel) + + self._reader: AudioReader = MISSING + self._ssrc_to_id: Dict[int, int] = {} + self._id_to_ssrc: Dict[int, int] = {} + self._event_listeners: Dict[str, list] = {} + + def create_connection_state(self) -> VoiceConnectionState: + return VoiceConnectionState(self, hook=hook) + + async def on_voice_state_update(self, data) -> None: + old_channel_id = self.channel.id if self.channel else None + + await super().on_voice_state_update(data) + + log.debug("Got voice_client VSU: \n%s", pformat(data, compact=True)) + + # this can be None + try: + channel_id = int(data['channel_id']) + except TypeError: + return + + # if we joined, left, or switched channels, reset the decoders + if self._reader and channel_id != old_channel_id: + log.debug("Destroying all decoders in guild %s", self.guild.id) + self._reader.packet_router.destroy_all_decoders() + + def add_listener(self, func: CoroFunc, *, name: str = MISSING) -> None: + name = func.__name__ if name is MISSING else name + + if not asyncio.iscoroutinefunction(func): + raise TypeError('Listeners must be coroutines') + + if name in self._event_listeners: + self._event_listeners[name].append(func) + else: + self._event_listeners[name] = [func] + + def remove_listener(self, func: CoroFunc, *, name: str = MISSING) -> None: + name = func.__name__ if name is MISSING else name + + if name in self._event_listeners: + try: + self._event_listeners[name].remove(func) + except ValueError: + pass + + async def _run_event(self, coro: CoroFunc, event_name: str, *args: Any, **kwargs: Any) -> None: + try: + await coro(*args, **kwargs) + except asyncio.CancelledError: + pass + except Exception: + log.exception("Error calling %s", event_name) + + def _schedule_event(self, coro: CoroFunc, event_name: str, *args: Any, **kwargs: Any) -> asyncio.Task: + wrapped = self._run_event(coro, event_name, *args, **kwargs) + return self.client.loop.create_task(wrapped, name=f"ext.voice_recv: {event_name}") + + def dispatch(self, event: str, /, *args: Any, **kwargs: Any) -> None: + log.debug("Dispatching voice_client event %s", event) + + event_name = f"on_{event}" + for coro in self._event_listeners.get(event_name, []): + self._schedule_event(coro, event_name, *args, **kwargs) + + self.dispatch_sink(event, *args, **kwargs) + self.client.dispatch(event, *args, **kwargs) + + def dispatch_sink(self, event: str, /, *args: Any, **kwargs: Any) -> None: + if self._reader: + self._reader.event_router.dispatch(event, *args, **kwargs) + + def cleanup(self) -> None: + # TODO: Does the order here matter? + super().cleanup() + self._event_listeners.clear() + self.stop() + + def _add_ssrc(self, user_id: int, ssrc: int) -> None: + self._ssrc_to_id[ssrc] = user_id + self._id_to_ssrc[user_id] = ssrc + + if self._reader: + self._reader.packet_router.set_user_id(ssrc, user_id) + + def _remove_ssrc(self, *, user_id: int) -> None: + ssrc = self._id_to_ssrc.pop(user_id, None) + if ssrc: + self._reader.speaking_timer.drop_ssrc(ssrc) + self._ssrc_to_id.pop(ssrc, None) + + def _get_ssrc_from_id(self, user_id: int) -> Optional[int]: + return self._id_to_ssrc.get(user_id) + + def _get_id_from_ssrc(self, ssrc: int) -> Optional[int]: + return self._ssrc_to_id.get(ssrc) + + def listen(self, sink: AudioSink, *, after: Optional[AfterCB] = None) -> None: + """Receives audio into a :class:`AudioSink`.""" + # TODO: more info + + if not self.is_connected(): + raise discord.ClientException('Not connected to voice.') + + if not isinstance(sink, AudioSink): + raise TypeError('sink must be an AudioSink not {0.__class__.__name__}'.format(sink)) + + if self.is_listening(): + raise discord.ClientException('Already receiving audio.') + + self._reader = AudioReader(sink, self, after=after) + self._reader.start() + + def is_listening(self) -> bool: + """Indicates if we're currently receiving audio.""" + return self._reader and self._reader.is_listening() + + def stop_listening(self) -> None: + """Stops receiving audio.""" + if self._reader: + self._reader.stop() + self._reader = MISSING + + def stop_playing(self) -> None: + """Stops playing audio.""" + if self._player: + self._player.stop() + self._player = None + + def stop(self) -> None: + """Stops playing and receiving audio.""" + self.stop_playing() + self.stop_listening() + + @property + def sink(self) -> Optional[AudioSink]: + return self._reader.sink if self._reader else None + + @sink.setter + def sink(self, sink: AudioSink) -> None: + if not isinstance(sink, AudioSink): + raise TypeError('expected AudioSink not {0.__class__.__name__}.'.format(sink)) + + if not self._reader: + raise ValueError('Not receiving anything.') + + self._reader.set_sink(sink) + + def get_speaking(self, member: Union[discord.Member, discord.User]) -> Optional[bool]: + """Returns if a member is speaking (approximately), or None if not found.""" + + ssrc = self._get_ssrc_from_id(member.id) + if ssrc is None: + return + + if self._reader: + return self._reader.speaking_timer.get_speaking(ssrc) diff --git a/vendor/discord-ext-voice-recv/examples/recv.py b/vendor/discord-ext-voice-recv/examples/recv.py new file mode 100644 index 0000000..b0f57e8 --- /dev/null +++ b/vendor/discord-ext-voice-recv/examples/recv.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +import discord +from discord.ext import commands, voice_recv + +discord.opus._load_default() + +bot = commands.Bot(command_prefix=commands.when_mentioned, intents=discord.Intents.all()) + +class Testing(commands.Cog): + def __init__(self, bot): + self.bot = bot + + @commands.command() + async def test(self, ctx): + def callback(user, data: voice_recv.VoiceData): + print(f"Got packet from {user}") + + ## voice power level, how loud the user is speaking + # ext_data = packet.extension_data.get(voice_recv.ExtensionID.audio_power) + # value = int.from_bytes(ext_data, 'big') + # power = 127-(value & 127) + # print('#' * int(power * (79/128))) + ## instead of 79 you can use shutil.get_terminal_size().columns-1 + + vc = await ctx.author.voice.channel.connect(cls=voice_recv.VoiceRecvClient) + vc.listen(voice_recv.BasicSink(callback)) + + @commands.command() + async def stop(self, ctx): + await ctx.voice_client.disconnect() + + @commands.command() + async def die(self, ctx): + ctx.voice_client.stop() + await ctx.bot.close() + +@bot.event +async def on_ready(): + print('Logged in as {0.id}/{0}'.format(bot.user)) + print('------') + +@bot.event +async def setup_hook(): + await bot.add_cog(Testing(bot)) + +bot.run("token") diff --git a/vendor/discord-ext-voice-recv/pyproject.toml b/vendor/discord-ext-voice-recv/pyproject.toml new file mode 100644 index 0000000..eddc8e0 --- /dev/null +++ b/vendor/discord-ext-voice-recv/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 125 +skip-string-normalization = true + +[tool.isort] +profile = "black" +combine_as_imports = true +combine_star = true +line_length = 125 + +[tool.pyright] +include = [ + "discord/ext/voice_recv", +] +exclude = [ + "**/__pycache__", + "build", + "dist", +] +reportUnnecessaryTypeIgnoreComment = "warning" +# reportUnusedImport = "error" +pythonVersion = "3.8" +typeCheckingMode = "basic" diff --git a/vendor/discord-ext-voice-recv/requirements.txt b/vendor/discord-ext-voice-recv/requirements.txt new file mode 100644 index 0000000..14c3c56 --- /dev/null +++ b/vendor/discord-ext-voice-recv/requirements.txt @@ -0,0 +1 @@ +discord.py[voice]>=2.2.0 diff --git a/vendor/discord-ext-voice-recv/setup.py b/vendor/discord-ext-voice-recv/setup.py new file mode 100644 index 0000000..07d7e5a --- /dev/null +++ b/vendor/discord-ext-voice-recv/setup.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +from setuptools import setup +import re + +with open('discord/ext/voice_recv/__init__.py') as f: + version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) # type: ignore + +if not version: + raise RuntimeError('version is not set') + +if version.endswith(('a', 'b', 'rc')): + # append version identifier based on commit count + try: + import subprocess + + p = subprocess.Popen(['git', 'rev-list', '--count', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate() + if out: + version = version + out.decode('utf-8').strip() + except Exception: + pass + +with open('README.md') as f: + readme = f.read() + +extras_require = { + 'extras_speech': [ + 'SpeechRecognition', + ], + 'extras_local': [ + 'pyaudio', + ], + 'extras': [ + 'SpeechRecognition', + 'pyaudio', + ], +} + +setup( + name='discord-ext-voice_recv', + author='Imayhaveborkedit', + url='https://github.com/imayhaveborkedit/discord-ext-voice-recv', + version=version, + packages=['discord.ext.voice_recv', 'discord.ext.voice_recv.extras'], + license='MIT', + description='Experimental voice receive extension for discord.py', + long_description=readme, + long_description_content_type='text/markdown', + include_package_data=True, + python_requires='>=3.8', + install_requires=['discord.py[voice]>=2.5'], + extras_require=extras_require, + zip_safe=False, + classifiers=[ + 'Development Status :: 3 - Alpha', + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: MacOS', + 'Topic :: Multimedia :: Sound/Audio :: Capture/Recording', + ], +) diff --git a/vendor/discord-ext-voice-recv/update_notes.md b/vendor/discord-ext-voice-recv/update_notes.md new file mode 100644 index 0000000..efe56b7 --- /dev/null +++ b/vendor/discord-ext-voice-recv/update_notes.md @@ -0,0 +1,21 @@ +# Update notes +Notably, not a changelog, just notes. + +## 0.5.2 +- Adds `extras.localplayback` module +- Adds info about the extras modules to the readme +- Adds `WavSink` as an alias to `WaveSink` +- Fixed a member cleanup error in SpeechRecognitionSink +- Changes the optional dependency format + - Previously it was a single optional dep, `extras`. Now there is a dependency per module, with `extras` installing all of them. See the readme for details. + +## 0.5.1 +- Fixes a build process related error +- Changes `voice_recv.extras` import semantics + - The `__all__` contents of the extras modules are no longer `*` imported into `voice_recv.extras` (this was only `extras.SpeechRecognitionSink`). You will have to access them directly, or import that specific extra module. Example: + ```py + from discord.ext.voice_recv.extras.speechrecognition import SpeechRecognitionSink + # or + from discord.ext.voice_recv.extras import speechrecognition + sink = speechrecognition.SpeechRecognitionSink(...) + ```