From 0de24122adb96607a8b6a767e02a3968f464f3a7 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 6 Dec 2021 22:18:53 +1000 Subject: [PATCH] implement a basic native macOS audio recorder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was motivated by the fact that recording was crashing on the native M1 build. That ended up being mostly a PEBKAC problem - turns out the Mac Mini has no built-in microphone 🤦. I still thinks this has some value though - it doesn't crash in such cases, and probably doesn't suffer from the problem shown in this thread either: https://forums.ankiweb.net/t/anki-crashes-when-trying-to-record-on-mac/14764 For now, this is only enabled when running on arm64. If it turns out to be reliable, it could be offered as an option on amd64 as well. --- qt/aqt/_macos_helper.py | 55 ++++++++ qt/aqt/sound.py | 40 ++++-- qt/aqt/theme.py | 41 ++---- qt/mac/BUILD.bazel | 1 + qt/mac/ankihelper.xcodeproj/project.pbxproj | 4 + .../UserInterfaceState.xcuserstate | Bin 19893 -> 27529 bytes .../xcschemes/ankihelper.xcscheme | 23 ++++ qt/mac/record.swift | 117 ++++++++++++++++++ 8 files changed, 241 insertions(+), 40 deletions(-) create mode 100644 qt/aqt/_macos_helper.py create mode 100644 qt/mac/ankihelper.xcodeproj/xcuserdata/dae.xcuserdatad/xcschemes/ankihelper.xcscheme create mode 100644 qt/mac/record.swift diff --git a/qt/aqt/_macos_helper.py b/qt/aqt/_macos_helper.py new file mode 100644 index 000000000..f0c14d999 --- /dev/null +++ b/qt/aqt/_macos_helper.py @@ -0,0 +1,55 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +import os +import sys +from ctypes import CDLL, CFUNCTYPE, c_char_p +from typing import Callable + +import aqt + + +class _MacOSHelper: + def __init__(self) -> None: + if getattr(sys, "frozen", False): + path = os.path.join(sys.prefix, "libankihelper.dylib") + else: + path = os.path.join( + aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib" + ) + + self._dll = CDLL(path) + + def system_is_dark(self) -> bool: + return self._dll.system_is_dark() + + def set_darkmode_enabled(self, enabled: bool) -> bool: + return self._dll.set_darkmode_enabled(enabled) + + def start_wav_record(self, path: str, on_error: Callable[[str], None]) -> None: + global _on_audio_error + _on_audio_error = on_error + self._dll.start_wav_record(path.encode("utf8"), _audio_error_callback) + + def end_wav_record(self) -> None: + "On completion, file should be saved if no error has arrived." + self._dll.end_wav_record() + + +# this must not be overwritten or deallocated +@CFUNCTYPE(None, c_char_p) # type: ignore +def _audio_error_callback(msg: str) -> None: + if handler := _on_audio_error: + handler(msg) + + +_on_audio_error: Callable[[str], None] | None = None + +macos_helper: _MacOSHelper | None = None +if sys.platform == "darwin": + try: + macos_helper = _MacOSHelper() + except Exception as e: + print("macos_helper:", e) diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index 00e6f19b6..1f3e5e594 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -25,6 +25,7 @@ from anki.cards import Card from anki.sound import AV_REF_RE, AVTag, SoundOrVideoTag from anki.utils import is_lin, is_mac, is_win, namedtmp from aqt import gui_hooks +from aqt._macos_helper import macos_helper from aqt.mpv import MPV, MPVBase, MPVCommandError from aqt.qt import * from aqt.taskman import TaskManager @@ -607,6 +608,30 @@ class QtAudioInputRecorder(Recorder): t.start(500) +# Native macOS recording +########################################################################## + + +class NativeMacRecorder(Recorder): + def __init__(self, output_path: str) -> None: + super().__init__(output_path) + self._error: str | None = None + + def _on_error(self, msg: str) -> None: + self._error = msg + + def start(self, on_done: Callable[[], None]) -> None: + self._error = None + assert macos_helper + macos_helper.start_wav_record(self.output_path, self._on_error) + super().start(on_done) + + def stop(self, on_done: Callable[[str], None]) -> None: + assert macos_helper + macos_helper.end_wav_record() + Recorder.stop(self, on_done) + + # Recording dialog ########################################################################## @@ -662,9 +687,14 @@ class RecordDialog(QDialog): def _start_recording(self) -> None: if qtmajor > 5: - self._recorder = QtAudioInputRecorder( - namedtmp("rec.wav"), self.mw, self._parent - ) + if macos_helper and platform.machine() == "arm64": + self._recorder = NativeMacRecorder( + namedtmp("rec.wav"), + ) + else: + self._recorder = QtAudioInputRecorder( + namedtmp("rec.wav"), self.mw, self._parent + ) else: from aqt.qt.qt5_audio import QtAudioInputRecorder as Qt5Recorder @@ -706,10 +736,6 @@ class RecordDialog(QDialog): def record_audio( parent: QWidget, mw: aqt.AnkiQt, encode: bool, on_done: Callable[[str], None] ) -> None: - if sys.platform.startswith("darwin") and platform.machine() == "arm64": - showWarning("Recording currently only works in Anki's Intel build") - return - def after_record(path: str) -> None: if not encode: on_done(path) diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index bf01be109..465c9ce05 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -4,11 +4,8 @@ from __future__ import annotations import enum -import os import platform import subprocess -import sys -from ctypes import CDLL from dataclasses import dataclass import aqt @@ -335,27 +332,20 @@ def get_windows_dark_mode() -> bool: def set_macos_dark_mode(enabled: bool) -> bool: "True if setting successful." - if not is_mac: + from aqt._macos_helper import macos_helper + + if not macos_helper: return False - try: - _ankihelper().set_darkmode_enabled(enabled) - return True - except Exception as e: - # swallow exceptions, as library will fail on macOS 10.13 - print(e) - return False + return macos_helper.set_darkmode_enabled(enabled) def get_macos_dark_mode() -> bool: "True if macOS system is currently in dark mode." - if not is_mac: - return False - try: - return _ankihelper().system_is_dark() - except Exception as e: - # swallow exceptions, as library will fail on macOS 10.13 - print(e) + from aqt._macos_helper import macos_helper + + if not macos_helper: return False + return macos_helper.system_is_dark() def get_linux_dark_mode() -> bool: @@ -378,19 +368,4 @@ def get_linux_dark_mode() -> bool: return "-dark" in process.stdout.lower() -_ankihelper_dll: CDLL | None = None - - -def _ankihelper() -> CDLL: - global _ankihelper_dll - if _ankihelper_dll: - return _ankihelper_dll - if getattr(sys, "frozen", False): - path = os.path.join(sys.prefix, "libankihelper.dylib") - else: - path = os.path.join(aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib") - _ankihelper_dll = CDLL(path) - return _ankihelper_dll - - theme_manager = ThemeManager() diff --git a/qt/mac/BUILD.bazel b/qt/mac/BUILD.bazel index 45948e07d..b28232188 100644 --- a/qt/mac/BUILD.bazel +++ b/qt/mac/BUILD.bazel @@ -9,6 +9,7 @@ genrule( srcs = glob(["*.swift"]), outs = ["libankihelper.dylib"], cmd = "$(location :helper_build) $@ $(COMPILATION_MODE) $(SRCS)", + message = "Compiling Swift dylib", tags = ["manual"], tools = [":helper_build"], visibility = ["//qt:__subpackages__"], diff --git a/qt/mac/ankihelper.xcodeproj/project.pbxproj b/qt/mac/ankihelper.xcodeproj/project.pbxproj index 637f03e40..8232fba8e 100644 --- a/qt/mac/ankihelper.xcodeproj/project.pbxproj +++ b/qt/mac/ankihelper.xcodeproj/project.pbxproj @@ -8,10 +8,12 @@ /* Begin PBXBuildFile section */ 137892AC275D90FC009D0B6E /* theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 137892AB275D90FC009D0B6E /* theme.swift */; }; + 137892B0275DAE22009D0B6E /* record.swift in Sources */ = {isa = PBXBuildFile; fileRef = 137892AF275DAE22009D0B6E /* record.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 137892AB275D90FC009D0B6E /* theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = theme.swift; sourceTree = ""; }; + 137892AF275DAE22009D0B6E /* record.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = record.swift; sourceTree = ""; }; 138B770F2746137F003A3E4F /* libankihelper.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libankihelper.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -30,6 +32,7 @@ isa = PBXGroup; children = ( 137892AB275D90FC009D0B6E /* theme.swift */, + 137892AF275DAE22009D0B6E /* record.swift */, 138B77102746137F003A3E4F /* Products */, ); sourceTree = ""; @@ -110,6 +113,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 137892B0275DAE22009D0B6E /* record.swift in Sources */, 137892AC275D90FC009D0B6E /* theme.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/qt/mac/ankihelper.xcodeproj/project.xcworkspace/xcuserdata/dae.xcuserdatad/UserInterfaceState.xcuserstate b/qt/mac/ankihelper.xcodeproj/project.xcworkspace/xcuserdata/dae.xcuserdatad/UserInterfaceState.xcuserstate index ca596a1eb3edee7b74b87a9d4d8ec1948630333c..ea5377348099ec0d5fcb046ca69fc398d11c5f20 100644 GIT binary patch literal 27529 zcmeIa2Y3|K_dh&$YI-)5gp%G9vYXv(Pa)ZwEJ-$H(@220NtO@^*|@s_LXjB@s3>-@ zB1r%N6}w`?f)y)8KtNGcR7AzzP~S5%yD5VH`2C;n`+wf|dA?|p-I-g@Iro&$J@>Zt zc6WJwa`}D?VK_!$Bt~I0#$X{$sS8~mZs(clew>7mMzX!dtLVEDVdmCSjAYX;=;>#bj76CdU+5 z9;U=>*bJ;3tH3HTJ645NV>MVU=D=LoT&xqDhjn2WV)L>>>X($Itksjrv z0#t~KkO7&H8O=cDs0vl1dgMZLQ74**y3mDaKDrWJg;t}h(KTodx)xoBu17bZ8_`YZ zX0#TqL${z?(QW8qZ9-en4)g?i5n8eTEY_iBmX@Gk6dljECT%co-gzN8pk8L_8Xwil^b}cm|${ zXW`kn3RmMAT#M`QLc9bw;1=A5SK~E!Bi@3~!Y{y`xC@_)FTxk&%W)3B1Ye0?h2MtX zj^BabiLb}+!Z+Xp_*Q%yeh>Zt{v7@a{s#UIz6XC7KY$;`kKo7fPw~(3FYuH2*Z9x) zp9DcLL=X{1gcDK3L}D@#N2C){B9G7z`9uLxOf(Wr#7v@@Xdz}1vxzxGD{%qgBwR!{ zv4HRrK4KZMoVb|ah)ak|iOYzqiED^8#0|ua#9hP&Vt^PVhKT!#2Z#rWhlq!XCx|DB z=ZNQtmx*_XJ;Yw(DDgh=0dbr-L7XA}ApRu&A~6z?I7yHsNs;5p3FJgFnv5YQk?~|Q znL?G%rUE~7NOZv!V1lKhEv032E-j}Ow3;rYi)a&VrYq=5+D0(U;RJ>8t5$=$q-a^g8-3dIPcbwW+v0jv@o-n*~}c~LS{bG%`9Mg7&p_)T*NG5 zIOY=OQsy$|a%LrS9dkW%19LNTC$pZpiy30JGWRhL)Tj3Jb$9Q>!m$V}5|d!#vFJv- zLpI;l?`?*^ejV75+TQK-dIzy6Y#fWT#2_{So5+$J(y*>pNvd40u&DEtI;lcq(n(cH zom#3hC@fN|UaK{jRmMEILDwpoU^819-Jbd0UT3?@>~8N{;Og-OdY*zMW7&6OQ?Xbq z4vWVUutY40rC6F}*dR8T4Pis?#!|3UEDcMCBAHkk8^(sSdJd_eObn}MGv@HkclC6* z7g;^d1u(4Xqx-9Kwwc^LK99S*+vRCBDl}G&RjZU*^m!_&N@q1obxO5LnrAYpG;*EN zVl~2m(}!Dx+D50R(dljI@^-a#yBfQE^Bi40o!u^Y(<;dv-IF+mR=cCt<@LEePAD!| z3A5AZY?UNHQA4{A%*-$;#tuy~d8AjjySvTlu?2c7cdS6#}m#@p^ZS3ms%@d2+dc3`0V%?sRV#76sVzU~$T#L-Ew!Y3z;F@H7 zrE7uP(_iXw_Rbr58W_WH53`{=OGlT#-Qd9V>#$k8h}B~aSR>Yi&BU6q7Irc_g`LXA zvT3iFe!5W(S2qmU)iFFwa12U4?!I2} z1-KF3R!Lk_ySu|BgHe=s`DDXIWj3$3&js&e|KYtbrB+GeKfE3mm{!Tue<)Jn?u7RL z>1`bdFFPW;+JD9XW1Pi&zkIxD#2q6(C-TDW-R+&>`Lq^Y&CW@ zb`7=$yB50+yB@m%yAivI&0@3JX>1NFWo2wGD`ypK9;;+k_hM_Yb?~zmR2~15hOLL? zw-y^<)vOkN)S&D5A3d87i%`uL2yzLsW&+jb?De`jtXkK&w8N2XvmLho?n{_-eSJv9)*|R_kg9 z{eE_}Sa`>)43m37ugB$u0fTyMpD!qFFR#S;mtJE(KQwW){s*;vJ-#kpv5$j7!yOB( z7V7zRGxQ5A<*nHR*n=BbjmX}Iu}A9d4u{J(h&_z06yHCFJ-&g}iSM7lo}4qxXoJV& z1Rg(uJ%z0VYbbmTk{~c)OP7L*mGcTVPn(yMsMqO7)a!HDD_Hhs?0M`3Y!~(-b`$n8 zTf|Ohi`f!Z#TqtauVSxZyMdQ)fblZ2Cbj|iH;1=eA}Iwb1c(A+*5K^!gURx&5f(+A z$JyiU0+I^WglC%BSURj+c;g1|csBT(11eVaboT?*dVp)jZg=~9s1)ZXF#pO1Re%FN zc^8Nj9Hr0d$!bcy8zhYtYfpv$MzQ_SjvEs-1lz|9voO1(YjuE_B&?z)m>3yZZy} zn5yfAc02ij&g<&#@VI&eBD6~418tur>Hf}Bpn$2*12udV&c2@Zc@7Xcpl+>eF3i2B z-339NC=Fj=rv|Z;*q7`Kww$eK^Z{XEyx&zqH*e}?xAcwzUzhh^xKj7ytgdq?kNx=K5aJM^ol`_ZH0R!xs+vW09 zJ9+Ckaah^cy50ODa*5BO<^pF|&q(oB@E2g!F0PszXmjKpi?P-4GmEWaTY$X7DpeF3 zSj0SyWR}IQ_C9_PmY#)O9(ND#m5IFwbU`Ev-iRnfBL)SrHEbQ*$TqRf`%owf!y-{Q z_9}{O9_h1Q7!6ws67Fb~s8osV-EJ?a>ckX9Vpq@KzWHfu=F|HU{DJ?cpoIuE6PsdT(>9DKlbUOOgx3s26=5Ue#KwnKXDS7C`hZDHufA{DgRK^1RB=($3ZE@|<~^j1+0^8W7{_(6;dM&c+nl~T(%Q_!$O`QDs5qj8T)ya0*eGxafQ?C8&)`? zrirfanJXBEzZae)w&Ciiw~c+}S1@QgD#oHWY+zj?0WU|!LEfZ_%4b3w# zgm8kSil&=qcEJh}#t(}YA|Riky+10hhJ-Xbd**k|b9MK+JQ8egctm9Ax%zSO zmYG*t!bp^cUE4pnbIy?nXfrBwAY||O2@}t|+PvR`p<2x3!H`MKg2i3L2V@d*@6_11 z^KN9$kql@gA!#rqv8meQ?f_@ZDd+;QcR!D7Q;Nl6Ok|2JqfrIcJGen~00_8sBVfCaj@mJNoK&hoh0-JTAa zcTv||p9BHm7zF{c3@ZXCu^GaY4y*?}^JM@RUJ4H4CIAK>23YV(>`k!UpMp~R3hd8s zU~|TyB$N#+KM&x&B4h+WuLCVW9@K}HqZQ~f0PQxT`_Ur+&b@;U0RVRz{RN;~I4%Kr zO$vZo72X68S_j^V_u^iB8Gbpw3cnh^7QaIrIgb3(vHAb{|KRIIC0K)MA(jSX-YrJM zReiqht{xX3+Kp&JznU0A4phf$f%7ciDrsx2^Oc$!6dH3w-AEr0c;#tEzK-fr@OZWU z?phlhtxCm6Ibnf0AeIF)Gi;Gt1B#~s%?1Y#HKHao6E&k2G>cup_ONcYm%WJfY{t@1 zEBHles13D)TjvGONY7r&UdOA{L{Lx=jtuK4qhQp**aUF%!?|_f=49*kpNsq25 zxVy%Fp$6~UjD9!T;~r@@`ER90xyd7iCbS2lhf%$Y_P-ki-!GX80@H)ss25!Xg5yO# z5T1o-5n7D;(Gs*2Edx=y7=(#M9J&Nu3PN=`T8UP%KDLit$Sz_Rv;FK6b}74zUCv(2 zu3%Y~V=rMZWiMkdXIHYT*elp8*{j&q?A7cw>>9SYRWePK{d|j3Emx>i8Y!SMR;kKr zRY(naO1)HXkXwv;h1P7%Gmey2tMc?ltx6>|>E$M=N)5PE8XRAm1bO2Y4 zMzdC`*Qkvavqf!{%SVmGoTpZ)O>%`4xB%SI7-8I6V3E>fQtC}=J&&XHMym$S zTQp_`l-7U@XiXq77PUoV)Ecx#oqF_0EDDVpq#9%spi>1u2!kdMNR%fB-&YOX9aS2* zt~V&zh9z*S|Q%3#vv zsr4qsD9)>_X1(5^&`a}lN*Jv{WtHkwAX-L^POnf{lq!>Ul*9t1G&+kyF12dSKyQT= zMxxXjq>}RU zCNED4Dx6a$lM zwMxChV98UOw3<<+4O+Qgr!?xN22eG??>x{|uu`D|l_pQ6(<}8>;LceXf+lbIa#7W8 zM!_2(v=YI1v=u$To11Ou9&|6d4{Zm`=mz#i_9pgbb}g#{a|5R4Vfc9zJqCtG&#r@? zTiDxqbHiH=NBcb20+;B9gFPFDZFmIQF~6&~*VR!6F|POQ5*bip*itzJC%0$td}n^YxX$AOSAF#KmD-%dJdJ!LNO=}L&%5@|v9}JO7uef) z*Pd@dfI_?mFW*9dIxizYTOfYmfroPjJ}D)=C4wgssPP7R3(ForZ?fwL(A(@?e9a_) zA3W6_9t-feI!xfycLKg_SqxAL?{C}8UXd~T1f*!#R|~Xw5WUB@c!(Vwz%s$yLyJ=d zu!=`l{C1XibNpyppxOuMBP@Fm9YY_o8`({RJR=1L@>3bEUg2`icXb3{n}nv?;WvJ) z#_Dk|7)A*KO@EGl1k4eAfli_?(JAy5`Wk%${@l0dJM=yJfxVmE!fs`^vG=g|viGsu z+56cC*az8%?nOVLpV2SqR|tB4M`zF<=uh+uj^T)Xn0=Ogj(r}&;9cyC>`Uy+@auI>Q|VmXwV-Q>fMD_n*2F+*ms6OB;9v1w zfDHQwGTeoqW8h?u0~x}j*hdEN@$93#3`Kz}J4{m{niQlEkHH=v#3$jC*~i$YSzVxk zSUeGn9>U}Bcszl9oZZ1bF@z^!nRp8OB#7iwtZOzjH{#XJ;=hgTC`sTe&$>!yC%|rf z9j=)$R&g3BB`e1)Abc90dsa_!=qZnV271~VSgirE;0c*hvZPvAgn-(L0?=CFn(M1` z*LHQz^8o^b>+yVm$euw{@B)~*-7(Dj3Rh33uu(MXnV<;R7lgP1FT$s{jCwbS7h@|& zKWMJ%b@kYKV0+CQpp_9fiAp2zz>1fO55}N%_zb)pufYDo?bv$uZT3}mFMw#Tu=%Z$ z+;f9yB3>pNUXH^%6e8nlQScD%!0YgO_BHl(_RV4V3~v%(rrkWu)B^51M0*0*?nj~U z+1PpjrrrRk-!Uc_47_g>s=vio9|a{GhDdE5-i2R?&&MM1ZhQgWgS#Piy9oC{;>Q$- z;e6N`+=nl0>G1Eu*4I{mX7&QgB7(u)eG7W*eGA(7O}BBbo(>*(b&q}+)jrSZal%fR zr`Fli=>lHyz=a>pwDbVf0#)FVShoVgJ+Zl&Jc1AXEa-)X1O1J>1_}6E!|rzW9SJ3o zuzMsli;efAYkvW>2)L6gKQ}j**9G3(4yP-ZCx0%#^_JTK2|T$M`EnOH+jB<;(W2n= z<4bt-yrHhnwHTmIaU(ha#SY<1@nw7j`cDO5IYZUIR|B94zZhSEv&}rn1N?{iv-{b7 ztrGSBIKmMSNVb?Cey_)Up*Ye@@ylAEGceHt{yT^RLN@w=C|;`obH=Y=4~*!9)%XqI zIzW(q4Za2f_3QBK*@NsM_C5A6dxSl@8NU&~3BMVBuLH~TKKlXt8~Z+o;=u@IjM2|g zq9TA51iRmYGK0yB&&oiXf~kauu&pp#e8#J0g>qU8ud>Pk<`RO`IIvrN-5tF3s)kI1 zTGvIe2kHZT&g0Ag8wFP4AifEVB0hv~WRI~Q4&s|(>3sx(5BhK%zl055f0u$n1knoE z4tOc(43QT1qToUNK72cSoc)$DrKZHMwKLXr&6n_kV9N&RI0kZ!T{xtmFi9ZXj zQeX{;w!@E#+l35*Bwoh&eS01>ka-2Bjx_<`pMVtmMr{p(3(zBMhWlIkd-MYes&1@2J!q6f0q4GVDopt<`e9%e`oVA_^-ok z{)PR1ESvuVHWL_u*dN%RM%es~U`BrYCo@6>3w-|hANbrW@cA8{&!g$yE715ILA;1) zP(eft`#Wr>5>v29Vk!{}sC>J)@isPH6eba|oTs?8U&0C?U$R z>}`aBFcK!hOjrmjQA(5%Hev>cNDfgPqB+EHD2PMB917u3D2Kv06uyn9;C+28ji@GS zh+6nthh=goLU8^el_`otlQ=Y)&uW??MiWy;Bu*rvOx$Du2NyQJ&VC6>wz8`S+-)$g zN#}gc3$MuyNv_ULa99)0S;ht%fkK)SxUOfn0^TzC@x$5)%A}m#iV!pk7DMJ1LdSEp zB<`PxrFca6pQdE!^UZU6prf;A%$rNAyIb@&&ZgEP$Q0te3o(o%+EDNS(awQO)hvqb zT-e(rIh%$evFJf!A+d-<6F4++kmx6ta44EXF|4bFpRHiDT;4eWeJ}dE;<#1_KtNRZ<0&OF z!F|Rg0e_#koVbE-c_p!mLsK~vJErCOxj>Aw+k^xc;#%T5h*18ntbqFM^9PCRd2My> zmwX8BH|VY6lmm0TiMSQE6p5ROwZuB&77itHD2YSK97@?p+(z7vttal}P%4MA*nAF6 zV>6m~*PjnIW(wXv|EXCdpSYPQWUK^Y$-f6m#71H(-`gf)GjTVug+pl^O6O1phcY(; z>b{%k7ea90V6;CHZ-9W2-|&NA$F)d^o@^I#2$1Fx;xUNoh(|e;J%I7@>xNBQ z4>%MiKJ8U>1|`F-iZbhk*A5?`43$prd~j0&eGUhKHnZ4msi#-yhj&yv7DJYiI5sgL zVi8NQbWkvh2SchW$%aHhE+)lJkYI4`An}&_B@;6;v$A1hRHoJxnrD<(RMj^$HqRE* zc0p1h=aWzQOyu)Dg>=tEIsBIg+X3)cp-!xaQRnN%1drfQgBBPbrZ}2varp;49v)*7 zb7lAC0u-%O`7?l<7J*;yUSuw54 znR=TLF}eU=@h9aB>I+8F>58W378^{wh|M3Hl{`?wh7K5kUgP&!thuFSHgGwBC+&a| z_J};HueX`SSPUjT_LT#08zF}{0uq=tfXXX%0M%;v{Q#}jXoT$%*xS(=tN^X6RVKa4 zFl#AKRX)L1C0`CxtF!|vM|U<@S+K9VCb!m62T?_U%i=gh8vFB-6UM$9-Gop=OvMEi z_0Ji-pnpbTp8jDe)PN&vbxkveMNI5`u&DqZx6B%otvJ|HFh{;vJGV`)Z__zd9dfy_ zy*vd{th3<6iWW|+T!<}(Qz$oKcft9S?bzdxyLJHk0{amXf1=^+hzt@*^8DE&IygJR zZ!H(Y=CT=CQ5j@hxZtG78aNd)fF47SqqpH~$NO-u;}6(chU5x3&k>1F!BcQEUJ55Q z8u3;DE4l%u=*2w%Of1FM!dZ;_@cSWrI|QdMPJ+vO2F_h1fX`(Df2)+}fHM@A5Z4o6 zg1{bZ6>a43M)VxQYvLQ?G>5bt(s2kpG%#re94h2c5r?L8s2EbFiSLOYh#!fch@XjH zh+m1{c*R-5;nO%=#^Laxfx{a)d?trC^ZI>)5%973y>7?^@d_E?5Jo~)CHNKsVkw$h z!8!6mvK)jj-dSSexCrb+1L9Xm@B_;PxQ&oQ)GCqlX*xV!+U3w%?N=~hfuN0 z*AKBN*flWlfn_hsDVVn@;wL`y5#SvmC5+F2vh~b$L-9c9JzRW}xb-ZKInWqCWIsVk znhe9TH<1h(L4v zmOVtKk?ABjRuImVbEsm7=qIzuX@c`w$)PHqC4AmxYjuSUbe+X)tE;MQbyzAaraD`d zy|vsj^IQciP1Oc4wt)hCM#R__8f*@mvBFXd&G2mqo-nDvP7RQG9I~?&;sdp?6$934 zL;))DbgG<0wR$8Mm(-JmfF_aoB&^XI4mk$NA`(_Z2s*94)|>*}^V&pSRBGft*dTzThgg zp0jvBUj!CK0BZ$BlYWlYTg7Li8xII*3pt19>nw6Mhgvu^YmjUuVXE+L4nJG*WQ9`D zn&9INCyDzp!vwM6z{JIIo4dTd-A*CRQIs&35PUfd-QD0coIMn=(1j36lJhy_WSjSs zJ%EM*CPuo+UeZK*W)27G9D-=GjYG8@nwJ7eU>~oanwS@uOCPzIpUXmW5r;ZB8gGhvP4rCPKFBmtHBQFUYJ_#tlzf*jOyp+6*kM+beD*sgMA5b^H z7_KB&joC#ZuOip*YGXBdHF*t(fGXV_S};UjOI}A_&!HX;E#%N5oeGWiPmDuOxve4CH%u{3fYxu1iKGCtD*+^?%RbRER`4g6=2u^V~m zIvf8PUWn(yj=cY0M$68g%P^Mpe*;R3AMr>;MI`ffb-3U#qHGa=d{10L$N44nF^8`3 z!)fFRfYacZ%Wm=uz&wmRnsy~{s96#e5Q?wK(^&KX`3;9w50Kw-=<4%B)5ssmUjXwW ze z7T490%zQlyvg>mbuE&jk5x21vZqTU8pB#>D^VN4XVL zyx7n!XEk&_34Ws+N)a2n?W~5Z=aJzUphP+ap5g!nUg8}CR2GNsGBU^J+pD1bf zv`S{2uT2^8JV$xLqdv+Oj27i3Vh8vAFVX=FaN?p&IB_wysTE>V_qR$)&UdW-NGcFf z@yD2iRQ&*7E}^c0eIn{o>N4tbY9+Obx`Mirx{6v&UCp5fIrI>Rz!*QmAuz^|ap-Xl z?cmT89C~sawMMjl)D2+$!1hsVu}ls<6|jBJ@U~B|d@uZWn@Bk4E#qzZe`=eE+Q={5 zO&ogKZ`Y_Tyj|NZ{s)8?{|n@4Quk92f>EPjQSBU{fT_=(pHZV8qn-duL_N+MwdZ)F zMm@IhhE~)%N%-zL$7k^H4ecx*y|j6gF|m}=&fzkyM94@&o78@^pk_+Nxu`e zXIKyq{8teFUl7DEc|imv{!Ty;zxh`|{D}fDX@L5fLwg4(@M-p)ry%}8W4uT$q+qWc z+?f5aqe$Zd-1F{PA|<)hFH|%`hX{g5!yY#{QV0L7R-lfA22^Vs9Yv3$$I}z&iF7m_ zLr|7a_D^yeZZk(9Qu$$A93jTHab=mL^@FvL^_oh#E%1lcw$%( zzx=Nt{^tde&I3WDl^puSFNn0pFNm~W*xhX4(5Hf@j|VtCoh|`cq>DN9*#K?e(C6nT zi?oF<1F52|yi)vvSBkWaSBfXkr4*y^nSt)A=vq-2YXo6D1zM4|30m>wSt1;$(?qxP z!Z?#|rd#M)^lW+#-AZ3TJLxtKea#^VQciOSf|Tz#^gV}u;1IY~KXK^iZFGlU80Yzg zv6~mhUjo8-CLoNEKP}{hpB>Bn|2Hl@J0HY5D%a!xMivLXgcru89QrjNj2DA2(krmt zG{kB@>p;(@dxJZe}T{h1N}mB;_2yGZZi zqb0n zIK>Um`mb{Q&s%CH1cZ?R#gOV(j!Xmyid@-o|Jd9iwORnF6MeDPpEG#SCo1Lql>7 zhlcVvT*=`o4p(!yhQqZSuG_{K854hmo3S#bOc`TiX7JLh=Wzb6CHPao;e{Mt#NpF9 zyciz2X2OvP*a35x%V9rW{A`A;v39s+!s&yDS`)&qO;V_FM{ialGM3%a`Dx{isXJL9v>y7}DC zvjzc~K9Dfub2Y0-dih&9fVlFrYWcGx_+w&y z3;45Xq9i$7ox*N-y|E828ER6%$x*pd-XVcJa=1>4@nDY+FU^l`Y2!>c&lF`RwJ zEM}GgGR*WdOPHk`K7+%{IlN+sSq+jO|#P@H({Ue>>t7W)vAA5%*Fc`-GK0UoVlR-bF|S2NczYdE}`!)rLamd!{h zJ}p*l#}dGGZ;v0>a6L!~gsjCn{<+oVo~FW7phkUzn9B(y7$NLnqx~FDq4dmVKeGpC+TrW0*`Q8GiBE?pSc7w*`Z=DT zl{8lYS35i4&cr^rm*sM}_3u{fcI-|_q1y_#(d~eH;a-AE;$FjEhdWyKW5?i<=5Mew za4~ZjiiC@q$HUdka7`0jxm=6t;Chi(!>3zOj}Gu+j3 z5FJKG5u7GQAH$`|pTTv>33w7D)n(#JxCq&XSK#${Gn@llfcFVEw%mme!YQaH@#pau z;V94taDGPkH=H)tDt-_A#v#}Z>@hHF8OZe>1pOvzw~9gLHs*E?ujAlMV@66zkXWff zA%nXx#B6(J1Gpks6fD1hnHaHEklgy_ie2zzli!{#>ZeXz~Im&tIW zY!0S?OHPY{Ck=4<=NxJh6%W@9D`6|hLd}4yhV7Jts;8RZx?zsGh1vwy2tP*cpq`|j zhO301qh0`vbRYFDb&xtt9i={iYlV+f-_m)27OaHG>_z$*eFm=Ujb-AQL?#(7>rH1e znQXYSSH>6^6Jr4v!p4-t#l2N+hP)8+V#v!OuZHXnc_ZYlkat4%hU^bH5OOHw zaLCb+)1g73v7wsKn$YghD?;xMeJpf$=&{hRLw^qaCG@v2A}ly8A}l6sQrMKRl(4)o zeV8H47-kM@37Z$z7q%#@KWu5(@~{%wjbyD4mK*ezkVh20VMc-Wq>FT?5Z z^zh7Zb$Cg*G29$(4KE9y5ndnO7(O$+C46>xYq&GKKYSqk-SD3xVk6QbvLkXLWD)WR zU4$jV9?=rf7BN4fC!#mP6R|pCeZ+kcFGU=R_#xt_h+iUpi#QYUXCxM>iY$t>MwUg+ zh^&aTM^;D9j+__S6WJT-iS$L@6ZuHwzQ{w7KTAjnEeVo@NXAPhN|Gh1l5|O?L@m)u z^pXO}Oi7!hQ_?Hxmu!@5mF$;%B>6Rpj*5(miW(m^F)Ai1F)BGKH7Y$SGb%gE5LF-5 z6Ln?O+NgC=w?y3*H59caYFE@dQTw9ajXD%{IO=HB&*QLhapMxkB~9#`c=5!$CvKm3 zFuE|>7Tplt6x|#>D|$}!1<`HM?&yo6z0rNqi=z9Zmqss--Vps*^xo)SV&Y;{F-0*8 zV;09OiCGqNFi@7&ud(6>EA(M2I=1#h4(hHMLPmZ5lF?q@4Et3ySA*bX`xnRngDNj#1Id#%h z>r~&=!Kv>|{WCTvwmJ5S*t=r4#BPnfCw52dOR=xSz83p>?3=M~$L@*!Aoj!9J5oj<1Ywj-MUh8s8S*5kEKnviR%c*T&xxe_Q-r@dNQg@tfkG zi9Z&9DuGI15`qT%ZB6QUBvCrnI;Ntm24H6boRlb}n;Pbf^7o=}otOfV-{6Uq{1 zBvd3+CDbIi60S(NG2yXDl$E4RDonB^RVURZ)g?6~%}#1f zawfGWxsnzou}L>4txMXNv?J;Dq&JfeBppxsEb05CKa)d~!;-_3Ba>s3bK^7`Zr$%Dz;lb=d{Ci&Uq=ab(`K9c-? z^0DNPl24^fOi4=7rBtRkQ+z3_Qm#+gobqJK(L zQZGpDO7*2KOkJG%K(Xvb+njb!+I?yFr#+JPSlW)XC)3_eJD&Ds+Ud0K z(tb$$DLp1#nckS*mA)$d_Vl~cUrK)^{k8Ph)89;gJAF_3zVvs~zfM1${$2VH=|83a zlKxu;kr9*;k`a~>kufzRE+Zi$DMOl(pHY}GJ)ClCx5?(z7zNlvyQN#w>G| zHOrAzpVgQ(GpjAjmDQQmmF3C0ChOX)8?qkGdNk|ttS7Uc&f1yvT-FO&FJ`@;^GVA-SU$TD7`XlSF?6B;JY)SUG?1b#(?9}Y^Y(@6;?2>F_wmG{ddv11D zc6YWr+mqdwy*PVm_Qly;_GQ_(X5W#$K6^vE-C0J?vpk2*xjg5JoYgtkK*DQ)t_kMHFnKn%@)n0njM;_G&?oVYj$bg(d^ai*BsQwYp20AMQW{1Tc9;+ zE!r||xwcZXbh_Aec&V{{?9Fx_O`R9&1d zQP-;L*7fLmbza>wy4|{cx&ylRbVqf^bf4?K)P1e{R`-MMC%8tqQ@>XKto{xC9{qm( zLH$wvG5vAnctG%oxd=@KYv;Niu_CRSLff7e`o%N z{Gt3U`S;{+&wn8Q@%)|n`|>}?|Ez#1h$xU1m<*P+xdy;njt=7OpQGDBM`Mwea4;`wJf| z+)?;!;r_xe3V$m6x$u|5--?)`kfQJ+NzwSCiABjpsYU5USw&q%3yZEPx~}NPqP0c0 z7Tr;FSJ7b6rlKuH_Y`d}da3BuqTNMr6uninr)YoCfuci2hl}1X`mpGD(O=W!ryHkt zOuu6K_UW%r|E4&iSXyi^ZY*vto?YBpJg@koVsCL@@#5ko#mkB>E?!-{ruh2en~K*J z-&*`w@vh=mi(fB(t9W1Wf#Uayj})IQK2`i<@lVCSl%SG`lJO?XCG$$=mnI*l{%Hy|MVO*Y6HGCtDW*76f+@!&Gs)r3tS-|+ z(_+&S({j^p(?QdFrX!{g%oEMY=2UaKIm_H_?l-S7UvIw2yv}@^`A+i&^N@M7d8_$e z^Yi8x%`cl@HSadRX@1AN*Sz0+(EOhHi1`;wjHSRb+rnAyw!CgRX$`ifTXU^>a4VbE zT4J?X%dM5xDr=3^VZFfGZk=oGvUXd0tgEfJSnsrMunt+bSnsiJw?1Hf*805lE$cq( z0qc9#53CMhX-6qnx}o&N(i3GNW%9DRGH=-pW%rluD0`}GXW4UQua)gB+h2B|>`>X^ zvZG~Rl$|R3rtG`2AIpBWO|T`|(rsC`9Gk+XvT1F4TbXT!t6zqFUn7qe^dTL`OoFQm7l2yt(a6H zt&mqJD>N1Qio%NG3OEQ+QC3l2aY03Ug{z{oqN}32!d-Dug|}j1#o~%373(SLk=g^r&PP@E%s;Z&)Ij`U$(zy zf5ZN^eXspp`yu-g`)T|4_8;v(+kds6vHw+ts)#DODyS-?Dx=C;bz#-jRS#4hsQRsX za<#6yzIslzv$~^tZne95arM&b<<)HUCDoTzUtfJw^}6cYs_(46tNQ8c*Q(#D-c!B5 z`n~F-)yJwosy!HP)!1q(YN~2#YwBy7YFcXM)HrK8 zYC3C{)vT!DYA&t0yyl9U)irBsuB*AR=H{ApHILNntNFQha&1vMyQeRex3eHTBok-&lV~{ay8g^_%Lq)NiZbRsTW#uMIg3c?|^( zMGe-5%7*F&M?*tHQ-iCayTRSyY3OTM*07@Cl7`D0Ry91(@Ihlp4>}z?qz5@fA}PMc{%6Yd%kDiH|s3i^(tIA z%mH5q0^kDzfgA*ZSP%!|ffgiyM4$tDkOZH6 zC@>mSfiYk#7!R7j6yN|af~jB@m<#5C#o!gN1ndAi!7i{HyanC{d%!zjFL)R11N*@N za0na$9{|Tk;A8L!I0-%lUx2gVJopOS26w<+a1Zt`~4_%-|neha^Ym*CIvI{Y2p zg7@JA_&0orAVSC+36KvGA_?+CL8uc7N6{!2Vibogs0+$Q-B2%7jQXMeXaFif15r7$ zqY6}q>QMt~MB~s4=tbn1il(9IXfB$Eoakk=47H&(Xf4`;wxVrlJKBxjL7$?}&?$5p zok5?YFVIlxvYBp7ccurE!}MhGm||uSGngr7 z>`Vny&D1b6n3>EhW;Qd2naj*$<}(YJh0G#mF|&ecW!jiGm`%)1W*74@^9gg3`IPyL zImMi2&M==dUodBxubA(dADBzbHRfmL7V`&loB5l0$TBR;daxbYP6OFcHjGuV;cNsO z$wsl!teTBsHEb*!$Huc-Hi1oL)7W&jGuwsj%39eBwg+3l7P3WbG24&r&z7=f>~OY% zt!8W3TDF0m$WCE7b~ZboeaR_j(;4zH33Co)n+2W8hs1+ONoS&Ve$PG{9yDoEa!O)c zXi|!9a$M*nogtBaF()QYo_rbzfiDmN14sj!^&lA-feEAlGq8YE;!8wCOeDmQ_>+M3 zKn*&BE}$#0(yL4=T}G@VgJjdIZgibu&r;`?o|?EkPzW?@KtAXL`ho%yNaQ4l1g`-_ zpcwQ6{fUCa5DkeZYMVIJxwV7LTid^;ZghQZ>2SL<%d4BNeMdvVa9)lL32?ORon_3-9X z_O(BSwaSBAgMz}r&I7CGypFk z0ScNU(`i2JLjfE})gM6>YycBz-g_0S0k4BMz*cY=oTAC>61WPkfm^_EhvqOZn!iNQ z&%F+Uj~5Sn{Q39;c67rv4NP}Vk!n1^3@{Unb8eQ-k8%Gn8_cnZ{qqWYH#U@(RoSyD zhc{H#RF~FGXalo(Y#$pj_Nfuu@tqGA0Zl7d02Y#164y#MUqCG%Ozrh%2MttF(}y^6 z>gyZr_3a!>!CIi%1eSs2RI?S}HLwz_0;@p_Xa#Lx4bhSWl1Ow!Pm+j%BoiYskrZOy z1lED|^tBE|x?YiBv+HUbv5++Svd}2tU!6%8*Hu^E^CUT0*_kyvE_sWg;~;o%HA!`= zdlb+7BpPOke8lNZ`nXV2vT^d{@UFF8UEsHCE%^+b2AURdie$EcGbD=w6wooo0nWLW zec~zYb>-;Ct``HT-381zaWFNe_}kdXiowm-HriB%kylecu2- zQLp$JTn9J6P4El&m0tZ0Zjk~~M2bm2(w__U!6dJOAhuC6l_pTI=kkg)}M}3L8or?DbA)(Vkicr(1kJdgkFR43UjKRZ{5nLnX6hXD~oyhn&XoM*(&_wK{`W&>tRG0?Sse+wh7uXeAVFt{ESumTv zcZWTkfl*u3qev40q=HPO@8igXFJMpD3+7Um-Y^g5!#=PtEP#dJnsbitC_xPwN$N-= z89|Di*(y^H*dL~}!U3>^RFW!EeHspiLnx~iT!X^~=9Js38!9U*?RCX249O_E%hAtf z6*tVoVY&01s1w@??KI-giNgB2rHtUEkTE>dz|!i`l_Tv{we~u<`+o?lT6u3j4Xf#T zHLwJg-Y)Z4_#&Wpw{=gJgsv$kn?O}V|cu4a^b$5WtVP*%U9 zZk1Jbw__0M7|+~rQ@sFRbWRY*jpYtHc6Bk+qS5p^igpA_HXxD2iLc|4|$O|+Fi%Joi};c*$lT3Lb!jpj@wh(y=k}04_O9)$0~L+~(sA0B~6;W79DJWi&O>0}0(NoJARWDc22=8^du;fFL& z(|>ppeo7NmBs>k&WPvMFcP1~9Wpv@rWI03I3-yj0IelmlUF3u8d$N#JyLGt?Q`W#M zbf5G^7EyZsRW0t&9`iTgFSNwNo5a}yejZnRatF!#gM&Tf=b+LK7uK$N%fOr{&*>bkq4ED7{roS$%v-i#cLeTa@PW@nHIrNB&- z0zP(zg;zKig=+$8#XjN$vO(-cXv0Dy3#FnoXJSOKXB$dKovC7p5sB>`8tRHNXkkQF zvatnak~e6o-Kq^rLEXWzR@4LKkWFO!ll+cyQ6C^{L%mTR$|rA<&16d(>I>AUkZh$M zw2j*SeyGJ!Iqccmiv}UvznF%BNK{64P~Y20!faxZtBBF+HNsxU7qIZIgAYC@V+5+A zeJUD>D$yu3n(QWTk+;d7HE0Z~Msie3-XRCbI`SS-56HDUXT)1HXgq4>IhxP}G!acA zd&#?GAKAZ#cGcJ5pEL%f6z8S9+dF@apW=sRpjkW&Gs(dg>gIgK;j{`rG#@SGZ!aK+ zTF@eL*o}?@(R9;_UO`L9`{dYDbe5x)|59`nMW=-vp$i=)VRUm94gD(Zv0lRQcyW!)(drmYY+St7l-{{ZcCXyteF}>ssvE1SY~oIIa^VM; zP=0FRhn7(OU!QDDq<3`5p~bemLQ9IfzByT)85xYu_jLZw&a9`TC5d>oBoiMZKAmFL zSyI!SmvuU}1MwyTr$V1>>1xf$92haYs+d{bQ1Ctl1==-rYq-1`jFVJ*1UWwu7729dCmC8;_|Xib+z^_ z7@Sx5w4ZM6o8|OR8tN#6BjId%V6cLo6Ksbk;AixFfIktSxeWeB&jfD62PlwE{aTbr zqpgI__@n7`@1X9m2rWk~Xdn8P4(vam%XCB!#+|U14&+9hiMzRs3Z4uCMegtI@3@$J z#7Cr~9=(O$9`dBe&7!tzqqp{easB9a>9_Wbw-+4&n%B|0bjAJX06K`?Lx<2|^gj87 zoFt!;&&Vlqnw%k@lP_MU-f|3mfR3XR=tJ}o`WSseJ?AV3$8m512j_6Gg@dgeT*JY& zi2CtD!%YP`kG`h;7WxWZAm_-Jt>_!{EjiCYCH?(U@-XL0qeP1?p{qR2WpssnMZRuD zKcb(=w;U7^^)p2u-ECUc_0m4`8=1q?If5~)6eJ!$_$yD031JD{6bP*QKNqO$04MXr#`8?=D98*dzj)wHk1Djbd@a3qex z(c~uih5Sl>Bfpbd>u?O#AUSYc1J`I_`h$ZVXfiCK|LgckBRsRFs;acM-d-N+!Yz~{ zN}J7w389&Ev=~uSS83<>N9Pd=%|D4;zeW7dy=PU{*H)EIpu>yZCU%5BL)X4YztXBk zI?+BurhJC#IUDjb5Eb(TPl{B#lsuX^o)O~4$pnsV5&Pp5Y{nLxiqjl89e2iEa93=_ z88jNRa5nCSyW<`>2lphm$z5`f{7L>I_sIkDH+e`NaS(8jo**I)Vh%DKWI5=;K~D~P z@w2E1_r`fRzl3JjdUx-9$0pW7H;DPQF2goAXB6k;X4!IcG722lf)ZP|`~q86PGM0& zPDXK2PJW)N0^`28fL6ID0q<_b+vx2=*U5?dd;5z02wf)<=2p_dpn61eb5;qJZp+N? z-N#y#laZ5~Q&eK>mt9cklJ4GQ3D4r_^e>*`f`aV4qJL5k=BXo|64U*=*Rba1=J&T| zK?coJJ-wzYt&{T%sto;(=#p&e=I2$w75 zbm7oZXfLfBJ~EV_+v=N}+qe8*k|*;VW@~M2l|3}Gsdb^(n~d$K#znfWl3k+HGjUi|NuX#^IKeOFtS*N7&s0vh!U2)Gark zI&WTgTc7+K>X7Yw%cQ>Z1f3UoX@+)b|Bo;}G*+@msWCz`O8n z4$3(g)QaE60UQjb#Z&Fvme!>oK8W9QA0xH1ypNC5UL7C7M`3sT0S7yBFqF`~MYRT> zz#rm|@W&jC;GmgF$U?c~lI2g@A zH3wrjsNrBN2je&x&q3`*{Ih%D#=p1+ZhQ++vIO_U%|V@eEy2*Tl=yfGXIMIg)A`-??<(zld2jCz7$3%$&ftuYgZdUm#K9!@ z6h4&kXJm9@W&+?H4jSkf&IB^lCdnK${%a5yzwGi|hISux_I_*{CX@+xjogfiyvos2 z4MxsTKQK}mbcvcA*$5_vF;Ju!4HL`6G4YI+NnjEg9iwNGIB4NuDhJazn9jk@9PGlu zt{k*-FoT1c8<}J`QjFP+6qC*)mDP?^w|1m@@kqH2Co-8{JTAE$%x=de|Nq6MA2Wc* zr9TI|w=g9fJ=1ZWPV`}hFvIxMi93vqyu!g8dX&JFk)<5$`EOWWY=>e5Gs+D`B@YEX zyn4#xMxvIP#3M15sblJy2Bwi2$Bbv1mU?MMuL28ow=B%B;Qo^+qnFt7aQIZgZKUt?DBNUY@GpcZB| z2M4>62w~PR>)lAK$C zcbL7*yUadjKXZUN$iY$$mT_=62g^BV=U@c~M{sZ?2P-)^Y9n*#vD!x;t9^o3dvv?n zHSKCQJW)G`ImfI0B?sw=tlM2LP_>z_!8PVvaE*iXM3sZpd}xRR#bNErUuLfI%3tAN zZ42`w2gkaVFJ!JW^z4dvgPXkab-Z`~=JM|PfBAugX@88z++qH7BXW;Nr13c}-V*|t zM=YcZvVeo*TUZ*#O?1JJ3PZzKPu35}HnLu92iBVvus*Di^<_n@n5DUyddVc`ymYyK zBkRuw(1VIdHjtIGL2NLq;DMP+A9YhCXdF%F;0zAVEF0TP0qWHPpq9ZyRxf3JmY z;$Yk3rx@%cdUWifq#Zx4-K^tr?e5Eec>$)Zqc!OWo%BCoXRtH*s??EuMN1P0*Ku$? z75_{z+2SrH(r50mi`@4%JYR!^&R+(}9C#;w2OpzP+t1T7bA=X{Yxo!Z8@`2a(|=VFdlM`SPw@Rj{=Wj9;F`D9^*YG zcuew`?BVd3=`q`5uE%_jg&r???D9D7@s-DYPcKi2r@yDvGte{0Q{@@q8Re<=)Of~u zW_tGbto5AZxzux+=W@>#o@+g~dhYQ&>iMDP8P8ulA9z0Uf?kZ5hnJUExL2H4j#n?Q z-W@*caNZkxdwVB&FLijgc)#Vn-}|EXP48RYx4rLrKk$Ai00Jb?2=WDk1&xC7f(e32 zg2@7hfD^nZm?oGZm?fAam?u~#*dW*_*d*92*ecjA*eTd8cw6v};9bFf!9l?p!F3%xs6(1LW zDE?S{Q{pYrO1eshO6nw2Br7BNsqMzQ+ z;Aiwp@w50<`8E1Y^lSE;;z#`E`7QNZ>9^Xi)o+d8R=+)d=ls6*d*I)}U*IqF7x@SK zhx@Djll`s!z5ILo=ll2dpW(mIf1Upu{_pvp@c-QZtpAq*l7QfVsDRi2Q$W{%(E+sq zuLf)g*y;#)H(-Cj!GJ>n9|n9D@I}CNX^1pZnkKbM2S`h$<%jd~okuQ-ilefyZ$al(j%ing$ z_sI{)-;*DfAD5q#|0e${$S)``C?UuiG%#pz(9ob^L1jVZK@~wYLF0m&f+hwv2Tci@ z6ErVqLC~V0l|ie6T7%XEZ425Fv@7VXphH1F2czJ?;Mm~K!F_`(f+q$y2Tutm!7l_) z4W1snAo%s*t-&XPzYD$*{A=*g zZi*g?o{9kqyP`?)f?|$hsbZC)MbV~MtJtL2tk|m9uGpzKsyL~*pt!8Ks`yFqv*L#0 zj^du;FU14J!w?YCA;dqVV~8;%GbA@8FQiXMK}b=^kPur)X~^&pd&r28X(6p4M;syF zg!~+GGvwEhTOqeY?kWXJxl*B2DnpejWrQ+CX;pSp_E7dz<|<2+!<1#pa%F{bjIu^K zR#~rXR8CROP`<2uRr#87m9j;-LAg=6Nx50MRr!wcu=1SpqVgw|x5`H)RfVfGDxIpc zs;{b0Rjlf-Dp3tm4N=)tr7Fj8)i_m?YNBegil|;xO;^oQ%~dT>EmCbz9Z{W9{S+P& zt_|-MJ|w&;d|LSI@VVjh!=2%a!7%%WNL%Fm$R&}>9g!;{S4M7$ z+!MJsa$n?u$U~7IMxKlOHu7TR50RH6uSedD{5A6T$Uh?QN1>?bsLZImsQys{qXtI} zjT#-*6g4rbIciE2iFzSwYSf}AXVfcEOQV)Yt%zC~wL0o()c4UHq7$O?qZ^_pN3V~7@ zQm3f9sC%gUsteVV)Kk^d)f?2?)H~GY)jy~&$9TqwWBg)_FEarI3&6v9}e`-2vqBSv^-kN@z0h&h56b;d=(`?af)7;S9(cFu*#Ae23J7Sw- zr^U{Q-4lB#_Wd{}P8jDKCyon>Gsks}8x}V%?!~z2akJv)#x00j6t_EWf86=Ft8q8u zevA7f?rz+}co@&bd&YN&Pl+E9zc7AT{EGNh@h$P&<9Efs9ltkzzt&4D(RS2^YooL= zTCG;6P0}W7)3lk|0opOziP}lpX02n2c8>OC?Go*B?F#KG?FQ`|+RfT++MU|n+QZr- z+GE-i2_Qk3AWD!V1SFIuR43FX)Fm_~Y)aUZ@JYgF31w=PE4Luc1j>&7~C4Z3kU zhi;~Bwr;L&zHXuJCEY4rt8T4sgYFI8o4OOav$_kqZ*>=SS9Cw=uIp~<{?7(@;eY`$F-&x;FKTJPdU!kwmSLti?b^1nqlYWwZir%SzMZZ+PT)#rU zO5dVy)34QU&~Mal(jV75eoXR7(k2xoH6^V`+M9Gf>8=47u))LNWsn$x4Ize(hERjb z5Mj_63NNRCX7PL4^AOV%bQCihOR zN?w?}J^72|2gXoi7voUlB;!=$4C8F$T;pQnO5|yR;_A&dK1I?l4aC4+tZPu9M%vy7zIo;gVoN4Z6&N25gk2E)$Cz_kh4)av=4D)RB zT=T2umF73hTg}_eyUg#H_n8lx51BtRe`CJrFkdoXHD5DdH~(c}Eq<10i^dXfNwg$c zj25#c&CI0WxR#6EVZ;*-nE>xT(R6w6{o6G zt*N=Gg{dW}Q&Oj;PETEwx-xZr>c-SJQ@5t>NPRc;K+jav)_c~!GCVUn eWC${RGZY!hj7}LUcXyAF`;&FLKil6kYW@$!dEwyz diff --git a/qt/mac/ankihelper.xcodeproj/xcuserdata/dae.xcuserdatad/xcschemes/ankihelper.xcscheme b/qt/mac/ankihelper.xcodeproj/xcuserdata/dae.xcuserdatad/xcschemes/ankihelper.xcscheme new file mode 100644 index 000000000..998a3113d --- /dev/null +++ b/qt/mac/ankihelper.xcodeproj/xcuserdata/dae.xcuserdatad/xcschemes/ankihelper.xcscheme @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/qt/mac/record.swift b/qt/mac/record.swift new file mode 100644 index 000000000..1c22d71ed --- /dev/null +++ b/qt/mac/record.swift @@ -0,0 +1,117 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import Foundation +import AVKit + +enum RecordError: Error { + case noPermission + case audioFormat + case recordInvoke + case stoppedWithFailure + case encodingFailure +} + +@_cdecl("start_wav_record") +public func startWavRecord( + path: UnsafePointer, + onError: @escaping @convention(c) (UnsafePointer) -> Void +) { + let url = URL(fileURLWithPath: String(cString: path)) + AudioRecorder.shared.beginRecording(url: url, onError: { error in + error.localizedDescription.withCString { cString in + onError(cString) + } + }) +} + +@_cdecl("end_wav_record") +public func endWavRecord() { + AudioRecorder.shared.endRecording() +} + + +private class AudioRecorder: NSObject, AVAudioRecorderDelegate { + static let shared = AudioRecorder() + + private var audioRecorder: AVAudioRecorder? + private var onError: ((RecordError) -> Void)? + + func beginRecording(url: URL, onError: @escaping (Error) -> Void) { + self.endRecording() + + requestPermission { success in + if !success { + onError(RecordError.noPermission) + return + } + + do { + try self.beginRecordingInner(url: url) + } catch { + onError(error) + return + } + self.onError = onError + } + + } + + func endRecording() { + if let recorder = audioRecorder { + recorder.stop() + } + audioRecorder = nil + onError = nil + } + + /// Request permission, then call provided callback (true on success). + private func requestPermission(completionHandler: @escaping (Bool) -> Void) { + switch AVCaptureDevice.authorizationStatus(for: .audio) { + case .notDetermined: + AVCaptureDevice.requestAccess( + for: .audio, + completionHandler: completionHandler + ) + return + case .authorized: + completionHandler(true) + return + case .restricted: + print("recording restricted") + case .denied: + print("recording denied") + @unknown default: + print("recording unknown permission") + } + completionHandler(false) + } + + private func beginRecordingInner(url: URL) throws { + guard let audioFormat = AVAudioFormat.init( + commonFormat: .pcmFormatInt16, + sampleRate: 44100, + channels: 1, + interleaved: true + ) else { + throw RecordError.audioFormat + } + let recorder = try AVAudioRecorder(url: url, format: audioFormat) + if !recorder.record() { + throw RecordError.recordInvoke + } + audioRecorder = recorder + } + + + func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { + if !flag { + onError?(.stoppedWithFailure) + } + } + + + func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) { + onError?(.encodingFailure) + } +}