pax_global_header00006660000000000000000000000064150306553610014516gustar00rootroot0000000000000052 comment=effff2b4e298dad0afca77a6aee110a6b0d7f501 voip-utils-0.3.3/000077500000000000000000000000001503065536100136345ustar00rootroot00000000000000voip-utils-0.3.3/.github/000077500000000000000000000000001503065536100151745ustar00rootroot00000000000000voip-utils-0.3.3/.github/workflows/000077500000000000000000000000001503065536100172315ustar00rootroot00000000000000voip-utils-0.3.3/.github/workflows/ci.yml000066400000000000000000000012221503065536100203440ustar00rootroot00000000000000--- name: CI # yamllint disable-line rule:truthy on: push: branches: [master] pull_request: permissions: contents: read concurrency: # yamllint disable-line rule:line-length group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" cache: "pip" - name: Install opuslib run: sudo apt-get install -y libopus0 - run: script/setup --dev - run: script/lint - run: script/test voip-utils-0.3.3/.github/workflows/release.yml000066400000000000000000000011761503065536100214010ustar00rootroot00000000000000name: Upload Python Package on: release: types: - published jobs: deploy: runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/voip-utils permissions: id-token: write steps: - uses: actions/checkout@v4.2.2 - name: Set up Python uses: actions/setup-python@v5.6.0 with: python-version: '3.13' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build wheel - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@v1.12.4 voip-utils-0.3.3/.gitignore000066400000000000000000000002561503065536100156270ustar00rootroot00000000000000.DS_Store .idea *.log tmp/ *.py[cod] *.egg htmlcov .projectile .env .venv/ venv/ .mypy_cache/ *.egg-info/ # Visual Studio Code .vscode/* !.vscode/tasks.json dist/ build/ voip-utils-0.3.3/.isort.cfg000066400000000000000000000001611503065536100155310ustar00rootroot00000000000000[settings] multi_line_output=3 include_trailing_comma=True force_grid_wrap=0 use_parentheses=True line_length=88 voip-utils-0.3.3/CHANGELOG.md000066400000000000000000000010131503065536100154400ustar00rootroot00000000000000# Changelog ## 0.3.3 - Handle empty lines at start of message ## 0.3.2 - Compliant cancel message ## 0.3.1 - Add cancel_call to stop ringing ## 0.3.0 - Add support for outgoing calls (@jaminh) ## 0.2.2 - Always set `addr` ## 0.2.1 - Use Python port of deprecated `audioop` module ## 0.2.0 - Add outgoing call feature ## 0.0.8 - Close RTP socket to free port ## 0.0.7 - Ensure payload type matches everywhere ## 0.0.6 - Detect OPUS payload type - Update receiver URI parsing ## 0.0.5 - Initial release voip-utils-0.3.3/CONTRIBUTING.md000066400000000000000000000002631503065536100160660ustar00rootroot00000000000000# Contributing Our contributing guide lines can be found in [the Voice developer documentation](https://developers.home-assistant.io/docs/voice/intent-recognition/contributing). voip-utils-0.3.3/LICENSE.md000066400000000000000000000261351503065536100152470ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. voip-utils-0.3.3/README.md000066400000000000000000000001641503065536100151140ustar00rootroot00000000000000# VoIP Utils Voice over IP utilities for the [voip integration](https://www.home-assistant.io/integrations/voip/). voip-utils-0.3.3/call_example.py000066400000000000000000000123261503065536100166400ustar00rootroot00000000000000import asyncio import logging import os import socket from functools import partial from pathlib import Path from typing import Any, Callable, Optional, Set from dotenv import load_dotenv from voip_utils.voip import VoipDatagramProtocol, CallProtocolFactory from voip_utils.sip import ( CallInfo, SdpInfo, SipEndpoint, get_sip_endpoint, ) from voip_utils.voip import RtcpDatagramProtocol, RtcpState, RtpDatagramProtocol _LOGGER = logging.getLogger(__name__) load_dotenv() def get_env_int(env_var: str, default_val: int) -> int: value = os.getenv(env_var) if value is None: return default_val try: return int(value) except ValueError: return default_val CALL_SRC_USER = os.getenv("CALL_SRC_USER") CALL_SRC_IP = os.getenv("CALL_SRC_IP", "127.0.0.1") CALL_SRC_PORT = get_env_int("CALL_SRC_PORT", 5060) CALL_VIA_IP = os.getenv("CALL_VIA_IP") CALL_DEST_IP = os.getenv("CALL_DEST_IP", "127.0.0.1") CALL_DEST_PORT = get_env_int("CALL_DEST_PORT", 5060) CALL_DEST_USER = os.getenv("CALL_DEST_USER") RATE = 16000 WIDTH = 2 CHANNELS = 1 RTP_AUDIO_SETTINGS = { "rate": RATE, "width": WIDTH, "channels": CHANNELS, "sleep_ratio": 0.99, } class ExampleVoipDatagramProtocol(VoipDatagramProtocol): def __init__( self, sdp_info: SdpInfo, valid_protocol_factory: CallProtocolFactory, invalid_protocol_factory: Optional[CallProtocolFactory] = None, ) -> None: """Set up VoIP call handler.""" super().__init__(sdp_info, valid_protocol_factory, invalid_protocol_factory) self.call_end = asyncio.Event() def on_hangup(self, call_info: CallInfo): """Example implementation of on hangup. """ self.call_end.set() class PreRecordMessageProtocol(RtpDatagramProtocol): """Plays a pre-recorded message on a loop.""" def __init__( self, file_name: str, opus_payload_type: int, message_delay: float = 1.0, loop_delay: float = 2.0, rtcp_state: RtcpState | None = None, ) -> None: """Set up RTP server.""" super().__init__( rate=RATE, width=WIDTH, channels=CHANNELS, opus_payload_type=opus_payload_type, rtcp_state=rtcp_state, ) self.loop = asyncio.get_running_loop() self.file_name = file_name self.message_delay = message_delay self.loop_delay = loop_delay self._audio_task: asyncio.Task | None = None file_path = Path(__file__).parent / self.file_name self._audio_bytes: bytes = file_path.read_bytes() _LOGGER.debug("Created PreRecordMessageProtocol") def on_chunk(self, audio_bytes: bytes) -> None: """Handle raw audio chunk.""" _LOGGER.debug("on_chunk") if self.transport is None: return if self._audio_task is None: self._audio_task = self.loop.create_task( self._play_message(), name="voip_not_connected", ) async def _play_message(self) -> None: _LOGGER.debug("_play_message") self.send_audio( self._audio_bytes, self.rate, self.width, self.channels, self.addr, silence_before=self.message_delay, ) await asyncio.sleep(self.loop_delay) # Allow message to play again - Only play once for testing # self._audio_task = None async def main() -> None: logging.basicConfig(level=logging.DEBUG) loop = asyncio.get_event_loop() source = get_sip_endpoint( host=CALL_SRC_IP, port=CALL_SRC_PORT, username=CALL_SRC_USER, description=None ) destination = get_sip_endpoint( host=CALL_DEST_IP, port=CALL_DEST_PORT, username=CALL_DEST_USER, description=None, ) # Find free RTP/RTCP ports rtp_port = 0 while True: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setblocking(False) # Bind to a random UDP port sock.bind(("", 0)) _, rtp_port = sock.getsockname() # Close socket to free port for re-use sock.close() # Check that the next port up is available for RTCP sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: sock.bind(("", rtp_port + 1)) # Will be opened again below sock.close() # Found our ports break except OSError: # RTCP port is taken pass _, protocol = await loop.create_datagram_endpoint( lambda: ExampleVoipDatagramProtocol( None, lambda call_info, rtcp_state: PreRecordMessageProtocol( "problem.pcm", call_info.opus_payload_type, rtcp_state=rtcp_state, ), lambda call_info, rtcp_state: PreRecordMessageProtocol( "problem.pcm", call_info.opus_payload_type, rtcp_state=rtcp_state, ), ), local_addr=(CALL_SRC_IP, CALL_SRC_PORT), ) protocol.outgoing_call(source, destination, rtp_port) await protocol.call_end.wait() if __name__ == "__main__": asyncio.run(main()) voip-utils-0.3.3/mypy.ini000066400000000000000000000001301503065536100153250ustar00rootroot00000000000000[mypy] ignore_missing_imports = true [mypy-setuptools.*] ignore_missing_imports = True voip-utils-0.3.3/problem.pcm000066400000000000000000005650001503065536100160030ustar00rootroot00000000000000    ,$@4G]15JX\7]8#S^4n5R-z 6LB1H>g?OQHDoGrG7,S9p/UB"']&5E:G*ENXPf}Qa-g6e{%2/-BdoEcgAkMty*Pfq {PooSR ` X )4G+#+++"MVJ&צϫٕTJۄ8J: c   zU +(4jH&7*9hY8=8 ` } |@UK~1{xcMf? !W@4A[  > K>yoC':7w9 /$'$),++)t"`s 3*ѫ^߲ؓ5Z` / U,-  " P{CN {hb 9! |u~o t;L&;/28x(TR$\KX#bK;(`] WOXyf&c9vB:,&%0H4.330P%B /i#3"{̩>w7F C k X (L N'uXE+t H)W")+<)k$08V 3 l_h`xUS[>9L ~v!Zt+_x`R AC%KN#.[119x?j> ;6,\eX85_7RԌɭƱcލxdVa E|Pܘ*؆;>]g E` MICT $q')*H)% 6d| ['T4P1o|]!8b0XGtbJ5#:Y ] "%b3+CJVD5*)I-3X;?;, r c^WR ʇPհMUG +J QU k)A:Kb="I }#(M*'("I \ bHf =K bod,AMpn"_$d_ &Q2 bK,9@<1't$&+166-s.35ktͨriTg']eL D  E . j ] :W ? cK+ /gxkyqXG&V2^pfo BA|yV\9 jjs&,-)$ "'?-0/+"x$cCZЗ:lF8ם5er^ lP$ ^tA'WQF[  EN& o_cdMvOQ;|,nnz`?a,Hxz$ Go ""!  $=(++q' \#;`;+"SBAZ RC8O #XhAQ2V~<8;KegX`;!ofm6[S+ : %('#0!!$K*/33m/|&b5.Qo0'Ѿf Hd:.ߓ( V 9I[ 7bp7a8~iZ xt]p 6vFzwJj' \U=O l[/a _.mU~cpET  E.8:Z4,(5,=4<@v?j7u*zrys %EƨIIͬ.ւן 5L |K }GD J \ f 4 W 9 F M L4$],JB1JKRVm8T 2Je0%i xzc+Vr` y y ? Q ( [ )  Zae!@SV]Fi$>Dcq&FBj#<Fr0j:#%"?|ZV')Yd a %C7C5 } "U9 ~ * k IM  V YkDECGbbGj,:[Pf@#Gggq:Dt@ e0\|QQ?OyTD [} n  cV vR&] P *,bHS@n 0( " F`YyeI pBe9e 9gR EDbl a %v,',*+[^Q& Hhm ]yC{&'n~ +6ff6Bp k 8Z;nux JV l[J5_9V&z\xEo~39 !hoX irb LI8TpH/ | Is @I&`]3O5EHE(g B:] KrhXt 5 His Oj'xv `G{d ["$'P9qRW}Xxp %\o[&q N#  5 (h 3},u!Q Dj\X}KwrK `c *- s *l8# ^@ r # k^ ~i_HI0# 6~ )p  fF0Qc$ dDL]}rd_J6xnyq iEP D G( W22!# rELAz l1iAmn[dh 73onYK ;  Mt < cev  VGI]e tI&e t/ V[;r)NTd, xjEv% ' u  BM=H "k#"$`|>} 'Vu~[E#ޓ=buj +5=*E&VqGq JRs  u"^2G(ntO#`'~sUO%?*' *2r522F5T5e2240# o5ՉЉ#̈́#|D+ b[LR )l^;UPڹsy =v[ T?%~{`AmRH4ANlqfX  [ 3 n+2_66786\2^,(k'$Z6ؗ֊Dډܢ&߆XkNf yTwE gyMo6~-O8xrV" m+i iT_pG'o h:pdts N zz34., oDEr.1kiE s >jR(Tg$c+H/@Dz{'Rqyp^if]]_j6W;=4 :yl5R2*\3AUe|?he4h  " Ws[ l:uzwC(<N?03(vg Bau LMv01 5u,~~I/#_zl8,W) 0E JbI;}-qo;a_]$t M[=X0Y;rBff9& 6v=Z4l]N$2'1u0G/+&:wmn3Wy`9k5 +499+{48XB?b\AU2kaS[87:Ko?|e]"Gf8) %  7aj <r/nLz;;W7{Z8*10>g =mxfb^ZK1 n7QIf-/wd}+1>v;tgadmz*g9oG E&+%uH|`C76<7 fDhP; 9o PAE5JSVY]]WE%^ w>G[4 '>^Ej:] $'!&'fZJ.d9hXQM^z'Ty 7]~kU?%"uc\S@2$&#+3>GTbv,0Fcak{}skj`SMN^UPQ`vzy{q_T8( gjHeH kU){ _^,ijp0)~7lt ~M koQN x[ :;a# k zb[x+$CM- HB b(_`-!nD|=X2Q\a4!"} i w_{]k.QApF 8u5E3]Q\~Q{`1=@D4|Z8 JtYW7Ep&)@_C36#:4L=u" Lo9Q/AGe&'y h -(X5'i%\G[I sLy +(` >]KqlbNr| yHh M\ B -kw o0GU#]%df+>  r9>tU W tt/ h##"Q< Z2b;n fc=GBcAv7=5FHM2iYC F DY  L 1%<)1K773-}(%u#G!e v5O:W"ds i UPxm:;"FKg;Dz:7Gwxkj%D|4 >< Ya $2<@A>+pvSw0< 3 ` `V,zzr#.9B!IXLKEFPe-tygr&dTB) +i!sɁt(a> & ic[S߱\6+ , LmTN'@jQ?} # QV5 AP&1N@LVMlw1v#l``SA( `Į¢'^ BLmA[c  ~[O0ݼݶ&( 5 z 9 +mJ*w dCR - k  \RIg".?Se2nkd][TD/.gٞX~X6–̞@Apf -   > ">o$Op 0g.G/[hI^g [ %ve <~, [J|;O _3q%l-I24563.F',%m$S"2RIvVW " @wnm4y"md^yVM&oo 'i\qe`acCB5t|G"`7.O|*  i \ 9q*#E_+|A0Et*KEtRGIYc``pf[X6y  U,1Pwcv-h9 _%-ek3~8 t A X @ y  6KPk225Wl\7&k.j= )SM9/#;ku0s\\Y^;@gsE 1EuX=}+P!K'P ,17=f(_q?al9W[9BTR,ks? ~4~,g (LP.$ !__]c ufJDPP%%T^; j&8* A#er#{:&;enM7b^4iE|PLmisWlHA+e\Lzh2hV4:^GD_Io 41;8iNO +Ls_ZB~qZ OPPe Rg &s<Rq@ A_m*)!{4Bybsx/.| {["Jq%F 5  t PVhI<v6\zqSazH/( )_DIU[]*K`y0oPR s l% v6   CO9 r=&1sQ"BT')Sw"=$t\\^mm'm3 E . ` 2 O G 6 ^_|oU4Vm*eB\nj,%U?+(`^sNJD; HzGhbU 6[VTGy @[_3 78>k%NTFK$T(m<%Rhu5|xkGUL -?2{"h7Er%~!3 b!C;%O5eY *;g;IyNF/vWB+,RKbmS5wJ w h W%f "e&0*v,(SR7hX(v*. t!PD!))1OMc9ds _Y@^]cy:1}SWp"p&-Jl6M#:0Kw,Y-0+Q2 L/X7;2\-4?>/   wk6TܸS_փ`<^AlTz~ Ir?P(Xit3UY;GK s"FSo x]W VN] qTB{TT |h u2~mC| 5{g2P =^+Us]|4z T7  73 O S&,'&o1<:, /6E9P'gQ>G 3U /}[_ WmM .  "^ VAXTP89E0 0  =F  <\jc^ZxXip{ x*c u`n@  i yVn)WR 3!t6/7"~;p-sba!,,)G.S8h:/!"BE _Pߎwf7d_ 6AZ |7N9&A0Xhjf +]{DN O;vp [ ~ oM y $LEey<<(cL 5 T / 5 e. <:rz*7,}~h 8I'8r f|NCDU+^)/BI bsXv%%"m!"q/ xR \=zl5AX m % ( 7%rIN v:%5iM ;n= GeGNLob_Jub;hJe+q,'){ 6  dO6Z` bkqT3rd/JV]Fk P'Zj Q n . NGpjtC>? q AQ>=^kXCO% x 3 2\A|L. Vp u # : `zhVIplR@u)d _T@a-zH[A g 0.^BOAM6D"7X!&iG8}4)9Y2e7waW@0Cwvwu{4 B`jbbxdh+U& <F^ IwS x oI/RZxf t/|9CNIMf0'.o? *uS0Y;@Q6W!:3XP$\o_fIx1$$,F58,2AB60Hl}wx'nCo!*MGY+Zb,w|ONBQ*ljBadY9=T9ZNoo}j@1%FskB41$tO*OVpcf:^m )HT[Vmzg(Q e'  f ^  } RbDRI,{ZkU 6O/k+Ek%3{P3joJ'$236?8Fwyho1}N ;w[>looK_M{(ps4;A dG7,qF2B.SBago;]JwkXisEE\w4S6>#Q@@txa 7myNu>`"iwvLN%*HK =SnC*4wTr8#>t@&MqPF  ="cy&&o5#Kp<uH0 "Fby2V%)+v jH$`u{b=9(Ilj:vvYVK73??B9'T"(  2r}*RVpKEUK |  `Nc\ ?U 3]dCp|d-.g!O~EPR"f! #lgQV\oK&xWt;+# a$t_!tb*Lt+[ (y& "?p5_V1+Ctv9 c3.&oO|QrVJ j&Y=naxu;Fwcbo, @h@4;kq93w&1\ A[I@SljV\pnN53 {Hn)f9|WQ_`Um92\Y~?ndE@;vQDU]voHCo 'DD,whtza<4P_WRw!Qy :%2x?l3tp"eA4975MM!cRJpoF' :NKov5>RZ]~KL2r$rdJvYQb}*Lt#V+Y&Rt+WtFn ,JiwhU4wH z*r$52Q! |PB'xyd:&9ONDNf+oOds~oN62uxunkr.Ts:Wktu[H=(@sIl;/]|$m\ZwH.0Kk8sD%pZ>,|N@nx[C$cH6qW<mJ.cJ5 4OwEt 7\z~m]L>3&  ),0/1>KK>BGE>4--+  9b&m(^d"@i0QMeH0+9K`|.a-\9hDd +Imp\P;  pE[5`B4 $.>NiP(_(J[muzJ/%Nb"&A\`iad ydk6~b\ECV]"Xp}nN3 @yl1N#Dl{}9Mp~e@&OAV!3I;z]r X~"9, b*AamIILB^hjPP"z & } # x C  B 6R\D7%#4*K86l:}sYG6RxsQFr_*" B90z;38\RxT*_sYtlmM;zVFh 95]-h u] 3 8 cy|ZC> =X#a.d,}8sn CjY\OQ,FWkQ Y oJK .  q  n9Q^7d[+:\\Nh)m/.oT2Sr1$6&%]dK / = g 5|^!bnV>5m1Kv+)2O6R"I9uDXXGT_Cn4 ZXlN%XNG ! 1 KYe$?(Nz }e}iW@L~F!EbSABOFM?l I [  +   i J E n  o wXi^lINgwNVMfR^Y~4@a!Pg4;#jR{@Z5 2 w  8IPP j?: ~Dk ec 3fL?G:1G 1Gf8af,]5Cc0 ,n.jrj[~ $S&D"nhYE(MxZ0 1EAk=DP:6X%{ V@!?3iJ{>- =Xp[HqvRimfL5R6& Io@/LHxi>#0 ;pFn|]yq__u~X%M^Y^%m9+X]P\d g Q}7#?l*xS#Xe)-WD<2n/_5y[YqIo`?61;8cs3')1 2$ yOsMNjB_Phr\`T Qje'z0_O'<Pd&`fy%aoxZ[*` RW{k$&K|LhA Nxz+ 4me0#l'Xgk9{VT}Z@DVpH$ 8F>GuoG i*'@M`=W~n\J@8b(~d:N+SQk#<1 5P& eMl0e5Lu6O$:]K+_eVi}c([{Tt<s#RZE88thyS =\s-@9igM>24 X=*3L9Md%#iiL*u(qD EeAAg=crtv,\XfPE@ ~g-t]$SLbZ#yNY n7&1}RC i$LuP a?2Msb`n0UzZB X)U$"A?sAC]%|~kZ\vi :w = SI<,ur=g1m[]6[}u*Yl.p<  ! \ i$  ~ )1Q:~*]v + d  je=xI7M{`quK3   G jgw8=m]  b49Ddi +$"-%M%F1DDWNE5%-_* &o'hOI`yibPn͞Z> O_#T {{)J@Ex83$ 9 7 >! > z+d\WFxW ^s-Vf0gr )q@) A} |l&-;,P&4!~A %($L 9S$|p"No:&{K*4I$ZuW]7Db )BMw gXr K , ' , P . x< {fc2uE0-.~t#Q7+]tUDc p U n & K6?Xi(P]e;w!KimX!$DUmL1?~{GOS8C!m.B7ew+K%S+O!K[i}qI |lTSn*eG10(~@Bvm1&=r*:4.nzBT%YnF_w{.ahA$X6 v`Y4gQF[GCA K(h8>*&+7JL/Uw:VgdL'w2}LF V2:6;uvt,LqwuOwFFojQQ\Q="kK//|FX!T?/!?b.tAh^K1r{`6Lv!)6YN"AVwkJ;Q Ac ~zqg_YS5|HV@b"!G|x,n q<RvmS&y=p*A W$gI3'!0NdqS 8mP4^ qA^ a.v;sM"!;Yn*uYh R!DylVF:,~S5a;]1eF9, $.:;CRf{ 0MW(=MRYvrrG.Kl:2 n#LKQ;J7P 6 #S$4 Yn8L$:J4 5dfT`f9K@*2_a%)EH-H VO x0>+sQD  Ia k k\Zs9;?)UxFVi[Y@5ggL3/6& yw_x!mhmrYAaI!#v$=9spcJ~=2si<2 <lJ h%D;& Y};[g&[O 9+ a:i x:m#xW^HyE.i\2HU "-+mu?qmL;COEffm(>"";Q*X? |L;T &:^ IR9c` }yJ`OlP*E YiZ2U]8,  KJ * j N'J 1),k$Qc5&012k)x={`yMؼI-rc AL_M=ZL)Nvk;7!NG8 U R 9 s5z,>=-" v,;@? 8*h {ߪ/\ F-I>f~R^sajj CPk Qn ~L @ l p< .KTA-*"'!S'9HJ@;%*pS1OYLYشE:y.I;T qBK]WiZ"gn4o !,BW*b<)p87d Of^I6F.3?!Pv[R> *ƻ~ܽZ؝A% %'" a]b`&OPCj*\s(.*2!Je "^&"0 I14U39 A=@e[WK>BBHLPQJk>- ` ,עĕpDىٲח)p)33(/ k;Hf!zaѕģ;Qcl(p  )M*#p k rXFMuWI@MXpl6eXZ*)*MTz [S̾Aئ֌s`RiIK 8"x(+=-*" q4 T+B1d{bzS`SN[ DI"$! ,)SQqZzp " `  lBOqX~6K) OgJXXרMwwF]Z h=cPG h4 uӀԉgL$6. E,}_A } m@B& 4no`0}g Zs@#pc!Q-F34t2.a+*L.4;;3[T4>_ZR /oZ$(+R+(}%#$(,1120M(dd<ߎ޳xp&1 wH K .rx%('X%{#0#$',/c1.) Y /6i TO߻݀iܲݭ,eqk @  d5-g`M*s5:{r %W d!C+3SI0~v, ,5 M"),,+((( ,z0+440(j? R"m܈r/׮ބFXs^Mo m $^z@$:,nj ~xu5UkU O {W_5`;= PbStfA"*].Ud0uK}njW i$T/"67K3.+,04c7T6=1(d6EZGB޻\-; MI[\ G buC   -T;,}g o 5y7> rb]3{H). |M4Fix.eDon*6429%96m41<1000F.u*#oݚk[Wob{ׁIIaW//?C$z.Q*Zm  %&.+?wK  O[ ; 6&31;2103.k)c!<eQ$+fݼݽۻ٬cc/ kFNY]3o1;w[L: ^;,|(00:8Wxx'Y4&K5CD\ Z,8I! ?o-5:F>3:3c..U110>-'!] K<߉b -a&ܩ>G <g_5G4 NQ߸ݻޚ߃۪1=5S\WA }zPE T?N w+9C(o$U[ b+:l  -562.,*8+++V*&!2mEa ,hdf4, x/ #Q L%Nލ޵<& )N   "i,"LMALz0{fBAq:zXh+p=->2P.'&(0+*(^))&!S.Bbsu v ! I>OHO<.,~ B~  [;g5 f$'InG-uk"*( $o"%**,*+**+%)"Q RX{- *a 2 8g.R5A-^UE/Efn R ;  j tqQ};nFSxkZ[iT [%'#j!&u*)(+80a2W/%)!Q 3H?giײ#B'aCE/ K  HN[= |HOI-(c6h$QA[ M [.j z&k2I(T%" m9NsMi i%v$${+,+O+M-,E& ; R4D 4Do^+Ul { k  u # I )$R;E :W}SE+pG7  m t K3{V~da@*CI9Ks:9/n~B> r~ m%sg _1!&++,(&$; 6i$ 5E^M_JY0?#()pwPG|cQ0G   X7;Gx[yaMp#?xv a[fuVs7 K ( n 01 n  *> r4.Iw ?x[[0afcp#1C?@b:JJna(N!uMeqRs=O$L9[ ~ !#C)^u| b (5:rE<t_k.azz1z0p *p{$;%dVmm&LxEcDrRL # m dKl! . ^8 /H"H$9u[T xK:I2p/N P1[' k^lUDDX^sUxlUTzc` q ^t 6?;2Buzp#U0_S)qx4k53r>TUdrD0X=58y@/gYt9".RiO=f6MB)(/A0 \\! S|  wjpzAW 3C,]b oF# & u_j8&hX(7CM]`R8 Iz? H2Xs_a z61+=G\ CSeA"Hr  s :!#MoZ6 ?G :"j$Lof2*w _"^)Rp'k&a.@1'Z^%Tyh: >]U$S 9?H W[/g,J,kL M  [ @ h X C =  \ 5 |mMU}oz#81ds ':P4bU`>d/id-DN 3"nU`"$~`q3|uo0Yc' j"yHV\| e= WBI8 + u 5 5 " f atOjE^H4NQcS+3ZuP(ft?- fF`xwu"ZA\1s0_Bk\"!S,;XEe^"nX:3:|4%0 b-rGv{sW59bT  j$eY{cRru95G}I G0XZ{ -eb"Il.eI;"(dcWO]$[FNQNQMhde<^%[JsHw:3F<pm<~_}(d4\:, X%=*Gt4 ;['Ppt1;2s3N#HFI5HFq0RDSYD>bD~'$J>08PjwN _7tL]&'rTS[2nRcQ(wI"?=Ou:nTmV90O_<r/U[fwWQgb<S yGgcEady!r@&9p=;t?q~9uI58EO'**lP/2?_' mr78;{*#KZ,1bq$&a_]NtSRguQD`aUk/qR`N w# Fd/-Z4F~5x:6.y9.6(`t2*Wa|\@+v&}in`YhR].>3J6i7:$ SKL W{}|I_hI|\CG~JRNwdt<KLma~.TGm2Re}"] wq.T'_"2Q<%`[ ) QWJMݻk Xp ]b\nZa !V#"ERmV|l'i-NEME` _Z7~ i"a(/5:=>N>; 7O0(E ڬ[mcݤ^u`>XC / sVmߣIթ֊ٵJ`V H~Xy T4ly,.Z(8;q13Un 9'Z\aQV q*!5=KV[[ WPF8\) oВx,]ǣˈ7&ԝ13 )t2774.&k@mqRK/i΢u՘٢= *&,H1;3{2D/*#]am:Gn.ajT &6mM DB5]o_on t RP  .0a|j&"$AU%8DdKR,\ax^WUmK?X/b \hh1J1g P #<*.10O-& hޥ؛ԨF$] ^x[=t#*$'))(&"Tr YvX < $%%)-p+$Yn= lh2h_gػ1&=O^l - S}A80Ly IO%6HSVWVXRC(4()>Dx(lڬфȗin $2MK+N- k˒ϩW۹1%*,w+'"U dK-E)szPP 4_ _U/%*+5) $/ /]ܙۡ۬Ku ޹޴"M B[ ~ kRAm^lr Z =~I  Q f5B;,+_$& #d | g'֩փ#ߟXg3 #l:R/9XLkz S }5U# @)I u 'I^\dUc 6 L h G WC=j_TK! Qn8`DeVw8yD.=7'#(+137uB@s.R7x'X޹:o-,tJv=G0."?'&3!: 7*oP!kղחFܒk,g/h;F)}I" ~RO{ye ' }d _ wCDnLPj$':]# @n( R: (p8cP0& u]781e?'3 ,$+R7MA5I -i^zܨS -zS;dX6 q \ n <q @x)UT+?O]qy;S Yx?@#> ](:,H"6Z Z i rwJ ~cM $Nk 7>wZT|}"2]RKAf%mJ \MYN d5Aa8+!b!2F19=6$2nڸˈASzn JwRR ?i @[^2-?mlp ] Q m k f<QeUtIO8.gI`PW 8X x hsyXm*4*FuJgl7@mFz%*. !nu'&//6/X 5A7F-x(Bb_"~P߳ځ\GeW F$w$ Y El/31,>q u s" ?;  (fyML^PyeXm#+[ ?r}&f- ( S60QQ%(W,rcL!5oVC[; \ 9 mq k 1 I u%,E*QVP4J25t`zf gLH6SAa"0_6 EXdEd ;IXc0P4 07A;j[g] P\*A}sjL*N:F+i?B0lm2oXOt]{ Z B/ppx&G~F/)z W4 6?n lQ"~&9&E ,t1 ~ } RB)J TvN[n0 9 -CFk Av H$ B Ow7 s@xvhM xr}Z:c@?^ z.  D[o$ .@be  l~H D) \ ~}P ?gVK7o q' _hA%97o-d3 ?ji~&[^H3? 0 {TdM0WF0LZ ~Z(&/bRf\ Ms # q o 9 C   : oL8dh 6 `  8}F0@ckZO r X%C! XV e#gy aZ<=&Q gj 5,!  SC- O:sd/d''<- )1'`tc & :M` AY26 U3+ }   B 8~d ljn5ZmOs y x 3+XPpre.S R  Dq<V p%58l=n ;LCJ-8$^|G&D:.UyTP ks _{5Y~yFVtcs   G ` s46lj5z d@ %~N!}N.~:bN= oNr un>.5dF6('3|ij0b-;V-JlzdlbV0qv}\ l%T' o;n,SdDr <be\N[A@'-s\x" 7 1/y7*!,n7h>L.<r%u ](K'[^d7332#8CT&x<*=&G[Y(FsDr^<&RQyi ' u`IOtF?Y)o K O4@Q:s>.5Z-[{ a 9|`:V?7 %dc}I^0I- e)0t8{`jnpe' XT.o{+8 w<`};oKR5&6m(;EyEnJxDU? ;6ry=oWP`2]{ DFtf\ggtO +a8W]$Y1^*Sjt`t`|rusWQ_R9%64bMP)Dmu Nsc1FaIuFF GRo IddI#{bL0@A]6zZJg} /YZ\P}YW pr :a$y8R:|iG VZP3Ggut#k=~Rg32\AuVE3"ro8bpX8-!4`G me>4s4vp>h<@Ga/ c]hH = m  ' BLshLjr lB6rEVUNy>|6E%} 0  @ H d P  8uA[_> Z[v9RHA4  [Rl Ki`o lI l  0 +  % -W,#h 5 Q66 Y GC3[ 1+1 5 v *`bt 4aehB 1UIj]   KBIO SW BSP ]( ~t  3b_od 3 ;9V0AuCVV] v`.'6b$P4fSp6Xm>iy13M6 =D/rY  Dc ISDH eHM# B [WX UXYnE:h\TA' L28 I %?, i dK { tr9n28v., D1s k=[ MA]   78qz{O Ji ~b| )'3ss526B < R 4=-$ht 5 V8)U 2 Q,+G K_ dc Dy 3VDh mJ# 6AjlcaU "o R[V0Yry*Z:>*;w TFv W B{bEyeQ nIYn$#G w  .TQ U3`GS BOG*3wYpo2_ H66/ 6lC`E $2 "lKN! -XmrSH ""W}6\ U ? v 1ibcd}" j : 71ewPT ?72~HC v={ry @pC &  S- y"S{MxQ0 0 |GkY F Y 2LZ< .Z v;_@&8({Y {s5C 7,q H N & B  d! 8 Tw&kTKWQtu?/ (& +& |i ;2rL @9P9W- - "Uww2n7 4< #+2(b}n>t;1oa=8b cM' [ iA  y KIU x T|x&  "x ^s"Gw .{0^J H# U3&0 = J / !9A Yj ueM+#%y 66 .8-t3t ? A6'C$%) CG yRD% At} Sp O kH2  QX.zT R! NL_ K   7 3 ud] (} @u" \$\Pfm_Le;dBJV O%*g mr9J'8:*hQjW9 O p |++4qky|Q32E9gF ;dL=G7q}U L a0KRiC4T~0aa7+dAIzi#1#s~;]_0!xd-bNL_9N2  6={,5T - +^/WeANd!6zT<_!;]]pF[{ &% & %(e,5A;>AA K y2 JoP Mb>/jsSC} Boc8q M r(L,-2,d-/0 /*'+.C)'s-|l\ߛoa?P@  , C  D r2Ulx{6x0Rv=YB, ku #+1+-%#$%#!  L)^vuPMt"(?*Mgz[i x9 X6o,Ao@\8 Qu 6K k.= @ aJD>#vg<C< 8:^"6 aV':2Ld}-VYsyNYfM@j j'O/[ a d @ i\EY ).wnbp( up 3 Q~  @ y  ,`]9!_IiA>Oc.gM2!eMk^=Tv'"&x2' ( T4Xl$ic2L /H>bhmRXmt5 aLvrxM Yii(# {6yfUSOS2v~Z; \nOBQa  )[B  ,C %hEY 2 S H 2Tde   SxlX @*   v`/`|{6n? p. 0 aqU | }F?  GB  hw'U *?>Q  e  )66 Y7 6nK 9KsQI+iIKC  ><S"Zy JV*7F ~"S Z @/CL *Q-? (y \+0R w\s}Bq jg`/ wX,a1 a< ] Lv:   p mB ]Y CYU"D:C pM 4hDR *3_+x ){R~Tv^Pry9 Xd 6  g{~ 1El c:E/ !tkDq5'83G~a#e\'EecZ/ _ 6g7s : "G>   ^UN@Y:1U\{"=kL;<^ V =2t aI,+eJw~ qIi 24G.|W 5E_J=9-b ^H\~k ( / ^y( S_ 2}lZ2^(4 { `X58z3\[esQ~3 n`+/Nh Ri59L3u4;e-z5^Uy s:3n0`)S=I1<3zrDd`{kHi'q`cN|'LH`723fYzwcwGdM+abK[[r-`9`=iBr36?B TaDxjP]JHKk8XD9wY)(//.}KbI$ EQlrLd4h.vaMRw`p\",*K{{+#|'zrq|WE~W >B[$Mcy xK #@$ HSB(~ ( a%H??/:I KX z2HN}nl e2 @ !&  i|/ 78S.+ZD.> uBn Q  i  ?? (/g#z  Z ND3 l kE9:@} N#Z!/ m j|Q : .< 1fq^ o cH[m >qX9 :Q$4uE r w\U!R/LHQnl|WQRE2K?=Dn uCiZ?It/ h _k<I+w}& P#y  2O8  $ Tg,W2s0U ]{v45B a l$UD;sTAN " f m^C n  7 ! X * 1+fk( {^au}+)vE0i&Cj03;0 }bT |r=,88 /$1!!$Y# -Hpqoc[TړQ[ { Ii | 0T_2-3Z/)d ; @ ; / 4 ^O  F |x,f;Z>,l.n0-Ip_R1sP 6vK1:v s|_ 5}]ip maq eV #+Q+$q-h0dK܋ܚeS2 d ? kmrGb$g({gW/p W G O g WoM@&E7X vZhN+)qk ? ! _  8VBRfp]}nCU]4N5W5<( 9T[>]/V ) *n67 ! Iy(pc ]q:cIB|#FR?~!W5(ijT*>Q@Saq1 P'y~X+r,oGE`}#xZq"7P" 2NXYt1zx.Bj\uO|K9^{4l%U-I - S ) 4 J/0E =7 7b Pe,=KQ X:Znn>N6;qG 8O|W-IBe ##,Zu?\*lK0!lg80 O*Zmv{~"7e ]r-@ "  Lr;i`4N 6^SMIAyYs8GURHYTm5j( fkUQz`b2  ?6NU=M\ePQ "[+ 3|C +%bt\?F6 6aXimgdu.kA1 *`melyyPCq0Sp5rlP7g""@D <6E> sA yRBQ*ZD-B{yM8_znTUU}A)7 ,za~UQ+ e5HMMu[5X(vQ-Ev-f1] "ANE>8.* R l@v2 i7ycVD (ciWqEdI%Lm{`L0{.QZ<gd6 ?c(L%=+&::?HTh*[W P$0&+ b.KCbf--+Yp)u@ N>52<`<9Bkt*8t;Z c|x,tk ,_(?H ';="{, T9sA:=,ric@N0O~2%[5a.TZKO`sT$5,T.UV"MbOD0,t23^,[W8(:nUO #Q.PrC/h 3S 8|xE Fn^rxtO=I>FH(&w)n'XtRcWv#5?KG[ksLlQAnPXH8Eo@MrJg\T-](Xr!",cjDbdCd;D!ealBUn|Lz+  Xafu8"uBtJz !lQm lr &v0b=@MT K +=3r2 f^iiVF1%L0Yw- Q ({X!UYc# REu5 h ~ 'Wa}c T(*o Q 8g@, wt6E`ZA f[e  t !euZ@ kp% 7LG'BDfh/bB4'f ? @eV&zVnm : }U2 4uo[m.l1I* ++R* "`Y08?T l; 72_ s#\xow^wT~+  wa =S @uMCL~7aBm\6>)=l CGx Tk TxZ, [oTV!zM=C O\| nt jL26L]m}p{W 5yiz' >]a ]/e)OEw n "7 lA# Pj|cF DS , 82ovvu2!O|, b| g KMM M8 <`=7/`Q }r 2X{o {  -1.G v ] (Ht 4w U9_ N(A3HqT [P=rr {dn "t-6 u P]Q Wug1yzQ &M0  t C Df6 w ', lZ .  3vNY:T_Q {p4 4IS s_- @n7F F 2uIUTxGjGQ*<;dPI ^;VJ !1 9\RP}v|  s> W's!n |QZ 3 ],5T7Q PE;sYT +7o  Dw/V^R r. d 4 P (A WxN ! ?b% 0X9~^, R\Np03vez [ zs}%F%|I/6 h ^R8  T,Y.J7A,ow7^? )?snn~4?ATi @#Q)YW8Ne2V:lnx ( 8b-]`F.;!0"JQlk!|*b45J F]|W:8c.r8^+WoM$O1U @+B{ Bn cf  9XwN._%ZA*5 b m+kx/U|Mx;dqFK`M:S)GR c8b6 Y#(*+,.I+)Q-248~3(((4E3JGԫxb~]t _ cF%ݢW"]  RfCh 0RR+}zS MN;~pU#+ -681+1p763443p/$ hQC#"SY *\ y& ^"@.ӀӬ>b#mr0,if@&: qs5M l U P  j.D[h1Q;A E ;9+53r*"!^#',R,"c 2\)H w SM~ޢNS`&l. OjKsc5y.{Vf  A. ()SY~XnW~FT7|Tk/HVf5L :&03+-#81:s:t2%;.U)Jڶ= E qgLMm)UڥmI=%    :ti n<*r 5{g L B{p{r !i}>-kuQ 4+47638966995.$POZ )yya=H .oWR@۟ ~\tf #) z g w i ls joTe Cx4/s\HM%Y.,Rj:u] S K +]!(001*o(*,I+'l&Z"fw yS2pG2utI IAf.N>Q c|D5 D5@}4m\Q_}c0(D3[{Av#Z 7'>(-RKZB(Z. ETC o  *  l)c%&%A$"" !^2aGߖޮߵv=M Ona&gpd' pyjAR@0 #  E e3XQ ! Q1k:}\jxu-u<Q  - - -  f<@.pI2d|?&i % w 2  @wU6k>>1DS.2(8M@OyrI#-q"_;EkvPh L=b_(DrLay91\ Y#>6):"G0F+Tj]_fH43<[v0$U}^EHoQWNi>zP%0?WQw]f %L O8taTRi8>2)D J D|VJ3L4gYvc19S@67~}b &fV n|2?W;!Q9H( lZ-!Hd U[fg:O` ynq7GW8D (DQtb X vYG  d6' ZY WI[=fYN:TX}z;OT J5KG%D\\6 ] S ])JU!x 0Leg2 _K }W&:kQ1Y _dvC (R}{? 2. { k{xN*eG+Gil`_~llY)"m 9@CoJh@'4z]A4 3)c|o ([>"T Y -F3GP`V:=;\< U(KXG^nkQPlMr|]-VLl  mB j d( poS DYLhn0un @h|LA/Y-}}[+g3+ gwPaz"IUm- 68U[W`%damC*+@d45]` :~1[%< VysDo"D?lOk)_.< )L?Dj::5;1[B|VB*{g)ep72"5J{!$_ 6gx K<+w*V}O(4R v(frh<Pq5g]6ZEe&//qwM3rTL{ ^ 3 , % I .  }\ @dTZGw<.-!S 1Up"0X5w VC 4resBaz=ghkOBm~ |2 !}v>B+L2{{ Dki!} l [ T A5+gKn[P~^ !#0&87DW]6X*@~h0 ys ."%M&!*#./&'].no6LZJ 4 D*r3^o*q 7(>E F d5O h % *U Rq!'{a\Cy`Q P N E  VVu jRi CY):VY]T tXdsd c Ty nBjG,-S0 c ]'u9{|jptgG^o 9  :H.70/_TLy;OgFyHA0v-k(J6EMB0{[SMF:;BGT[`XPN:NQrO6&"5Ml-e IztaJ;( %)<FYmtf?gw6$k6K`voHUHdy)-yqIA_}#unmC}Tu#Rn0;/Ai JHotZ3fjP 'DTg8bh00 ^@xJAA\Q6:L,Kg`SM> /,;B6=+EM|tU,X*Tw$Yg GSA|eyaA!tmT7pbQ 4iV4H,d"XeggYI%~ C&lq@35e9*g8Y ^VI]ka9Z,ZM+mby2G1jqlmDa4u:M>7)INyU2C V?n) eq 0(T, F(utz3 (0_S2<}E0Cy~ `_sTx=[R;Fy]$r.bf4oM%E nVytW7K6yp)?d`TO"wow@AP']^p:zweE]zWGL{2_|0gG16F_d+cP.5lUXn|tl%=Q6fN%[na([)ux&h1c^k2;%sF1%n)6qu% p:&^dqIvjwk:>BPc_A |/(Ged^y;TL$d:_k62iHaB}X5vc/~s M .LlqsY c`?>Q~%j@~D}%n_ (r y+vH /6R;Dubc P s UO 4BMT >| +c SxQ -`-q\)( 'n{ rE!:{J^S d'zjR{7Y G+ 8:g lWd g*yeGK $ wXsh; H !1=BiWA|?Wn(^ } A-{tM :_5UX2)>/I-KF e Er-a0$E*QrXkd~c-&A$'x}9>t_;:[3/'xF$!V'[GJev5%g}Rz/R6 i?~~ ni J0JyiV , M'Dp,uf1Mxqr i [V ~i-jkWp`Jsg#9#]aq ~8|RQ9{<~ JH$m!ddK4F xelYtvLGIurBHL-Z;_O [CF!F^7Kp9X ani"q*wzJlg~CnP3^{Xs L`DO 0.>b8KX}B:D#MQXN}He&v/ScYgh:)lu~I!' AY "P`31 Qn.c~#`hr%Ne.e 1'A-}gE(,67+@)2>\cAYXfgf,:FAm`-s/C-nUIVe{1brBTpzT"A+Yi*  WT1Sls<i}YEd\YBJ ?U&a yb^}W%p#Lv1Lx/(Qn2~[( e}Aj_A_MmX\_M hR'\B&+( 05 !  :! '  A* (     )  $! !6#3"<' ++=-4":+/0              "$  &                                     /F#p `9;oQUh:b <k 5`6d}L#N,w\hEE$ T|!+ 4aF@JuP PBr$&"eT7~06SE#anwM#.} quF9+RG|u>AR=3!k6o"us*u\l9lxsc_2ksa>=r; `<4C;%hUPEC/gARDHRil/!@gu`5_cXNB&6YJf~d$ {_7l?Cf(UVV` 8$$A4)8^It`MY t:`f@{{ -#3 mF-Xf8#' m16] T 1u c"Kf)4:(^@x BRJd,MGI\*^ )lvqh zdM"FM=^{SgA,p[GFLn#QocRhKen y0,3 UJ y,+\h5bUdGT*m"sy5mHBd=&" Lhbi\Ocah+Ce,E%E*Nr 8Xnf)4@~1$JhD9ViQMrr_&>tPs@GgtF\oq zH BOR;Cf O2!7VRfk^kBwwZMk6%|+tufD<kn1&Yy$.'Y_%}5"a<7xSA %Hsq?;TeTo3*GP# s tD%qtau609mGeesd\-#J>Hx/tY\k(&xv`CA`K*t/t9\YHL{5yTceBzWeh.jub.dPxKi ! ]G8\{{ B.^x"zz76PF &t  ce z yG M"&L  "  . BYv y3q\.rU*l_b}Q<Co  +;B&$CF!)z/10H.n041&~"!X Kq e?شO YAפ ׊;5U r  =_  /y%#rC/4!Zw 8g1k v<,2 z#O-147f:2:H:7O0**-,  sgP3#;ж"n\E'y%'  5 = &  v  RK9o;`3TmV(NJTdl ,* J1 h = ""! $)1 8<:b41>2C1-X'S$*  sܞϊ5rс~ti r 5 j ? f 8|g ^e#' scL)a_=}niB=q ~ Tt[ }!@-L3,>()20[6-7?3 ,h&$%M#M[ \dܒٳ- sޠQ+A2mvc7}bSwkBlW&hF0qR'X< 7T ^#"p G#1)030 +*/O1-%L r5L3٩wFڡj߲^ m 2TV#G{W8M|&&"d6bzS S  B X P")b*']%y&@+0d0s*# ^  dD-po;A]tl0{fgshH / A7Fr~9f$.!?,Mg(WxLl6?&}:oMS\f 7@ `U**5- )3&&-X571{*y)+W+' hH9 g7kduVדו'8M EIdFy{;F :e2!JC l,$5F )D b4B r&& /8;5]+]&-+c35.0#X>+&6R,ۘ<$*Bg )[<.=TV6T '\V1O"7, : !<6+#h+f.*('&+24,#"=$ N F:Pl2("-NE9:C8_D<,| N6xk+CNdt-on#Qhl  f :sX |$%#"L&-M1d-(&Z(u*(U#Av!dPYyܜڗں߯t[qa}mp]skk:~UjBA`S<.}$ @= v}!+6.*&'Y.75M5-%#&$'! e.3$e/ٹ&cܪ 8;VGI[1#g(F^[u'RipwcN`fd ) 3B7$*)$K"|'/~3.'V#$('5!  <P 5Gݢ܃މߊ Cܥ܄t*@<5E/"rhqo5[PzfFhgHoY)a: R b?vy"&%"!&h/3/(%H),+%g rW\$STpvM@BY \Di+ NE80 JE3XB2/.3!UdFW 66*F<Q!$  )02-)o)9.;20)B"g6. r'jߧ`'cߵ0saD~=M|[|)8/w"RZ LU pc#w+-\+(O(=,13a-$";$ % 1E )_rڭ~ܪݑ<~hQFe@Nf\@01$4 # V\9c.kJz2#+.+++e/6:9K5q222.1 ,o$.o:y#nrُZ_ם Iݬd |vv xOr ]L$ox{]Ek{cW$Fn3j#2 Sp[)'p./-y*h,16[7+0+(k)'B#]# 1N[(fq &CG1ޫRFOjCg`} wYhAA:4N_n 2m- !"/+33 2//;4:?;4r-m)*)%D V _zޫLw6֣]ܡ %_pz}]]m riFB; *CAtQx2 \J / &\$Y&1993/1}9_<81.+]($h :޸Jd(<ِՖҠEiTco>s;}]}%,4z\3n;;  z 2('./-)G-w265j/ .-.Q+$ ]J~x{F ٩ۋ:2fg,p,uJ51(4LH%Y {  GUW "!!&+.-))3*)'&&}$% e J 8*\SQۨ%N8O [YG zi{zRam`>t7cX  uDD\[-@#$I# "_"! EB HBCD(߉"cyhFcn6:::R/@9n;jX /  gK:u } < N L F ^ / dUy ._D71eiX.v~G_iZfjR)?0.FO5cvN zQ | p 3   J`  Y8 ]u}R W 4 Q} w st _ c)~sS| u V`iEI;qjZk,|IhwKcpFW5L + ' ?Q (v9WcP bCc 1 J 6 RhPXfe2V{ eKy ?S}_8'V w> h8s{t I " 9 k <5 Nh+ N7H iL*C  - x=8[[ L?2E`L[-nh?P]8an~ & GF (nL WA6.SWXq}a JmI.mt zaV (?IT h J h8f jX xN {J QC A6DGy&ta!MXC xZd~Zi,t gr `h1 ShC bM;]% WKd 4 _'Tzv0 MKSy}L8B u<T Y *n%a\v#V{mHOss3 ^P~?g}cR Ce ?vk5 i\iD"rm(-Yo+C58,>~8uTz VT3)u_qPFm?AiOf?~Lv< SI72T@ebF cv&?_t/XM.O;P~X `Nq;a>B$fE5v\%e.HMPOK(|p^uS@ 1FW5)(LTpum4 5>JrWrrJWV:{YI{#K~&xNd  ^TF(d)W@y@_=fMj3l(y<X * kn~8^5;Rw!x;k512jSwg8dHx{B|tY'e R0yq;KiWp66[icf`.+"$ ],v6$Xp/ Mu]e=gH3"%#bV&T9vaed.i|1#xSL :NT""{rE>@I6|J0V"[kp=Gc\q^FW-G;3Y@3=\}|c z@JjC1(v{jxyRMBx}S$cn?%41}Gt9cV-Wu<- s[v !XDCw@`DAu9M"KhNK*+{@Fp$\AS"D"!?rZqLHa?& c1ԻR&ǟ_FɋЇug|h*"8&" }ޟֽϿ̨ZbLw ?g] # dQs")fd q*0 a;>YPC.94L[aV=e'5 k i=`Nԟ*ܥ)$[)"8`p`[LPҧЅXI}߹W;?  TZYGԲ`L@H= ~jp >S=,M^bs^qN]=0j'Tj 'a޼kheڼXB*,!jVU 10=|R8˓b۫ ,Jޢgwqeg ' , @ Wa)ݾCx! +X$ 8 "81RtO8,~8SRYbh^oqi@Xb=S\YgfJٲQήdj{!7w=-b_3'YYrr @ Zr߲ސD@ ~~DIr9ΖE2TZI:=1M{cuxyRp\E0nB߅VCַhߴ*j Un D']|;@݂8;qRQFYBK \FksQulW;Q<]OGFZ<K ]H'M(pu 7|Y(^Y]J&jr  wWX-aN6P& S[4>ts6sBw5>T>RGC$K'V^c&d`Q86Ez'(۹xi׵!l\ v!lS`فPЄ֩UZ*r ISf; M*M %|pV%9M,_YL7GNyUUOkIqF~>* ӚB{b ݒY osl OJ 0XUn[߾.))R<>SQ yJ ~kW.S^mT m WH\~} !Q |VM / )Qf7P[ ^TY7%(I}z/Mp^\nPD?BH6M{NF5Y;Qޥ.8ٵGh1 1 l F?)@[ܘقڬ@ 5 "    VuuIOzf0^$ E !z>] b!&6$G&]`wUUE<:BNDWWJ|2kh܉TD,W~Ѯ}ٝ GN* #m=߿߳<%݅ NI6& 44h wAqKeh Y 3. v׵L8yOPXGUmJ@=EOUb[dJYD+ c.@9׍O֟PPwzl3 / [#3 ZO'k#ݒ 4e7&-w  F]]$ _^._ D } --L  xk"8#{ګܽ޷..N&7U\dgd|YMD:CLFZ/edfT8Q T,ٟg~՚֭Dp}k $p7;@Ҟۻ!{ A P3S&QJ ?7*,LP :(][P P^JC=ٛۻlBގܷܶ+SBSZb`YRNIQYbfb^NK:1ddOH}՝Ԇol m e MC Ub_&JեOڎ~&M!0" -hr; {+bI [6[/ " $c+k2t?3#'SF`oph^V2SR?SqRK`>*!*R٫ɫ"VAڳvJppln~ XB׭8SѠ%4 Z&N(Y&?! f:" o}. R3Թn(%XG  Ng #uAYedXNL5C9@(BvFHC6#{e5eƱ̩שWa(a۸ܻ<b%rv jzVuOe7xڛ՞#v h.T BQ[$Yfc_ ' D H - ? 11&/Z (Xj   ja?$;BV7ZNF?4y259|;X9S2'{ at?=@-9R`eD  .43kL ы|ݮiux*%~>' /+  Dy \LFs$v = } J7)",,d*DLPRb8  ?0<52a01o59X===;4o,Q!%%z_ T!H`Db  itZ6^t bqP_<+ Y_A&3`&i 5 H c y [ Eq,,NwWA`1u8TI!2 %(!4O:;:;=@rCDeD@: 1s% (Gt2)ܮo e>kY(l-U\^zPtJ>UPepU $ iW;=o|D.sza9"k6-l!#^7 :  E  ;k<gK Ir 5 !S: c owOVQlV_Kh(;5qH.Y|@C{] > p)R&*$,4/AQxph!QN)[G5 H '  :  M  #C!9tj'i`_>;aG1(S8p5[;)wDZV*CoDlpV?35x2(B4~5BWZ':2tI/Vz  q L & }> 8& Q>0+%$UyrIutUh~ttH% .F! YA~N &RWT+ @%bd)f6v!z6~W45q6q KgpM ENT~tPXZB7`E]|` .tq0$ g.N`&X#=:t !p)`z*@#O}>8vHQA#m2s _S F.HJ48O':Ne<_IH4mK. 7E`rR/5wi"% R(v*+\T!LP1a|I5j :rd@  ?v(e0Uu6.WR MQWuWalGXF'!|aE``gD=!EQNW2HrGdCSG~sbEc!R_/T.W2}>.`^6Ua{#2 I001Y:"JH[oq~0xWv*Y#-<Gr9aRm8mvK5,4 w,&*/Q P1%(B&m 44,|Oe|9VTPLXWE87_j.>{jS7iF W/ +t=!>-y0xf~`*`2A]A"$Z:+$ s<S!gNxG$YidfNlcwqZ <HP<>r8v4n5=55e MO%5V^e"XCbs1wB_PN$N8uT!-!57{8%?-{~[3-{ 0dJP[qI$`#\k9Sb6 OTdtaC2~X h!iq. xPEyxpRr(gJe z2MR}f1"z%rdk!;65m \SM0I |?dE/uY:irmB:7c]M7-*<\;SVC1WUB3sPzTX0=cid> ~U(y ^Pq_5L (hAc_8P0!V76O\@h/KM-9CcF)57'WN[!va*# mmO9C6M` ;Y",2u530*|"Z p H 6WJ; Mqc|8VxUW& !3   e %QZt x&f&Td<HOdriO4%J@Um  xjW:3nQ=V!J"i p;gHdkgOR0A/ ΰioĢ́݌Tާc] ;7CОQVS t> |JnTE xw1qd@=4Xay{-  K%_l2 i 5 o  gZ3((0I'c#MnXxn+(nD{z\  #{7GUidM82-5-HmW> *t L} )HP/>sr&i'T sh!j 1S/W_o a%}9JSgDiV{?0.Zf5ͮAGacC!D  :ϱ̅̌\6`!`g`V $\ !a 3o*/ $a   q$79\3 : In42y < bY#ssZ.k/%WjN%R,u "k@ @ j}fa!G7jqR/WS3#w? n$q8<:%ALJ0 |;g;X9+9f/ocڌ֡AcnBB#HgbL 3!PX i 2D  :[ g 0P,~nwx@9JeV(pkHo_0,qK 3)3sa=)W]@q9UBqjb>].?fA-APE;i-:3R\KUJNKA'0dqrcڱiy C rM[ S3Aoi3 4y[05xbfx; d?nvG( nB9h: [\, G  z v 8I {vgf$C }nrh6[ ,/ YZ>E/odLFxrZ%=g<5LsJVG= #.y~ ~ l%1=EB5 #^Gf# e U_ojLk @T =ZJY T \ Mrc)_ kQ ~z%O[s U 4=H % vgrPzn6c?oCeL ~n)QK|\s$ } 9 I\4/NU;;- YFq,  { i:QSF$B AR(>U0P3Uu2z+^UqCobۀ-iW* 5$sGj#L g S&=x  - 4CM [C ][d!`Ts"4^&C`n:E#}.=\twQp$'%w(  'V81!s"*+l('(&?p(خ +s]F. ' JA.6,NA+t1* a (  gz{L* ~9oh <~#SJb7b"/,;4g3X3PadT#n;86 gzI IsD " q ] ,M*m&#+--' C?>Y%L7U-aP5hhY /J 8[fzWEm/M(< VG+`SH  D   tpca\7$jS+~Ei>]oAg2<_-6! qnr& T /S r m F ( CB) !}  TwXށw?GG\kR25`WX=v >/o/vI $ (L[AhhD ! {ql z^%KI|69cuZ 0OyT`)be'b(+U;xka Jb\ F.U[ na,}?!& Jp8g5R/<\G8p\( ~R|h =?JQ#A\W's x w E$hF?i H5AueO#kU9%';3sOJbk_4)1{' R% JTE Ch@Aw2J%RTxhkk4[`;$"!aqu!1Z.{ $ x < epw*`N  9II%S_Ez/mOIIais5~ 6]jg_q|d40-bFi~IR(+%"d;xkblY"+`lU@@!(Ub'=Btea%ow:e~+} h  m!6WM~ / yC`1WJUN ^3GJYg;. Ex!ayX\DK&N_pY'{<;QDVd O03C?peZW /PCMY0 z.Wsw%((#i!#\#:I  ^";)ٹ؟ق&d9תؐ~V''-|QY 6 + ,  ; T$_QG0:Jh~ !CGl|lvUju< sde:A`%k*=;g\XJ;x!#!!#%+6??83-1,.(%"/ a/iza\FOG|Gي9V2{ ! )VrtOx$S=84  b 7c5AP  1<=?8F%: T5e~J6@ B =)w4 /axUݚ:)ތ= G"K'N)6)B($!% nr#M*0hvEY-=AS84  w!"(0%=7H;NLRVUYV[TRNF`=5+#!p3="peǡoX@µǛe  J {m(+eԛе,rd$< ] "M* 0100.)%;"kL  &C46Qnb T8;Dv?8].zEX\KP X(0BNvm PoI aNޭ֣͜7ٺ[!/eO!(-0L1I2k1i.L*&! GJE-O XV2%LA|`,SݱDh/"+7D)PXclnj@h eZ[M\A 5%@q4wՅ^ؾ締zʓx'p wn!#w%j#B$qZ/٫ 5(Y.37:93974i0*$ )Wr2/7G2[RU(^N=ܪl,! k2} %m2LBRPZRd7mrsxpjcaSC1o"*ݳF?f*ɻ AKf~$b)2,,)d$" ѳɏÏۺyԹQǺUT .M%,?3k8:8}8n72,Q(p"\{ 4CpH)`F.)hxoj i0.e rX m&O_" 8ID5!39$ZcxwD$'L *&8HN Q^P{,xDzP3O^SoS6 lZ~&}:Pp=|(`m+=fz,t/c d&7rIu {a0lz/D|U"7V3u .U&X[V,=u)D`RcU)e'#q4~eyF/eS.x_TUUrlD]+ ~IT9\mae_Si/ng4kO5~G@aCdjf6Q9>7L A591&I\yJM&$WhdD%hc~" 8" s~a0c 63VHg bU6|l]&A[b&6M3>b'tC8 J.fw9B]=&X]S.gF_X;cQ 3 S6{ >NHxYq'QQ,{G&sR97H6 o6 ,2E::dQoEuN*W FNFT t  gxuz'V^ka c D V uAULdi;;6^AT/  ,  mO r,S|a?> *6Zs#.DU]7(,((M(U|;9"~)=R3#|JM@JP(k/y8W&;fA-Fe46 FgA[Ob_L;EWa$d@6r8X?xz] RlDVIYQby%GrU0^LZ|Fn%xSr^OuMk#2*On4[ "-t Rwv% H  v) t)'L0P-HeP]qwCkBuX(]u1\"5  .D%f&^#U/0e)')-)1(4/#k~Z4} Z*X T{,&}L=w]O9)3?mO|>b%v0}ZZd1+Lp(37y\ 1  B!>'#U"'+R/.f,+* (i#qbl4cb hK%KT4SU* ;d3s .;-d.;dq-j@h5"iR>OP  t }AX'ugP67 +:=B1f~BB'QqQ58'zDv/rMg,HG6F+`;`G<rE yL "N%'p((((&#!"$ 1s=ql.+$m!]m"'= `z$nQ5_8V[kRjtyp^,GR8e~&pAv ;iW< " #! v |YG tWHs_Aji]1,' Gq%C}M'Z[iR +ul AA%/S  G |":I S8>I-k,6jVQJrq"6QFOr14.YJpicV &spy  1 Y><}U 6 ' tNI|BL7nSlqc r {L)`,Mv tY [B#9/oUK]ey*!%fDLqVJiV4V__82!irl4[HZMBZrU*.G(W^<+:Vwr Q \HSN4t%E8&%llHu d Gn\I3Z#1,ZuVd,/^VM{h 1+^,BY,0gp$ch6M,bRsSmEK2 Ey(@&O.h:K#L!?>?p_V/ 6uW`Wh^G90eVc|[[C Rn |isV :@<?r"keih~] t!* *)wzgVlow IqZhyI/z .( ncu A;}SG i7"mnW/&H+JrO.@g{/ " 4W <WYl!KvX32,{@w:&y:4,5mz[o - c s q&n_ 0~|w  H^c ES - m$ {9  k ~m 47 znNv"Xyo*'RLUuun k+{y+sD'.)kI[   ~u  Q B? E # !^Q~B}D|K`qJ.TC@o+l-4#ns bQ (-2Q*"&/6021552i/U)8nrp3YI;ӓl6l˸Ձ@tҼڤ1@ 3[ ~B 5Y1y< KR$+&/;'{%w #"!l(|/o.,3CGA?A?3P--5-j'h&BX̔\g%.# [ a:  /N{TN_A+x *>@KU@$4jK,$ %d'6NJRG!5/y;AHDLHfB76%yX9d[g՚a (jY٥݌ ڞ {K%  a #   z#QMIZjd,B_(c>0C,0]SU)'|pPim(!i# ! &9ED<2/81[9=19l-"uWb E`߅٧:D53ܹ֔5T  W|]-nH2A1]S%\NovuM(n^a\K~3rqJr(kZt. m .83o)_$R'~09?q9c+& +#c&$KFE<-"߂/GZndah JrX2BEer onxD7J1P ] d:  8/ssgu0r63]uo!c|J4)P dklX9 =##}" }!N""1>Fk IP*l*\"_OLNco/|9.Zo3Zrs})KjJ5$F/$(ipe N Y G F h,MbaLJ [  D  ^UZmc& &Yi;1 22gVdNXd]5 r6/}=[} h#jjCjFr578<}N1S"7_ M  !j GT3 Z rO9698Ac(.$':>Y8UR)C [2/ r H G k y v _ . C  H+J h::`,0kQr7F) ^l 8  a *D%{mX i Q z ^mfe 5tbYeic(f5ugN$V H&yxQ:SUu/i162upoPcmLMgs8 1"r'uB&8 h O &mOb 1 ! hsN,95{4= %s{Wq#33>Bzw p\ll  01O!A?<.e2VJu mmb# t4 *    V   f = [ m b : M | ?'k\WS0B2t#e!! 8S D|kO"w''cgTl ]iJ@h >^ *2 k  ,(?,6.82}(o%K20f!q cW oT=ԢDȼ͓Cԇ+FNCt {  / C? x "  9 % : -NBjr'AN:|CRc|d Z;C#+"R}1IPQLKG@F81^7JO\zlffJ:3IO[nZrL&Ex?+ 0 ._ˡv l[VϷۃh$?GZk\<!f!_ }ZmJs_bqC6GHb*k|a Q= ,w&: 4rYG3Rz]yAOH5 V+o( J#t+00,&M= -VFѐIV|3ޓj$jstE I/]b S10ݫڻLHNU8SM,2&tM.lw\wnW>8iIXYMUA,3PlGJPeΕr?Ɛy?4ֱrִ N$X/3332-s(?% ku ^) k߅\Q-e' Y[ms# 48 ((k}`g{@ڍvnJvXh~M &Mf|^@/]7vK^|q{bD_5572'Z)V)ťɻ.O@ z(.p14@6z2'D%, VmItlE6 (iT 3i!Ia 0A7ٟ;ߓ֧ԑעݐ?S1! $cv 6OJQoKA\>A>GMgW_\`O|@6/%Q tAμ_4~ZI޷Q Nvw) "0%%}"]f}"ޫިHE>"C+p?=i? zL?1;&ڒp*i߈$#;$F  P\('4@ED ?8e4-4w7;<91(+ 3!X߳4ٵѩ4О֣ڗ=g\q, &k . M*_ &m`JD Z&BW= h%6_l]Q \\AqY&/ GX'! (,.-"+(&'%$%# \Y /o?WNXf`\O0My eV!Nf\z,}^]bJ q V   2 _  ! z iQCKO5q'}gZ[OD>qc} { ! ;^!n 4  /8 u35T :RCV 4@/O$6Ql-Y5|1@U gh-*/s]! va70UF$ mM$NU_^0w v a,, |>%F1x-7chK%:a_q1,)}'k QvX- ,"*1t55f42T1110S/,&eP!TּKتgBW GAAPl$"%"qB 2 G$s? : -@@A?:Gs@0.o&nip K 1Q7.Q+c 4jMo"bPw%c(T/McjcrWPU2a2o*xLudH%)`Qp,J*Bҽ7XZQ &:5EB:7 )@w"%|"#0a_6ržБ-4Ga,)@4::3*Q#}U  +rKd\g<6ik?݈ޒJT u%4JJ`g\I>}D1Q ^pd_\DP#@V?[*zo̅,M(L.7-%|> W >ہwϜ£<ïŷaM(./ ,(&#&&#4 q]`&u52LPO3g8 \Q .  =PM:A9=IHS_5cRK:I(b#<&:!ce}8щWO"I_ˤX>JS85 r`( )$ "ٔѕ̆̚1ЬϑՉ,} sp& +)&{%d)\,G)"sNF U;-rL3H)V&A_|pt !0+!<"z4L\]fg6[kJD[MaVR'Ce1i"4/G DXcʗһ_ϒjz+S? F="#1) 6>-DϙƝDN*p 'X-0/a-I-,,(,$*^&Z .hU=YlI+K?&R$MQ/bY30lz] \,+<_DR@^=DP&[sa;c]!O)>]665/"lѾҾǘW`TyZ""cuyͨ((ήӶz! =~  '#K,/.*''a++ *d%OVs߽. ns\u Yy M=eu C /#/A:MQVM[A!=5)|  a@n'BQ.Ӽ߬UGK%9[J$1UtM!@$c Q^ t  `(El'|N$cbfb /e .7y%*N)%'N-[36751 -*** ([# r+ Tߊߢ&2jc VS +1P i*, l '.mQuUU a hXWB u78TRc1#K#SxE  2 ~ 3 s xO* ( ,%"9 Kdy_ X0cZO-\8E]3 J * S m _ H   &Qh@_  r C  9aLNdJ|F3;&X:E & l?  =_g/B& Wm<9A u8#cZUHqm@* zFk"-r }@3 7U[G3L `% O 22Zy2Q &"Mz mmRqwl,[~)$n v'S 2H# p J;5W'/ z|JhШ$ %?/#6; Wkr*EL&P )\ 3 KdncN'%'}}A_3 6} @h c'p>y_cHg!Lm b& Ouk x Cd G zx4@$/1 I 44 >S v < 9  r9# ]i J FXv Q'owӰ٤  5; $v* ~v2 W''K-e;||!g4*?-%Z$}GHn? qfK[$g 8si kWpq "}4)QKQw"@^F>V !{H 1s ! @J4 9RN79OaB 0ڿ =t~5T|f *7Pؼ/"l 0[1]߰g 1!d tdVNhUR!P%ʷB)-`u8  8fdp'H_!+J_H;5aǜ-#naVYl*&. ye 3 xaV۩[l*`@7 =A9  @H u ;&h!B !'7 #A+f# W˵ܸ m# 'E  I!)Z2 b.oD(Mp+% nc!_5 [r#CHAtv[e ݻtZ*#w!#8$pf8'^j# I zy\H"!f. J>-w% r R c&)'pJЃ& : y" hw3  e,YZ3( A}o |(  1P $5E B߁%#"7@l L2}% z4!w rv?  )"3AJލ4^^U- /Pg:!o 9~ݕ e& Q v Dd< 2?'\/ ߉Lk + -"  &s#B Q0Kd* g{~*E  .y"! 3qrM%<^U& <" ~ؠۢ2X(> n# YiW o Dd+ m'e+ cN7(AZ X e]^Kzk D `J+ Y "ja v,^T8 vEb n>; c% Li #*? $`Os) 4(~6 D[Qu< =i <+ 5./ n xv`D" F @ng++ &G6 EB ~)*4,x,fs4q,3. "GP }aB >AH4jD #wH83ep'oe`R /, Uxx2 TMr21k6~He{mA9*U/ay,6Zwe7+, kQ &Pw&T$[zF&_-`rQ;s 3QB _r8 r? $^VlVs^h+ #dv~i`byzy=*]p_/d( 6KUuf  _QPM@ A 7b/mX/Y jKahMl">$ w1S5'T>-XrL8_<lB%h.4 [FV8#\Om: -]>E_s!/*A4] 9,EG h:'< } 4<j !4$" kt \&QQL 6F`<;|^y&iBMFpKm2+h]aW b W 8nNI+ V)Zi>[!j8&$M.qI0lkuH&y yw1l4T$ NA/ z.LY_u#%W# ""A!Zi|c vF&c' DHkOjPFL'$6SgEndz4 ) [ ? G H z[dHYl[pe5KjIm=MascX6J"Zru96.,1w"W?+D0xe{KM6obj h/I@ f6ZiYL!!V"$Y&$ 6 _j3?h)ja~A2CMehKw oy4SB1NJ J?8{u  ^ #   [ h ;DwU8T[%6 ~>eH<2+p 4jn$C4ShF R l&rh3'D~-RpR<^|_Y~K[4L/GrXT'\& w1,  &y}T+;a:0hcW7^ t/Hfz(gCNLH\|v3H#(*a7/[ bgThNU'JC< ~inHJV)## *W6XqB!t iVa<: $c^ =MA  k r -dXrh\jj e+,l$ OGc/u,"9^^Py2jZ1v?s\iC z<dX]lMOnh<yGB<1 nn -N {Fl&,eizI~Xvskb*;dQ*+;i37Zh}Hq ],#/%sUKTQ-}c5K3$zP.&%:;L_4Fo)4UrJUyhk<-\, ImjqE|5hK&?5n!N&3@t $W>Sh*%0N&/h9HIo)'>+Qs`CzP\P,? wOW 5 [ 0 * nd\:Fy$gjgv_*/1m) peUOs 6 ]4tp_4l] q[O=6M=-6m"]|5/ \i0$9^`5B}.\csaS>1)3#{)6 1W`T<BOco,4J-M oUR9_ %H\/BB QP(q=J'xGr.|#WXM4C s  :|V_`pTnwz-4V~D\[&Pc~Xg5i7#\ Vg +nPWky|K2pnLeR0$wUc-a G#i7W~#Y8cfU^ONlUYmTYc@n*Bj3 ,Kn'1c d< ,{ _HM,0{V&"RFLGy  /  DQ38?Qx4!Ce wWT<3)obB.6-G?IwN9C3`*- <u"(% ""f  . a3 aQ=#&M^ 0  S !uo20& 7aZ8n!Z(MUR{uE>tX\>B8q &m '$%&i)+*5(y& #JN(P`^0TS5Kky= *t4e_~/POE c ry w BCOdFdt- ;-dQn~? bB5q=+j6 9 *   % I~5d:eCdd;Os/`E`%Q U2S.)j( + ' ] Uj`!~:l> ~d?x'Ls==boF &Uds5K-HG.  FqJB$P=5?D;=N'!R|>*bZ L  9 o ;t`-io"h @8a 1 - $K2 a  CCD"b#xqa?/j6 Q<iV_?x<dw'A3dL#vO *6{ O{q!< 3 # ! &   i9=PV(> E# l TB  |[nD '~PeXl$|OI0rGs:b_VpYlx*z;c! mLCrqz94u^A\1p1c>\ B R3 mv\  bW $!   sV  }>d1%B$P(%uODlm (_*pb"$b$5,k}u+FhjK\ ?e= o /Yko 3 Pt 49g  SH}= lO"! %$ ~-S0* >my)F(qCXo20^>qc'Ckr-%!v i8eGyul@,fLxzOmwu>>of#-75  :4E :r]t,c7jV;3RE'}`svW 6 I =VF}}) 1$KQGUyy5",WjKk@b?OiDpZ"7 /TD%i v*rlY\ R # JwuZ_nFpa`3q ?jM0zq zrPk6?LeQjo7_\y3vQ`k P b K4oh^ B6U q B `D{ 2 {: J e N&o o )lM C 1*v.BhV yk6h9  :WjOu_   :%n"Ln6:}"U>94i[`SFjS  *( 1>cn{AV:$@M1qeUD tv3I < E bX1 sU z T{jo |=F9AiTRc qm<h L4 j I>c=N>/GjbTc' <+-;xR|: teY vl@AQ V;6 9C]f^*`xF 9VEgG`nT|$ a .N*%^!hi^uU;-!2nAMTY ghFHDb 7z@tQ*|d, bgD<>&+6<#;MH\J- H a!MQ"j+e DtYf Px/,g_}C'W%u8+%p:#%/ 96%\!iIQu^W S-W?  4.P \ e u e>C.B 1E05"rglH KD=BGyaP ,$|e^pWHl~L_\ koCR'zMK9$njf@a?p@  z r I $ z c V & e&?.(3:4,i(#e f!$A%!GEr;@hN܃~r^8WfRJ gRYueec g&r1AQYkCRh`.dcnO*A,T5b&  > E.DJ9WJWS@PIA?=;X82.<+")M&UC)"۳ۍ+1R 6]z rL:EAؤX׊>d%b \Bgx^ (QZiX9 ) i =h(RHaV -p=4D FO >#)0A8AK&RPLJeD=4H*" (B!5n~g2WI=ajX؃ փ U߇ 3 ^# -8p]x p 9  -~G$;0߀GYT>+7 wY;6 9*=R;]\[(_aJ[OHE=@0#T N6֚!4#9ZA \s? @6ݮ ]k^ }/U&7 ! z< ]ZH`btYg  N u Z X*6~.c&k*i yuK#q@ 4'3C#Ua5c{`ad]GQGB;.B"Q +&ɳjlىZp y )\ lBv"֎l aj !N"! ,#KA@V>=<_5 R- u'Ove5!a9ez$#'1(r:[%?<  1AJLR>YKaxc\SMH@5-%_ ƦԷٵa=;" ߼Y7 &d^okg6NߩܫۈێgN $!a"v!S +-x v:BoAnst Ev7Hbi[C$t"N3_[v 6[`S`P: O#_*%19ACA@J@<5.)}"gE#Uu˩~/T64_I };\  rDW!7@b e !@dx *  R % ?V Xt%@ >QEM(p'4UC2$IX7I-Kz9N. RkV#()-26X8G8774/,)%xm//j)ޯDGcE%e AF ;Mf<0/&R  9 x w~M4#WuOj2!xUJ1&-*~Il!X' 1  l [^(Gbz) l4T -! =*/A>R%v3seK.jSEyv'o'fwkT_tJ% 4 v#e!W.[GV2O! *wHB%)AAXU (U.j^+leI   P' h +D( Y7,K{xdqvab2R(=vMU[e ]Y #{ B u z ` 3*%e ^ON5 +%Z NJP\1:/ESpCdu}Y *  e/Y> J"#V$n$$\%$R#!k 6 o~NTUI/F[}%z2g92 .4my/he7 v q M = V D GY,e!+uMyI[{e0'd#lb5FlZ & !%!?###&$G$!# X pM NCO3s}e3f%^>uPp_-uG'BlD K  {   Q @ ~ bY_Z+GS?+6P\6f\.4ynG+uB sf B}"&!h""K#~$$N#! (b ,uIQAqJO>h|149@w'ThP'g#hyurtlj,$*"!r*%@ORumTs nF!3 d)Bj-<tX  U%]x a@)F=- `2Ya`t\OH#t6}L:lzGN)9`[_}s? q  FbRsObM\6y4xu$.$`u>S:qIU=>rp:qcytNh9LF/ljSz@N At1wZe^9M^'\tI k.e-8fnLe)U.:-)$*)U_ jA(1'5a4696/f*'U"$`E+._ %  d-%z5f+9+6sxcvw)/8fz= v ` 'P%b`*9yS&o--2@sZ(gU $4y8$0.0o1],]% " j,cTۣܯ02 O \ & ] hJM!f4 a vb>T\21* 3%'iqORLR)tnZ<QZv[ ) { / -`1#v"'/3,*$#"h5.ߡ.9Nn OZQ5c+d/;],r ` ^n v04dpLRiB]sreru!7@6cW[ @  o W+ C !7b z\Ym ?ptw?Q$q!3z]P'~(!] q #+eM|pp 3@|8;_mKuBM9G/ ;~38 Bz &R : ]~I  v ]Y?5+"|" #*)W#b; 6VAqB:,i- d  R/ /B RgoNs.{:e UF B?6]" :N! c 6|uD *Ru4 H{^eW 7 @ %)j  z'K:L B{# m>* 9 s]/I;9T9{ y: \Gzv n : 3R&4|T{g| *e 0zi a%W y<f (1#_|\lEQO<xk A)BIgN  :5jI u^%>& =Dya 1Z$5 gP I% 4 F1H( fJ`ZGo|f  N JR WT DAN >#V = Q5B VzEMs C`v \  p-s b .J pET e~&cdd Y-)r"# ?.+QeZn I7 !: pH0^j 7/ /+e ~OIn SoUa`18 vQ21B$?;;i\8cd 4x[0M d5i& xo* C t 9gyIY3 |6taWYRbgbm OM |d ' ~R B`.}>4S gw 9"nN ZEXOUj _Fg 5j_ MF I(zOD" hO Sg,:b`^  sDHPb.`,`iD}N 5'6 V#toTYU aB l9:4X- pP5  0ZowlhJn 'k_6D\'yib5e 5gG[+=N6Sy 6gZXPe|c){8" %D0HNIDt5DBCDkQV[= B Prci[`v x}S "86Aoh*u ,"^1za]! T# 4(&k'%XpaqG2wv0?ZK"pp&xT' [ .  s a_2WID@WmK-FJ_9|:3Fn4G+hc[3({u`?}7r  T8 F!5&Y'h%4(*)F%: |   =Xqa 1sލ>ڽث܎|$yjI}!V x u Y  ::&%oL6'Zl^|if#;B4 u FM D E   v ]/R#Sx~_sP1Le!+m)A"@     +mc.d[YuA ' K4O}Lw_3MG/<9*<,BLf 2X~b^Dz'!C}nxu>A,[XNmz p\ )K7 B )NB 3) SW ! e,Wq jJ^N 4#iRZ4fo`^'3x@ q3 hQd4#6;Md * _M,C-= !pcw Q ; 0 )/ 8t : T d(A~ "6;xdU)=B%SL8*bHloT*oU>LZ A [eC%5 r fC\ ^&[x Ne L Pil3 /m'DrVC{* s|Mq  T S;I:+*U%z>vhL/o~g mf\ A9Wj.Kr\]Z38@ea;MD TG b   ?&H pf) =^|J@$mC( Qy cL sO76 @& [|;n^-H!0f= X w2J5Wp]u?[:7x-aW5e9Mh~0/XYH .QY BRBTme,G/*@ipT|d+v F8-T}u3kCrXRN!^c{}+>dCw0.78|**+AxhbkCtp"ZRuv9R-Zs _517'_Kw5*jeR xb 8R y w@uQN*l~=B$&< HvbY=%/\FTy9?q0MS u-oSL%e,^E.zhs^"i(E?LRTCFD[*)  d^vk<N<^kM8 !MBGrFJm4P`g:,!d*\hL;_4Lnx M ,*4[; ,X,Lte<r4h|&D\vX8Y zh0{c> bL\~ZU+} !DS r\nB(\) ~ju \$*E D];^-2`h  a&:ge#$l D6 +uz=|hJM4h B$'$04&\ z $UGQHVِ'}X:C@7 U XriN'{*V>S  N  |H( yohMDEh#<>ak (d'` OLB"J%L"Z1#2I06!;850$$"Rsx?I>E٘ށإihiܚ$xy>  PxI (} [54VFM*=(c^zu L  ,% SE)Ocz;EJ; '6jQ7=hCvvtp"(2<:f k*T&tFI%f$(G!1y4*N(&~E$! s>ieh8R aqh : 3(G^,B ) q ! kpq_ 1 o ZmwF]Y[c =xk;k9YD[&G!os*G  B|K gU]y&"gvAc* 1L?1}r6K W7ErjVu[ 8(K@_ m6(_&"&]*l%96 kYTmܷ&1F^dVu o u}.1na*m0n$$ RIj  y 1M-RSO?[]n\OC, <$9 i]!//\pr`SdZMFo>.,Q`Qm?E#btq W`y:Fex'-| Et0/d::eZ$o7r*){k8&fbm zkY6e ~ z Wvw}x?4#$Z:C2lk!&h#sFiXPYB q  V{$?/l09 j[7w(K]$}FO >:x #*r{[| t [cM\~[kQ=Fss i._qfh?S- Z|<,dA"WiZqv[}uEgoaLAw 9^RN 2 eob67l x9x{S{[qDyZ%Sc LQiYWynEyX<gKEFKevGt<~"GeFH{LxtC%sXQ@*{Z0 )Da -Mlqv~At'|%tAp " \9Wg-.{>y0~IuTx V;}BqWVso`,^v`:}; xC8k$^SZ5!y+a32oD2^6p]E$N4h2=`&=H[cn~ ";Qcstwq]<4/X^ LnG}xhOQQ?N`v>]tB!dMz!9H]fiaVD5%zjiW9nE~G:(}WD]TUc]ao|./Kx/@BIm{8h$nF wAU)lM+!SvD| 0`~~xaKC,87!$9804=QMANS7-6?9 zeW^aaes4HWnzvjg[JHBA?<0/(z}rcemf]Wt:BA\goyvmkmXD@;# {jhl\RRNF:914@FHGKVl{&<Pev{tnj[SIC9,' }~vrys~vz}r 8GW[bt{dc\BA/ aCnPv>G.A/[05)y - CmvM R`dh1+H3]`}@as}-4cu~ay:Kt S2&iOi=Lp42{c Gn p8\L-]9= zA pZx  )a iO> -| LLS CuhdNL'DEz LH%A' J$fUJ~) 1O,PNoA{f#G9 :[[ r !:4:u`vwvqJ# #`  P8 MGraxc;@0:OpSC^Le2;K`OwU14t%iZcq[XM;3U` 2ADibV!^OSj[zuY /Yrhxc:_ ?$bTMNV.t2yeuGMm&%2B,eB+ `s~e@)af, {~ p)DHs &mKS 4K%!S:6%C~>l }Lm5WId]Dq1_;0^ I hZ|nn<` [sOdNwOSWVp XT`>bI)d.<hI@-,:m@Kb FcSs:hB~y/~0Rv >E_U@)w~mN4bny` 0$,`*>)UkjjS=s{!U]l7h _HqCc>{#%/(E #FWTQ`JYjc_Ru'\qr/l|T>Q^% 4RS+8xEs8&^@mO>}LD8Ceu3,M5h`zg{s6,*>Nf*?z~;A{M\Cm<dTQ1V9F}6V_r4o<zE/$FhMq0kM!o aTm-?b (.Sx 325U8jscL1W97_`$YGJg. xb9RUp81:5VK$!Dtg Fe<:NtsA;w]$~W!r2i3_ *~I\iw |.\'YK/ 5?- *SC T}Mrf$HM Ey8FM &]?+SMA A: G2F812D%:{88h433B0408"%  *   ! 2=A(-1*.,5( %3%"+! **    "     "    ,&%)      '" #    "   #(&                                                     voip-utils-0.3.3/pyproject.toml000066400000000000000000000044031503065536100165510ustar00rootroot00000000000000[build-system] requires = ["setuptools~=74.1.2", "wheel~=0.44.0"] build-backend = "setuptools.build_meta" [project] name = "voip-utils" version = "0.3.3" license = {text = "Apache-2.0"} description = "Voice over IP Utilities" readme = "README.md" authors = [ {name = "The Home Assistant Authors", email = "hello@home-assistant.io"} ] keywords = ["home", "assistant", "voip", "phone"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Topic :: Communications :: Internet Phone", "Topic :: Communications :: Telephony", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] requires-python = ">=3.9.0" dependencies = ["opuslib==3.0.1"] [project.optional-dependencies] dev = [ "build==1.2.2", "black==23.3.0", "flake8==7.1.0", "isort==5.12.0", "mypy==1.1.1", "opuslib==3.0.1", "pylint==3.2.5", "pytest==7.2.2", "python-dotenv==1.0.1" ] [project.urls] "Source Code" = "https://github.com/home-assistant-libs/voip-utils" [tool.setuptools] platforms = ["any"] zip-safe = true include-package-data = true [tool.setuptools.packages.find] include = ["voip_utils"] [tool.pylint."MESSAGES CONTROL"] disable = [ "format", "abstract-method", "cyclic-import", "duplicate-code", "global-statement", "import-outside-toplevel", "inconsistent-return-statements", "locally-disabled", "not-context-manager", "too-few-public-methods", "too-many-arguments", "too-many-branches", "too-many-instance-attributes", "too-many-lines", "too-many-locals", "too-many-public-methods", "too-many-return-statements", "too-many-statements", "too-many-boolean-expressions", "unnecessary-pass", "unused-argument", "broad-except", "too-many-nested-blocks", "invalid-name", "unused-import", "fixme", "useless-super-delegation", "missing-module-docstring", "missing-class-docstring", "missing-function-docstring", "import-error", "consider-using-with", ] [tool.pylint.FORMAT] expected-line-ending-format = "LF" [tool.pytest.ini_options] testpaths = [ "tests", ] voip-utils-0.3.3/script/000077500000000000000000000000001503065536100151405ustar00rootroot00000000000000voip-utils-0.3.3/script/format000077500000000000000000000006431503065536100163610ustar00rootroot00000000000000#!/usr/bin/env python3 import subprocess import venv from pathlib import Path _DIR = Path(__file__).parent _PROGRAM_DIR = _DIR.parent _VENV_DIR = _PROGRAM_DIR / ".venv" _MODULE_DIR = _PROGRAM_DIR / "voip_utils" context = venv.EnvBuilder().ensure_directories(_VENV_DIR) subprocess.check_call([context.env_exe, "-m", "black", str(_MODULE_DIR)]) subprocess.check_call([context.env_exe, "-m", "isort", str(_MODULE_DIR)]) voip-utils-0.3.3/script/lint000077500000000000000000000012301503065536100160300ustar00rootroot00000000000000#!/usr/bin/env python3 import subprocess import venv from pathlib import Path _DIR = Path(__file__).parent _PROGRAM_DIR = _DIR.parent _VENV_DIR = _PROGRAM_DIR / ".venv" _MODULE_DIR = _PROGRAM_DIR / "voip_utils" context = venv.EnvBuilder().ensure_directories(_VENV_DIR) subprocess.check_call([context.env_exe, "-m", "black", str(_MODULE_DIR), "--check"]) subprocess.check_call([context.env_exe, "-m", "isort", str(_MODULE_DIR), "--check"]) subprocess.check_call([context.env_exe, "-m", "flake8", str(_MODULE_DIR)]) subprocess.check_call([context.env_exe, "-m", "pylint", str(_MODULE_DIR)]) subprocess.check_call([context.env_exe, "-m", "mypy", str(_MODULE_DIR)]) voip-utils-0.3.3/script/package000077500000000000000000000004631503065536100164640ustar00rootroot00000000000000#!/usr/bin/env python3 import subprocess import venv from pathlib import Path _DIR = Path(__file__).parent _PROGRAM_DIR = _DIR.parent _VENV_DIR = _PROGRAM_DIR / ".venv" context = venv.EnvBuilder().ensure_directories(_VENV_DIR) subprocess.check_call([context.env_exe, "-m", "build", "--wheel", "--sdist"]) voip-utils-0.3.3/script/setup000077500000000000000000000014671503065536100162360ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import subprocess import venv from pathlib import Path _DIR = Path(__file__).parent _PROGRAM_DIR = _DIR.parent _VENV_DIR = _PROGRAM_DIR / ".venv" parser = argparse.ArgumentParser() parser.add_argument("--dev", action="store_true", help="Install dev requirements") args = parser.parse_args() # Create virtual environment builder = venv.EnvBuilder(with_pip=True) context = builder.ensure_directories(_VENV_DIR) builder.create(_VENV_DIR) # Upgrade dependencies pip = [context.env_exe, "-m", "pip"] subprocess.check_call(pip + ["install", "--upgrade", "pip"]) # Install requirements subprocess.check_call(pip + ["install", "-e", str(_PROGRAM_DIR)]) if args.dev: # Install dev requirements subprocess.check_call( pip + ["install", "-e", f"{_PROGRAM_DIR}[dev]"] ) voip-utils-0.3.3/script/test000077500000000000000000000005461503065536100160520ustar00rootroot00000000000000#!/usr/bin/env python3 import subprocess import sys import venv from pathlib import Path _DIR = Path(__file__).parent _PROGRAM_DIR = _DIR.parent _VENV_DIR = _PROGRAM_DIR / ".venv" _TEST_DIR = _PROGRAM_DIR / "tests" context = venv.EnvBuilder().ensure_directories(_VENV_DIR) subprocess.check_call([context.env_exe, "-m", "pytest", _TEST_DIR] + sys.argv[1:]) voip-utils-0.3.3/setup.cfg000066400000000000000000000007771503065536100154700ustar00rootroot00000000000000[flake8] # To work with Black max-line-length = 88 # E501: line too long # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # D202 No blank lines allowed after function docstring # W504 line break after binary operator ignore = E501, W503, E203, D202, W504 # F401 import unused per-file-ignores = voip_utils/__init__.py:F401 [isort] multi_line_output = 3 include_trailing_comma=True force_grid_wrap=0 use_parentheses=True line_length=88 indent = " " voip-utils-0.3.3/tests/000077500000000000000000000000001503065536100147765ustar00rootroot00000000000000voip-utils-0.3.3/tests/__init__.py000066400000000000000000000000341503065536100171040ustar00rootroot00000000000000"""Tests for voip_utils.""" voip-utils-0.3.3/tests/test_pyaudioop.py000066400000000000000000000122271503065536100204240ustar00rootroot00000000000000from voip_utils import pyaudioop byteorder = "little" def pack(width, data): return b"".join(v.to_bytes(width, byteorder, signed=True) for v in data) def unpack(width, data): return [ int.from_bytes(data[i : i + width], byteorder, signed=True) for i in range(0, len(data), width) ] packs = {w: (lambda *data, width=w: pack(width, data)) for w in (1, 2, 3, 4)} maxvalues = {w: (1 << (8 * w - 1)) - 1 for w in (1, 2, 3, 4)} minvalues = {w: -1 << (8 * w - 1) for w in (1, 2, 3, 4)} datas = { 1: b"\x00\x12\x45\xbb\x7f\x80\xff", 2: packs[2](0, 0x1234, 0x4567, -0x4567, 0x7FFF, -0x8000, -1), 3: packs[3](0, 0x123456, 0x456789, -0x456789, 0x7FFFFF, -0x800000, -1), 4: packs[4](0, 0x12345678, 0x456789AB, -0x456789AB, 0x7FFFFFFF, -0x80000000, -1), } INVALID_DATA = [ (b"abc", 0), (b"abc", 2), (b"ab", 3), (b"abc", 4), ] def test_lin2lin() -> None: """Test sample width conversions.""" for w in 1, 2, 4: assert pyaudioop.lin2lin(datas[w], w, w) == datas[w] assert pyaudioop.lin2lin(bytearray(datas[w]), w, w) == datas[w] assert pyaudioop.lin2lin(memoryview(datas[w]), w, w) == datas[w] assert pyaudioop.lin2lin(datas[1], 1, 2) == packs[2]( 0, 0x1200, 0x4500, -0x4500, 0x7F00, -0x8000, -0x100 ) assert pyaudioop.lin2lin(datas[1], 1, 4) == packs[4]( 0, 0x12000000, 0x45000000, -0x45000000, 0x7F000000, -0x80000000, -0x1000000 ) assert pyaudioop.lin2lin(datas[2], 2, 1) == b"\x00\x12\x45\xba\x7f\x80\xff" assert pyaudioop.lin2lin(datas[2], 2, 4) == packs[4]( 0, 0x12340000, 0x45670000, -0x45670000, 0x7FFF0000, -0x80000000, -0x10000 ) assert pyaudioop.lin2lin(datas[4], 4, 1) == b"\x00\x12\x45\xba\x7f\x80\xff" assert pyaudioop.lin2lin(datas[4], 4, 2) == packs[2]( 0, 0x1234, 0x4567, -0x4568, 0x7FFF, -0x8000, -1 ) def test_tomono() -> None: """Test mono channel conversion.""" for w in 1, 2, 4: data1 = datas[w] data2 = bytearray(2 * len(data1)) for k in range(w): data2[k :: 2 * w] = data1[k::w] assert pyaudioop.tomono(data2, w, 1, 0) == data1 assert pyaudioop.tomono(data2, w, 0, 1) == b"\0" * len(data1) for k in range(w): data2[k + w :: 2 * w] = data1[k::w] assert pyaudioop.tomono(data2, w, 0.5, 0.5) == data1 assert pyaudioop.tomono(bytearray(data2), w, 0.5, 0.5) == data1 assert pyaudioop.tomono(memoryview(data2), w, 0.5, 0.5) == data1 def test_tostereo() -> None: """Test stereo channel conversion.""" for w in 1, 2, 4: data1 = datas[w] data2 = bytearray(2 * len(data1)) for k in range(w): data2[k :: 2 * w] = data1[k::w] assert pyaudioop.tostereo(data1, w, 1, 0) == data2 assert pyaudioop.tostereo(data1, w, 0, 0) == b"\0" * len(data2) for k in range(w): data2[k + w :: 2 * w] = data1[k::w] assert pyaudioop.tostereo(data1, w, 1, 1) == data2 assert pyaudioop.tostereo(bytearray(data1), w, 1, 1) == data2 assert pyaudioop.tostereo(memoryview(data1), w, 1, 1) == data2 def test_ratecv() -> None: """Test sample rate conversion.""" for w in 1, 2, 4: assert pyaudioop.ratecv(b"", w, 1, 8000, 8000, None) == (b"", (-1, ((0, 0),))) assert pyaudioop.ratecv(bytearray(), w, 1, 8000, 8000, None) == ( b"", (-1, ((0, 0),)), ) assert pyaudioop.ratecv(memoryview(b""), w, 1, 8000, 8000, None) == ( b"", (-1, ((0, 0),)), ) assert pyaudioop.ratecv(b"", w, 5, 8000, 8000, None) == ( b"", (-1, ((0, 0),) * 5), ) assert pyaudioop.ratecv(b"", w, 1, 8000, 16000, None) == (b"", (-2, ((0, 0),))) assert pyaudioop.ratecv(datas[w], w, 1, 8000, 8000, None)[0] == datas[w] assert pyaudioop.ratecv(datas[w], w, 1, 8000, 8000, None, 1, 0)[0] == datas[w] state = None d1, state = pyaudioop.ratecv(b"\x00\x01\x02", 1, 1, 8000, 16000, state) d2, state = pyaudioop.ratecv(b"\x00\x01\x02", 1, 1, 8000, 16000, state) assert d1 + d2 == b"\000\000\001\001\002\001\000\000\001\001\002" for w in 1, 2, 4: d0, state0 = pyaudioop.ratecv(datas[w], w, 1, 8000, 16000, None) d, state = b"", None for i in range(0, len(datas[w]), w): d1, state = pyaudioop.ratecv(datas[w][i : i + w], w, 1, 8000, 16000, state) d += d1 assert d == d0 assert state == state0 # Not sure why this is still failing, but the crackling is gone! # expected = { # 1: packs[1](0, 0x0D, 0x37, -0x26, 0x55, -0x4B, -0x14), # 2: packs[2](0, 0x0DA7, 0x3777, -0x2630, 0x5673, -0x4A64, -0x129A), # 3: packs[3](0, 0x0DA740, 0x377776, -0x262FCA, 0x56740C, -0x4A62FD, -0x1298C0), # 4: packs[4]( # 0, 0x0DA740DA, 0x37777776, -0x262FC962, 0x56740DA6, -0x4A62FC96, -0x1298BF26 # ), # } # for w in 1, 2, 4: # assert ( # pyaudioop.ratecv(datas[w], w, 1, 8000, 8000, None, 3, 1)[0] == expected[w] # ) # assert ( # pyaudioop.ratecv(datas[w], w, 1, 8000, 8000, None, 30, 10)[0] == expected[w] # ) voip-utils-0.3.3/tests/test_sip.py000066400000000000000000000151431503065536100172060ustar00rootroot00000000000000"""Test voip_utils SIP functionality.""" from voip_utils.sip import CallInfo, SdpInfo, SipDatagramProtocol, SipEndpoint, SipMessage, get_sip_endpoint from unittest.mock import Mock _CRLF = "\r\n" def test_parse_header_for_uri(): endpoint = SipEndpoint('"Test Name" ') assert endpoint.description == "Test Name" assert endpoint.uri == "sip:12345@example.com" assert endpoint.username == "12345" assert endpoint.host == "example.com" assert endpoint.port == 5060 def test_parse_header_for_uri_no_name(): endpoint = SipEndpoint("sip:12345@example.com") assert endpoint.description is None assert endpoint.uri == "sip:12345@example.com" def test_parse_header_for_uri_sips(): endpoint = SipEndpoint('"Test Name" ') assert endpoint.description == "Test Name" assert endpoint.uri == "sips:12345@example.com" def test_parse_header_for_uri_no_space_name(): endpoint = SipEndpoint("Test ") assert endpoint.description == "Test" assert endpoint.uri == "sip:12345@example.com" def test_parse_header_for_uri_no_space_between_name(): endpoint = SipEndpoint("Test") assert endpoint.description == "Test" assert endpoint.uri == "sip:12345@example.com" def test_parse_header_for_uri_no_space_between_quoted_name(): endpoint = SipEndpoint('"Test Endpoint"') assert endpoint.description == "Test Endpoint" assert endpoint.uri == "sip:12345@example.com" def test_parse_header_for_uri_no_username(): endpoint = SipEndpoint("Test ") assert endpoint.description == "Test" assert endpoint.username is None assert endpoint.uri == "sip:example.com" def test_get_sip_endpoint(): endpoint = get_sip_endpoint("example.com") assert endpoint.host == "example.com" assert endpoint.port == 5060 assert endpoint.description is None assert endpoint.username is None assert endpoint.uri == "sip:example.com" def test_get_sip_endpoint_with_username(): endpoint = get_sip_endpoint("example.com", username="test") assert endpoint.host == "example.com" assert endpoint.port == 5060 assert endpoint.description is None assert endpoint.username == "test" assert endpoint.uri == "sip:test@example.com" def test_get_sip_endpoint_with_description(): endpoint = get_sip_endpoint("example.com", description="Test Endpoint") assert endpoint.host == "example.com" assert endpoint.port == 5060 assert endpoint.description == "Test Endpoint" assert endpoint.username is None assert endpoint.uri == "sip:example.com" assert endpoint.sip_header == '"Test Endpoint" ' def test_get_sip_endpoint_with_scheme(): endpoint = get_sip_endpoint("example.com", scheme="sips") assert endpoint.host == "example.com" assert endpoint.port == 5060 assert endpoint.description is None assert endpoint.username is None assert endpoint.uri == "sips:example.com" def test_parse_freepbx_options(): options_lines = [ "", "OPTIONS sip:10.5.1.2:5060 SIP/2.0" "Via: SIP/2.0/UDP 10.5.1.3:5060;rport;branch=z9hG4bKPj67dd8ad6-5b27-4860-b9fb-8bae195d6443" "From: ;tag=bab3c78d-7659-466f-8326-61d6da0c5267" "To: " "Contact: " "Call-ID: 9aa75329-b33d-4e27-b2e3-73ab30677942" "CSeq: 14010 OPTIONS" "Max-Forwards: 70" "User-Agent: FPBX-17.0.19.27(21.8.0)" "Content-Length: 0" "", ] options_text = _CRLF.join(options_lines) + _CRLF options_msg = SipMessage.parse_sip(options_text, False) assert options_msg is not None def test_parse_with_body(): invite_lines = [ "", "INVITE sip:6002@192.168.0.18 SIP/2.0", "Via: SIP/2.0/UDP 192.168.0.18:5062", "From: sip:5000@192.168.0.18:5062", "Contact: sip:5000@192.168.0.18:5062", "To: sip:6002@192.168.0.18", "Call-ID: 5443482144267586", "CSeq: 50 INVITE", "User-Agent: test-agent 1.0", "Allow: INVITE, ACK, OPTIONS, CANCEL, BYE, SUBSCRIBE, NOTIFY, INFO, REFER, UPDATE", "Accept: application/sdp, application/dtmf-relay", "Content-Type: application/sdp", "Content-Length: 391", "", "v=0", "o=5000 5443482144267586 5443482144267586 IN IP4 192.168.0.18", "s=Talk", "c=IN IP4 192.168.0.18", "t=0 0", "m=audio 59756 RTP/AVP 123 96 101 103 104", "a=sendrecv", "a=rtpmap:96 opus/48000/2", "a=fmtp:96 useinbandfec=0", "a=rtpmap:123 opus/48000/2", "a=fmtp:123 maxplaybackrate=16000", "a=rtpmap:101 telephone-event/48000", "a=rtpmap:103 telephone-event/16000", "a=rtpmap:104 telephone-event/8000", "a=ptime:20" ] invite_text = _CRLF.join(invite_lines) + _CRLF invite_msg = SipMessage.parse_sip(invite_text, False) assert invite_msg is not None assert invite_msg.body is not None assert invite_msg.body.startswith("v=0") class MockSipDatagramProtocol(SipDatagramProtocol): def on_call(self, call_info: CallInfo): pass def test_cancel(): protocol = MockSipDatagramProtocol(SdpInfo("username", 5, "session", "version")) source = get_sip_endpoint("testsource") destination = get_sip_endpoint("destination") invite_lines = [ f"INVITE {destination.uri} SIP/2.0", f"Via: SIP/2.0/UDP {source.host}:{source.port}", f"From: {source.sip_header}", f"Contact: {source.sip_header}", f"To: {destination.sip_header}", f"Call-ID: 100", "CSeq: 50 INVITE", f"User-Agent: test-agent 1.0", "Allow: INVITE, ACK, OPTIONS, CANCEL, BYE, SUBSCRIBE, NOTIFY, INFO, REFER, UPDATE", "Accept: application/sdp, application/dtmf-relay", "Content-Type: application/sdp", "Content-Length: 0", "", ] invite_text = _CRLF.join(invite_lines) + _CRLF invite_msg = SipMessage.parse_sip(invite_text, False) call_info = CallInfo( caller_endpoint=destination, local_endpoint=source, caller_rtp_port=12345, server_ip=source.host, headers=invite_msg.headers, ) transport = Mock() protocol.connection_made(transport) protocol.cancel_call(call_info) transport.sendto.assert_called_once_with(b'CANCEL sip:destination SIP/2.0\r\nVia: SIP/2.0/UDP testsource:5060\r\nFrom: sip:testsource\r\nTo: sip:destination\r\nCall-ID: 100\r\nCSeq: 50 CANCEL\r\nUser-Agent: voip-utils 1.0\r\nContent-Length: 0\r\n\r\n', ('destination', 5060)) voip-utils-0.3.3/tox.ini000066400000000000000000000003441503065536100151500ustar00rootroot00000000000000[tox] env_list = py{39,310,311,312,313} minversion = 4.12.1 [testenv] description = run the tests with pytest package = wheel wheel_build_env = .pkg deps = pytest>=7,<8 commands = pytest {tty:--color=yes} {posargs} voip-utils-0.3.3/voip_utils/000077500000000000000000000000001503065536100160315ustar00rootroot00000000000000voip-utils-0.3.3/voip_utils/__init__.py000066400000000000000000000003241503065536100201410ustar00rootroot00000000000000"""Voice over IP utilities.""" from .sip import SIP_PORT, CallInfo, SdpInfo, SipDatagramProtocol from .voip import ( RtcpDatagramProtocol, RtcpState, RtpDatagramProtocol, VoipDatagramProtocol, ) voip-utils-0.3.3/voip_utils/const.py000066400000000000000000000001231503065536100175250ustar00rootroot00000000000000"""VoIP constants.""" OPUS_PAYLOAD_TYPE = 123 # Default GrandStream payload type voip-utils-0.3.3/voip_utils/error.py000066400000000000000000000002521503065536100175330ustar00rootroot00000000000000"""Errors for VoIP integration.""" class VoipError(Exception): """Voice over IP error.""" class RtpError(VoipError): """Realtime Transport Protocol error.""" voip-utils-0.3.3/voip_utils/problem.pcm000066400000000000000000005650001503065536100202000ustar00rootroot00000000000000    ,$@4G]15JX\7]8#S^4n5R-z 6LB1H>g?OQHDoGrG7,S9p/UB"']&5E:G*ENXPf}Qa-g6e{%2/-BdoEcgAkMty*Pfq {PooSR ` X )4G+#+++"MVJ&צϫٕTJۄ8J: c   zU +(4jH&7*9hY8=8 ` } |@UK~1{xcMf? !W@4A[  > K>yoC':7w9 /$'$),++)t"`s 3*ѫ^߲ؓ5Z` / U,-  " P{CN {hb 9! |u~o t;L&;/28x(TR$\KX#bK;(`] WOXyf&c9vB:,&%0H4.330P%B /i#3"{̩>w7F C k X (L N'uXE+t H)W")+<)k$08V 3 l_h`xUS[>9L ~v!Zt+_x`R AC%KN#.[119x?j> ;6,\eX85_7RԌɭƱcލxdVa E|Pܘ*؆;>]g E` MICT $q')*H)% 6d| ['T4P1o|]!8b0XGtbJ5#:Y ] "%b3+CJVD5*)I-3X;?;, r c^WR ʇPհMUG +J QU k)A:Kb="I }#(M*'("I \ bHf =K bod,AMpn"_$d_ &Q2 bK,9@<1't$&+166-s.35ktͨriTg']eL D  E . j ] :W ? cK+ /gxkyqXG&V2^pfo BA|yV\9 jjs&,-)$ "'?-0/+"x$cCZЗ:lF8ם5er^ lP$ ^tA'WQF[  EN& o_cdMvOQ;|,nnz`?a,Hxz$ Go ""!  $=(++q' \#;`;+"SBAZ RC8O #XhAQ2V~<8;KegX`;!ofm6[S+ : %('#0!!$K*/33m/|&b5.Qo0'Ѿf Hd:.ߓ( V 9I[ 7bp7a8~iZ xt]p 6vFzwJj' \U=O l[/a _.mU~cpET  E.8:Z4,(5,=4<@v?j7u*zrys %EƨIIͬ.ւן 5L |K }GD J \ f 4 W 9 F M L4$],JB1JKRVm8T 2Je0%i xzc+Vr` y y ? Q ( [ )  Zae!@SV]Fi$>Dcq&FBj#<Fr0j:#%"?|ZV')Yd a %C7C5 } "U9 ~ * k IM  V YkDECGbbGj,:[Pf@#Gggq:Dt@ e0\|QQ?OyTD [} n  cV vR&] P *,bHS@n 0( " F`YyeI pBe9e 9gR EDbl a %v,',*+[^Q& Hhm ]yC{&'n~ +6ff6Bp k 8Z;nux JV l[J5_9V&z\xEo~39 !hoX irb LI8TpH/ | Is @I&`]3O5EHE(g B:] KrhXt 5 His Oj'xv `G{d ["$'P9qRW}Xxp %\o[&q N#  5 (h 3},u!Q Dj\X}KwrK `c *- s *l8# ^@ r # k^ ~i_HI0# 6~ )p  fF0Qc$ dDL]}rd_J6xnyq iEP D G( W22!# rELAz l1iAmn[dh 73onYK ;  Mt < cev  VGI]e tI&e t/ V[;r)NTd, xjEv% ' u  BM=H "k#"$`|>} 'Vu~[E#ޓ=buj +5=*E&VqGq JRs  u"^2G(ntO#`'~sUO%?*' *2r522F5T5e2240# o5ՉЉ#̈́#|D+ b[LR )l^;UPڹsy =v[ T?%~{`AmRH4ANlqfX  [ 3 n+2_66786\2^,(k'$Z6ؗ֊Dډܢ&߆XkNf yTwE gyMo6~-O8xrV" m+i iT_pG'o h:pdts N zz34., oDEr.1kiE s >jR(Tg$c+H/@Dz{'Rqyp^if]]_j6W;=4 :yl5R2*\3AUe|?he4h  " Ws[ l:uzwC(<N?03(vg Bau LMv01 5u,~~I/#_zl8,W) 0E JbI;}-qo;a_]$t M[=X0Y;rBff9& 6v=Z4l]N$2'1u0G/+&:wmn3Wy`9k5 +499+{48XB?b\AU2kaS[87:Ko?|e]"Gf8) %  7aj <r/nLz;;W7{Z8*10>g =mxfb^ZK1 n7QIf-/wd}+1>v;tgadmz*g9oG E&+%uH|`C76<7 fDhP; 9o PAE5JSVY]]WE%^ w>G[4 '>^Ej:] $'!&'fZJ.d9hXQM^z'Ty 7]~kU?%"uc\S@2$&#+3>GTbv,0Fcak{}skj`SMN^UPQ`vzy{q_T8( gjHeH kU){ _^,ijp0)~7lt ~M koQN x[ :;a# k zb[x+$CM- HB b(_`-!nD|=X2Q\a4!"} i w_{]k.QApF 8u5E3]Q\~Q{`1=@D4|Z8 JtYW7Ep&)@_C36#:4L=u" Lo9Q/AGe&'y h -(X5'i%\G[I sLy +(` >]KqlbNr| yHh M\ B -kw o0GU#]%df+>  r9>tU W tt/ h##"Q< Z2b;n fc=GBcAv7=5FHM2iYC F DY  L 1%<)1K773-}(%u#G!e v5O:W"ds i UPxm:;"FKg;Dz:7Gwxkj%D|4 >< Ya $2<@A>+pvSw0< 3 ` `V,zzr#.9B!IXLKEFPe-tygr&dTB) +i!sɁt(a> & ic[S߱\6+ , LmTN'@jQ?} # QV5 AP&1N@LVMlw1v#l``SA( `Į¢'^ BLmA[c  ~[O0ݼݶ&( 5 z 9 +mJ*w dCR - k  \RIg".?Se2nkd][TD/.gٞX~X6–̞@Apf -   > ">o$Op 0g.G/[hI^g [ %ve <~, [J|;O _3q%l-I24563.F',%m$S"2RIvVW " @wnm4y"md^yVM&oo 'i\qe`acCB5t|G"`7.O|*  i \ 9q*#E_+|A0Et*KEtRGIYc``pf[X6y  U,1Pwcv-h9 _%-ek3~8 t A X @ y  6KPk225Wl\7&k.j= )SM9/#;ku0s\\Y^;@gsE 1EuX=}+P!K'P ,17=f(_q?al9W[9BTR,ks? ~4~,g (LP.$ !__]c ufJDPP%%T^; j&8* A#er#{:&;enM7b^4iE|PLmisWlHA+e\Lzh2hV4:^GD_Io 41;8iNO +Ls_ZB~qZ OPPe Rg &s<Rq@ A_m*)!{4Bybsx/.| {["Jq%F 5  t PVhI<v6\zqSazH/( )_DIU[]*K`y0oPR s l% v6   CO9 r=&1sQ"BT')Sw"=$t\\^mm'm3 E . ` 2 O G 6 ^_|oU4Vm*eB\nj,%U?+(`^sNJD; HzGhbU 6[VTGy @[_3 78>k%NTFK$T(m<%Rhu5|xkGUL -?2{"h7Er%~!3 b!C;%O5eY *;g;IyNF/vWB+,RKbmS5wJ w h W%f "e&0*v,(SR7hX(v*. t!PD!))1OMc9ds _Y@^]cy:1}SWp"p&-Jl6M#:0Kw,Y-0+Q2 L/X7;2\-4?>/   wk6TܸS_փ`<^AlTz~ Ir?P(Xit3UY;GK s"FSo x]W VN] qTB{TT |h u2~mC| 5{g2P =^+Us]|4z T7  73 O S&,'&o1<:, /6E9P'gQ>G 3U /}[_ WmM .  "^ VAXTP89E0 0  =F  <\jc^ZxXip{ x*c u`n@  i yVn)WR 3!t6/7"~;p-sba!,,)G.S8h:/!"BE _Pߎwf7d_ 6AZ |7N9&A0Xhjf +]{DN O;vp [ ~ oM y $LEey<<(cL 5 T / 5 e. <:rz*7,}~h 8I'8r f|NCDU+^)/BI bsXv%%"m!"q/ xR \=zl5AX m % ( 7%rIN v:%5iM ;n= GeGNLob_Jub;hJe+q,'){ 6  dO6Z` bkqT3rd/JV]Fk P'Zj Q n . NGpjtC>? q AQ>=^kXCO% x 3 2\A|L. Vp u # : `zhVIplR@u)d _T@a-zH[A g 0.^BOAM6D"7X!&iG8}4)9Y2e7waW@0Cwvwu{4 B`jbbxdh+U& <F^ IwS x oI/RZxf t/|9CNIMf0'.o? *uS0Y;@Q6W!:3XP$\o_fIx1$$,F58,2AB60Hl}wx'nCo!*MGY+Zb,w|ONBQ*ljBadY9=T9ZNoo}j@1%FskB41$tO*OVpcf:^m )HT[Vmzg(Q e'  f ^  } RbDRI,{ZkU 6O/k+Ek%3{P3joJ'$236?8Fwyho1}N ;w[>looK_M{(ps4;A dG7,qF2B.SBago;]JwkXisEE\w4S6>#Q@@txa 7myNu>`"iwvLN%*HK =SnC*4wTr8#>t@&MqPF  ="cy&&o5#Kp<uH0 "Fby2V%)+v jH$`u{b=9(Ilj:vvYVK73??B9'T"(  2r}*RVpKEUK |  `Nc\ ?U 3]dCp|d-.g!O~EPR"f! #lgQV\oK&xWt;+# a$t_!tb*Lt+[ (y& "?p5_V1+Ctv9 c3.&oO|QrVJ j&Y=naxu;Fwcbo, @h@4;kq93w&1\ A[I@SljV\pnN53 {Hn)f9|WQ_`Um92\Y~?ndE@;vQDU]voHCo 'DD,whtza<4P_WRw!Qy :%2x?l3tp"eA4975MM!cRJpoF' :NKov5>RZ]~KL2r$rdJvYQb}*Lt#V+Y&Rt+WtFn ,JiwhU4wH z*r$52Q! |PB'xyd:&9ONDNf+oOds~oN62uxunkr.Ts:Wktu[H=(@sIl;/]|$m\ZwH.0Kk8sD%pZ>,|N@nx[C$cH6qW<mJ.cJ5 4OwEt 7\z~m]L>3&  ),0/1>KK>BGE>4--+  9b&m(^d"@i0QMeH0+9K`|.a-\9hDd +Imp\P;  pE[5`B4 $.>NiP(_(J[muzJ/%Nb"&A\`iad ydk6~b\ECV]"Xp}nN3 @yl1N#Dl{}9Mp~e@&OAV!3I;z]r X~"9, b*AamIILB^hjPP"z & } # x C  B 6R\D7%#4*K86l:}sYG6RxsQFr_*" B90z;38\RxT*_sYtlmM;zVFh 95]-h u] 3 8 cy|ZC> =X#a.d,}8sn CjY\OQ,FWkQ Y oJK .  q  n9Q^7d[+:\\Nh)m/.oT2Sr1$6&%]dK / = g 5|^!bnV>5m1Kv+)2O6R"I9uDXXGT_Cn4 ZXlN%XNG ! 1 KYe$?(Nz }e}iW@L~F!EbSABOFM?l I [  +   i J E n  o wXi^lINgwNVMfR^Y~4@a!Pg4;#jR{@Z5 2 w  8IPP j?: ~Dk ec 3fL?G:1G 1Gf8af,]5Cc0 ,n.jrj[~ $S&D"nhYE(MxZ0 1EAk=DP:6X%{ V@!?3iJ{>- =Xp[HqvRimfL5R6& Io@/LHxi>#0 ;pFn|]yq__u~X%M^Y^%m9+X]P\d g Q}7#?l*xS#Xe)-WD<2n/_5y[YqIo`?61;8cs3')1 2$ yOsMNjB_Phr\`T Qje'z0_O'<Pd&`fy%aoxZ[*` RW{k$&K|LhA Nxz+ 4me0#l'Xgk9{VT}Z@DVpH$ 8F>GuoG i*'@M`=W~n\J@8b(~d:N+SQk#<1 5P& eMl0e5Lu6O$:]K+_eVi}c([{Tt<s#RZE88thyS =\s-@9igM>24 X=*3L9Md%#iiL*u(qD EeAAg=crtv,\XfPE@ ~g-t]$SLbZ#yNY n7&1}RC i$LuP a?2Msb`n0UzZB X)U$"A?sAC]%|~kZ\vi :w = SI<,ur=g1m[]6[}u*Yl.p<  ! \ i$  ~ )1Q:~*]v + d  je=xI7M{`quK3   G jgw8=m]  b49Ddi +$"-%M%F1DDWNE5%-_* &o'hOI`yibPn͞Z> O_#T {{)J@Ex83$ 9 7 >! > z+d\WFxW ^s-Vf0gr )q@) A} |l&-;,P&4!~A %($L 9S$|p"No:&{K*4I$ZuW]7Db )BMw gXr K , ' , P . x< {fc2uE0-.~t#Q7+]tUDc p U n & K6?Xi(P]e;w!KimX!$DUmL1?~{GOS8C!m.B7ew+K%S+O!K[i}qI |lTSn*eG10(~@Bvm1&=r*:4.nzBT%YnF_w{.ahA$X6 v`Y4gQF[GCA K(h8>*&+7JL/Uw:VgdL'w2}LF V2:6;uvt,LqwuOwFFojQQ\Q="kK//|FX!T?/!?b.tAh^K1r{`6Lv!)6YN"AVwkJ;Q Ac ~zqg_YS5|HV@b"!G|x,n q<RvmS&y=p*A W$gI3'!0NdqS 8mP4^ qA^ a.v;sM"!;Yn*uYh R!DylVF:,~S5a;]1eF9, $.:;CRf{ 0MW(=MRYvrrG.Kl:2 n#LKQ;J7P 6 #S$4 Yn8L$:J4 5dfT`f9K@*2_a%)EH-H VO x0>+sQD  Ia k k\Zs9;?)UxFVi[Y@5ggL3/6& yw_x!mhmrYAaI!#v$=9spcJ~=2si<2 <lJ h%D;& Y};[g&[O 9+ a:i x:m#xW^HyE.i\2HU "-+mu?qmL;COEffm(>"";Q*X? |L;T &:^ IR9c` }yJ`OlP*E YiZ2U]8,  KJ * j N'J 1),k$Qc5&012k)x={`yMؼI-rc AL_M=ZL)Nvk;7!NG8 U R 9 s5z,>=-" v,;@? 8*h {ߪ/\ F-I>f~R^sajj CPk Qn ~L @ l p< .KTA-*"'!S'9HJ@;%*pS1OYLYشE:y.I;T qBK]WiZ"gn4o !,BW*b<)p87d Of^I6F.3?!Pv[R> *ƻ~ܽZ؝A% %'" a]b`&OPCj*\s(.*2!Je "^&"0 I14U39 A=@e[WK>BBHLPQJk>- ` ,עĕpDىٲח)p)33(/ k;Hf!zaѕģ;Qcl(p  )M*#p k rXFMuWI@MXpl6eXZ*)*MTz [S̾Aئ֌s`RiIK 8"x(+=-*" q4 T+B1d{bzS`SN[ DI"$! ,)SQqZzp " `  lBOqX~6K) OgJXXרMwwF]Z h=cPG h4 uӀԉgL$6. E,}_A } m@B& 4no`0}g Zs@#pc!Q-F34t2.a+*L.4;;3[T4>_ZR /oZ$(+R+(}%#$(,1120M(dd<ߎ޳xp&1 wH K .rx%('X%{#0#$',/c1.) Y /6i TO߻݀iܲݭ,eqk @  d5-g`M*s5:{r %W d!C+3SI0~v, ,5 M"),,+((( ,z0+440(j? R"m܈r/׮ބFXs^Mo m $^z@$:,nj ~xu5UkU O {W_5`;= PbStfA"*].Ud0uK}njW i$T/"67K3.+,04c7T6=1(d6EZGB޻\-; MI[\ G buC   -T;,}g o 5y7> rb]3{H). |M4Fix.eDon*6429%96m41<1000F.u*#oݚk[Wob{ׁIIaW//?C$z.Q*Zm  %&.+?wK  O[ ; 6&31;2103.k)c!<eQ$+fݼݽۻ٬cc/ kFNY]3o1;w[L: ^;,|(00:8Wxx'Y4&K5CD\ Z,8I! ?o-5:F>3:3c..U110>-'!] K<߉b -a&ܩ>G <g_5G4 NQ߸ݻޚ߃۪1=5S\WA }zPE T?N w+9C(o$U[ b+:l  -562.,*8+++V*&!2mEa ,hdf4, x/ #Q L%Nލ޵<& )N   "i,"LMALz0{fBAq:zXh+p=->2P.'&(0+*(^))&!S.Bbsu v ! I>OHO<.,~ B~  [;g5 f$'InG-uk"*( $o"%**,*+**+%)"Q RX{- *a 2 8g.R5A-^UE/Efn R ;  j tqQ};nFSxkZ[iT [%'#j!&u*)(+80a2W/%)!Q 3H?giײ#B'aCE/ K  HN[= |HOI-(c6h$QA[ M [.j z&k2I(T%" m9NsMi i%v$${+,+O+M-,E& ; R4D 4Do^+Ul { k  u # I )$R;E :W}SE+pG7  m t K3{V~da@*CI9Ks:9/n~B> r~ m%sg _1!&++,(&$; 6i$ 5E^M_JY0?#()pwPG|cQ0G   X7;Gx[yaMp#?xv a[fuVs7 K ( n 01 n  *> r4.Iw ?x[[0afcp#1C?@b:JJna(N!uMeqRs=O$L9[ ~ !#C)^u| b (5:rE<t_k.azz1z0p *p{$;%dVmm&LxEcDrRL # m dKl! . ^8 /H"H$9u[T xK:I2p/N P1[' k^lUDDX^sUxlUTzc` q ^t 6?;2Buzp#U0_S)qx4k53r>TUdrD0X=58y@/gYt9".RiO=f6MB)(/A0 \\! S|  wjpzAW 3C,]b oF# & u_j8&hX(7CM]`R8 Iz? H2Xs_a z61+=G\ CSeA"Hr  s :!#MoZ6 ?G :"j$Lof2*w _"^)Rp'k&a.@1'Z^%Tyh: >]U$S 9?H W[/g,J,kL M  [ @ h X C =  \ 5 |mMU}oz#81ds ':P4bU`>d/id-DN 3"nU`"$~`q3|uo0Yc' j"yHV\| e= WBI8 + u 5 5 " f atOjE^H4NQcS+3ZuP(ft?- fF`xwu"ZA\1s0_Bk\"!S,;XEe^"nX:3:|4%0 b-rGv{sW59bT  j$eY{cRru95G}I G0XZ{ -eb"Il.eI;"(dcWO]$[FNQNQMhde<^%[JsHw:3F<pm<~_}(d4\:, X%=*Gt4 ;['Ppt1;2s3N#HFI5HFq0RDSYD>bD~'$J>08PjwN _7tL]&'rTS[2nRcQ(wI"?=Ou:nTmV90O_<r/U[fwWQgb<S yGgcEady!r@&9p=;t?q~9uI58EO'**lP/2?_' mr78;{*#KZ,1bq$&a_]NtSRguQD`aUk/qR`N w# Fd/-Z4F~5x:6.y9.6(`t2*Wa|\@+v&}in`YhR].>3J6i7:$ SKL W{}|I_hI|\CG~JRNwdt<KLma~.TGm2Re}"] wq.T'_"2Q<%`[ ) QWJMݻk Xp ]b\nZa !V#"ERmV|l'i-NEME` _Z7~ i"a(/5:=>N>; 7O0(E ڬ[mcݤ^u`>XC / sVmߣIթ֊ٵJ`V H~Xy T4ly,.Z(8;q13Un 9'Z\aQV q*!5=KV[[ WPF8\) oВx,]ǣˈ7&ԝ13 )t2774.&k@mqRK/i΢u՘٢= *&,H1;3{2D/*#]am:Gn.ajT &6mM DB5]o_on t RP  .0a|j&"$AU%8DdKR,\ax^WUmK?X/b \hh1J1g P #<*.10O-& hޥ؛ԨF$] ^x[=t#*$'))(&"Tr YvX < $%%)-p+$Yn= lh2h_gػ1&=O^l - S}A80Ly IO%6HSVWVXRC(4()>Dx(lڬфȗin $2MK+N- k˒ϩW۹1%*,w+'"U dK-E)szPP 4_ _U/%*+5) $/ /]ܙۡ۬Ku ޹޴"M B[ ~ kRAm^lr Z =~I  Q f5B;,+_$& #d | g'֩փ#ߟXg3 #l:R/9XLkz S }5U# @)I u 'I^\dUc 6 L h G WC=j_TK! Qn8`DeVw8yD.=7'#(+137uB@s.R7x'X޹:o-,tJv=G0."?'&3!: 7*oP!kղחFܒk,g/h;F)}I" ~RO{ye ' }d _ wCDnLPj$':]# @n( R: (p8cP0& u]781e?'3 ,$+R7MA5I -i^zܨS -zS;dX6 q \ n <q @x)UT+?O]qy;S Yx?@#> ](:,H"6Z Z i rwJ ~cM $Nk 7>wZT|}"2]RKAf%mJ \MYN d5Aa8+!b!2F19=6$2nڸˈASzn JwRR ?i @[^2-?mlp ] Q m k f<QeUtIO8.gI`PW 8X x hsyXm*4*FuJgl7@mFz%*. !nu'&//6/X 5A7F-x(Bb_"~P߳ځ\GeW F$w$ Y El/31,>q u s" ?;  (fyML^PyeXm#+[ ?r}&f- ( S60QQ%(W,rcL!5oVC[; \ 9 mq k 1 I u%,E*QVP4J25t`zf gLH6SAa"0_6 EXdEd ;IXc0P4 07A;j[g] P\*A}sjL*N:F+i?B0lm2oXOt]{ Z B/ppx&G~F/)z W4 6?n lQ"~&9&E ,t1 ~ } RB)J TvN[n0 9 -CFk Av H$ B Ow7 s@xvhM xr}Z:c@?^ z.  D[o$ .@be  l~H D) \ ~}P ?gVK7o q' _hA%97o-d3 ?ji~&[^H3? 0 {TdM0WF0LZ ~Z(&/bRf\ Ms # q o 9 C   : oL8dh 6 `  8}F0@ckZO r X%C! XV e#gy aZ<=&Q gj 5,!  SC- O:sd/d''<- )1'`tc & :M` AY26 U3+ }   B 8~d ljn5ZmOs y x 3+XPpre.S R  Dq<V p%58l=n ;LCJ-8$^|G&D:.UyTP ks _{5Y~yFVtcs   G ` s46lj5z d@ %~N!}N.~:bN= oNr un>.5dF6('3|ij0b-;V-JlzdlbV0qv}\ l%T' o;n,SdDr <be\N[A@'-s\x" 7 1/y7*!,n7h>L.<r%u ](K'[^d7332#8CT&x<*=&G[Y(FsDr^<&RQyi ' u`IOtF?Y)o K O4@Q:s>.5Z-[{ a 9|`:V?7 %dc}I^0I- e)0t8{`jnpe' XT.o{+8 w<`};oKR5&6m(;EyEnJxDU? ;6ry=oWP`2]{ DFtf\ggtO +a8W]$Y1^*Sjt`t`|rusWQ_R9%64bMP)Dmu Nsc1FaIuFF GRo IddI#{bL0@A]6zZJg} /YZ\P}YW pr :a$y8R:|iG VZP3Ggut#k=~Rg32\AuVE3"ro8bpX8-!4`G me>4s4vp>h<@Ga/ c]hH = m  ' BLshLjr lB6rEVUNy>|6E%} 0  @ H d P  8uA[_> Z[v9RHA4  [Rl Ki`o lI l  0 +  % -W,#h 5 Q66 Y GC3[ 1+1 5 v *`bt 4aehB 1UIj]   KBIO SW BSP ]( ~t  3b_od 3 ;9V0AuCVV] v`.'6b$P4fSp6Xm>iy13M6 =D/rY  Dc ISDH eHM# B [WX UXYnE:h\TA' L28 I %?, i dK { tr9n28v., D1s k=[ MA]   78qz{O Ji ~b| )'3ss526B < R 4=-$ht 5 V8)U 2 Q,+G K_ dc Dy 3VDh mJ# 6AjlcaU "o R[V0Yry*Z:>*;w TFv W B{bEyeQ nIYn$#G w  .TQ U3`GS BOG*3wYpo2_ H66/ 6lC`E $2 "lKN! -XmrSH ""W}6\ U ? v 1ibcd}" j : 71ewPT ?72~HC v={ry @pC &  S- y"S{MxQ0 0 |GkY F Y 2LZ< .Z v;_@&8({Y {s5C 7,q H N & B  d! 8 Tw&kTKWQtu?/ (& +& |i ;2rL @9P9W- - "Uww2n7 4< #+2(b}n>t;1oa=8b cM' [ iA  y KIU x T|x&  "x ^s"Gw .{0^J H# U3&0 = J / !9A Yj ueM+#%y 66 .8-t3t ? A6'C$%) CG yRD% At} Sp O kH2  QX.zT R! NL_ K   7 3 ud] (} @u" \$\Pfm_Le;dBJV O%*g mr9J'8:*hQjW9 O p |++4qky|Q32E9gF ;dL=G7q}U L a0KRiC4T~0aa7+dAIzi#1#s~;]_0!xd-bNL_9N2  6={,5T - +^/WeANd!6zT<_!;]]pF[{ &% & %(e,5A;>AA K y2 JoP Mb>/jsSC} Boc8q M r(L,-2,d-/0 /*'+.C)'s-|l\ߛoa?P@  , C  D r2Ulx{6x0Rv=YB, ku #+1+-%#$%#!  L)^vuPMt"(?*Mgz[i x9 X6o,Ao@\8 Qu 6K k.= @ aJD>#vg<C< 8:^"6 aV':2Ld}-VYsyNYfM@j j'O/[ a d @ i\EY ).wnbp( up 3 Q~  @ y  ,`]9!_IiA>Oc.gM2!eMk^=Tv'"&x2' ( T4Xl$ic2L /H>bhmRXmt5 aLvrxM Yii(# {6yfUSOS2v~Z; \nOBQa  )[B  ,C %hEY 2 S H 2Tde   SxlX @*   v`/`|{6n? p. 0 aqU | }F?  GB  hw'U *?>Q  e  )66 Y7 6nK 9KsQI+iIKC  ><S"Zy JV*7F ~"S Z @/CL *Q-? (y \+0R w\s}Bq jg`/ wX,a1 a< ] Lv:   p mB ]Y CYU"D:C pM 4hDR *3_+x ){R~Tv^Pry9 Xd 6  g{~ 1El c:E/ !tkDq5'83G~a#e\'EecZ/ _ 6g7s : "G>   ^UN@Y:1U\{"=kL;<^ V =2t aI,+eJw~ qIi 24G.|W 5E_J=9-b ^H\~k ( / ^y( S_ 2}lZ2^(4 { `X58z3\[esQ~3 n`+/Nh Ri59L3u4;e-z5^Uy s:3n0`)S=I1<3zrDd`{kHi'q`cN|'LH`723fYzwcwGdM+abK[[r-`9`=iBr36?B TaDxjP]JHKk8XD9wY)(//.}KbI$ EQlrLd4h.vaMRw`p\",*K{{+#|'zrq|WE~W >B[$Mcy xK #@$ HSB(~ ( a%H??/:I KX z2HN}nl e2 @ !&  i|/ 78S.+ZD.> uBn Q  i  ?? (/g#z  Z ND3 l kE9:@} N#Z!/ m j|Q : .< 1fq^ o cH[m >qX9 :Q$4uE r w\U!R/LHQnl|WQRE2K?=Dn uCiZ?It/ h _k<I+w}& P#y  2O8  $ Tg,W2s0U ]{v45B a l$UD;sTAN " f m^C n  7 ! X * 1+fk( {^au}+)vE0i&Cj03;0 }bT |r=,88 /$1!!$Y# -Hpqoc[TړQ[ { Ii | 0T_2-3Z/)d ; @ ; / 4 ^O  F |x,f;Z>,l.n0-Ip_R1sP 6vK1:v s|_ 5}]ip maq eV #+Q+$q-h0dK܋ܚeS2 d ? kmrGb$g({gW/p W G O g WoM@&E7X vZhN+)qk ? ! _  8VBRfp]}nCU]4N5W5<( 9T[>]/V ) *n67 ! Iy(pc ]q:cIB|#FR?~!W5(ijT*>Q@Saq1 P'y~X+r,oGE`}#xZq"7P" 2NXYt1zx.Bj\uO|K9^{4l%U-I - S ) 4 J/0E =7 7b Pe,=KQ X:Znn>N6;qG 8O|W-IBe ##,Zu?\*lK0!lg80 O*Zmv{~"7e ]r-@ "  Lr;i`4N 6^SMIAyYs8GURHYTm5j( fkUQz`b2  ?6NU=M\ePQ "[+ 3|C +%bt\?F6 6aXimgdu.kA1 *`melyyPCq0Sp5rlP7g""@D <6E> sA yRBQ*ZD-B{yM8_znTUU}A)7 ,za~UQ+ e5HMMu[5X(vQ-Ev-f1] "ANE>8.* R l@v2 i7ycVD (ciWqEdI%Lm{`L0{.QZ<gd6 ?c(L%=+&::?HTh*[W P$0&+ b.KCbf--+Yp)u@ N>52<`<9Bkt*8t;Z c|x,tk ,_(?H ';="{, T9sA:=,ric@N0O~2%[5a.TZKO`sT$5,T.UV"MbOD0,t23^,[W8(:nUO #Q.PrC/h 3S 8|xE Fn^rxtO=I>FH(&w)n'XtRcWv#5?KG[ksLlQAnPXH8Eo@MrJg\T-](Xr!",cjDbdCd;D!ealBUn|Lz+  Xafu8"uBtJz !lQm lr &v0b=@MT K +=3r2 f^iiVF1%L0Yw- Q ({X!UYc# REu5 h ~ 'Wa}c T(*o Q 8g@, wt6E`ZA f[e  t !euZ@ kp% 7LG'BDfh/bB4'f ? @eV&zVnm : }U2 4uo[m.l1I* ++R* "`Y08?T l; 72_ s#\xow^wT~+  wa =S @uMCL~7aBm\6>)=l CGx Tk TxZ, [oTV!zM=C O\| nt jL26L]m}p{W 5yiz' >]a ]/e)OEw n "7 lA# Pj|cF DS , 82ovvu2!O|, b| g KMM M8 <`=7/`Q }r 2X{o {  -1.G v ] (Ht 4w U9_ N(A3HqT [P=rr {dn "t-6 u P]Q Wug1yzQ &M0  t C Df6 w ', lZ .  3vNY:T_Q {p4 4IS s_- @n7F F 2uIUTxGjGQ*<;dPI ^;VJ !1 9\RP}v|  s> W's!n |QZ 3 ],5T7Q PE;sYT +7o  Dw/V^R r. d 4 P (A WxN ! ?b% 0X9~^, R\Np03vez [ zs}%F%|I/6 h ^R8  T,Y.J7A,ow7^? )?snn~4?ATi @#Q)YW8Ne2V:lnx ( 8b-]`F.;!0"JQlk!|*b45J F]|W:8c.r8^+WoM$O1U @+B{ Bn cf  9XwN._%ZA*5 b m+kx/U|Mx;dqFK`M:S)GR c8b6 Y#(*+,.I+)Q-248~3(((4E3JGԫxb~]t _ cF%ݢW"]  RfCh 0RR+}zS MN;~pU#+ -681+1p763443p/$ hQC#"SY *\ y& ^"@.ӀӬ>b#mr0,if@&: qs5M l U P  j.D[h1Q;A E ;9+53r*"!^#',R,"c 2\)H w SM~ޢNS`&l. OjKsc5y.{Vf  A. ()SY~XnW~FT7|Tk/HVf5L :&03+-#81:s:t2%;.U)Jڶ= E qgLMm)UڥmI=%    :ti n<*r 5{g L B{p{r !i}>-kuQ 4+47638966995.$POZ )yya=H .oWR@۟ ~\tf #) z g w i ls joTe Cx4/s\HM%Y.,Rj:u] S K +]!(001*o(*,I+'l&Z"fw yS2pG2utI IAf.N>Q c|D5 D5@}4m\Q_}c0(D3[{Av#Z 7'>(-RKZB(Z. ETC o  *  l)c%&%A$"" !^2aGߖޮߵv=M Ona&gpd' pyjAR@0 #  E e3XQ ! Q1k:}\jxu-u<Q  - - -  f<@.pI2d|?&i % w 2  @wU6k>>1DS.2(8M@OyrI#-q"_;EkvPh L=b_(DrLay91\ Y#>6):"G0F+Tj]_fH43<[v0$U}^EHoQWNi>zP%0?WQw]f %L O8taTRi8>2)D J D|VJ3L4gYvc19S@67~}b &fV n|2?W;!Q9H( lZ-!Hd U[fg:O` ynq7GW8D (DQtb X vYG  d6' ZY WI[=fYN:TX}z;OT J5KG%D\\6 ] S ])JU!x 0Leg2 _K }W&:kQ1Y _dvC (R}{? 2. { k{xN*eG+Gil`_~llY)"m 9@CoJh@'4z]A4 3)c|o ([>"T Y -F3GP`V:=;\< U(KXG^nkQPlMr|]-VLl  mB j d( poS DYLhn0un @h|LA/Y-}}[+g3+ gwPaz"IUm- 68U[W`%damC*+@d45]` :~1[%< VysDo"D?lOk)_.< )L?Dj::5;1[B|VB*{g)ep72"5J{!$_ 6gx K<+w*V}O(4R v(frh<Pq5g]6ZEe&//qwM3rTL{ ^ 3 , % I .  }\ @dTZGw<.-!S 1Up"0X5w VC 4resBaz=ghkOBm~ |2 !}v>B+L2{{ Dki!} l [ T A5+gKn[P~^ !#0&87DW]6X*@~h0 ys ."%M&!*#./&'].no6LZJ 4 D*r3^o*q 7(>E F d5O h % *U Rq!'{a\Cy`Q P N E  VVu jRi CY):VY]T tXdsd c Ty nBjG,-S0 c ]'u9{|jptgG^o 9  :H.70/_TLy;OgFyHA0v-k(J6EMB0{[SMF:;BGT[`XPN:NQrO6&"5Ml-e IztaJ;( %)<FYmtf?gw6$k6K`voHUHdy)-yqIA_}#unmC}Tu#Rn0;/Ai JHotZ3fjP 'DTg8bh00 ^@xJAA\Q6:L,Kg`SM> /,;B6=+EM|tU,X*Tw$Yg GSA|eyaA!tmT7pbQ 4iV4H,d"XeggYI%~ C&lq@35e9*g8Y ^VI]ka9Z,ZM+mby2G1jqlmDa4u:M>7)INyU2C V?n) eq 0(T, F(utz3 (0_S2<}E0Cy~ `_sTx=[R;Fy]$r.bf4oM%E nVytW7K6yp)?d`TO"wow@AP']^p:zweE]zWGL{2_|0gG16F_d+cP.5lUXn|tl%=Q6fN%[na([)ux&h1c^k2;%sF1%n)6qu% p:&^dqIvjwk:>BPc_A |/(Ged^y;TL$d:_k62iHaB}X5vc/~s M .LlqsY c`?>Q~%j@~D}%n_ (r y+vH /6R;Dubc P s UO 4BMT >| +c SxQ -`-q\)( 'n{ rE!:{J^S d'zjR{7Y G+ 8:g lWd g*yeGK $ wXsh; H !1=BiWA|?Wn(^ } A-{tM :_5UX2)>/I-KF e Er-a0$E*QrXkd~c-&A$'x}9>t_;:[3/'xF$!V'[GJev5%g}Rz/R6 i?~~ ni J0JyiV , M'Dp,uf1Mxqr i [V ~i-jkWp`Jsg#9#]aq ~8|RQ9{<~ JH$m!ddK4F xelYtvLGIurBHL-Z;_O [CF!F^7Kp9X ani"q*wzJlg~CnP3^{Xs L`DO 0.>b8KX}B:D#MQXN}He&v/ScYgh:)lu~I!' AY "P`31 Qn.c~#`hr%Ne.e 1'A-}gE(,67+@)2>\cAYXfgf,:FAm`-s/C-nUIVe{1brBTpzT"A+Yi*  WT1Sls<i}YEd\YBJ ?U&a yb^}W%p#Lv1Lx/(Qn2~[( e}Aj_A_MmX\_M hR'\B&+( 05 !  :! '  A* (     )  $! !6#3"<' ++=-4":+/0              "$  &                                     /F#p `9;oQUh:b <k 5`6d}L#N,w\hEE$ T|!+ 4aF@JuP PBr$&"eT7~06SE#anwM#.} quF9+RG|u>AR=3!k6o"us*u\l9lxsc_2ksa>=r; `<4C;%hUPEC/gARDHRil/!@gu`5_cXNB&6YJf~d$ {_7l?Cf(UVV` 8$$A4)8^It`MY t:`f@{{ -#3 mF-Xf8#' m16] T 1u c"Kf)4:(^@x BRJd,MGI\*^ )lvqh zdM"FM=^{SgA,p[GFLn#QocRhKen y0,3 UJ y,+\h5bUdGT*m"sy5mHBd=&" Lhbi\Ocah+Ce,E%E*Nr 8Xnf)4@~1$JhD9ViQMrr_&>tPs@GgtF\oq zH BOR;Cf O2!7VRfk^kBwwZMk6%|+tufD<kn1&Yy$.'Y_%}5"a<7xSA %Hsq?;TeTo3*GP# s tD%qtau609mGeesd\-#J>Hx/tY\k(&xv`CA`K*t/t9\YHL{5yTceBzWeh.jub.dPxKi ! ]G8\{{ B.^x"zz76PF &t  ce z yG M"&L  "  . BYv y3q\.rU*l_b}Q<Co  +;B&$CF!)z/10H.n041&~"!X Kq e?شO YAפ ׊;5U r  =_  /y%#rC/4!Zw 8g1k v<,2 z#O-147f:2:H:7O0**-,  sgP3#;ж"n\E'y%'  5 = &  v  RK9o;`3TmV(NJTdl ,* J1 h = ""! $)1 8<:b41>2C1-X'S$*  sܞϊ5rс~ti r 5 j ? f 8|g ^e#' scL)a_=}niB=q ~ Tt[ }!@-L3,>()20[6-7?3 ,h&$%M#M[ \dܒٳ- sޠQ+A2mvc7}bSwkBlW&hF0qR'X< 7T ^#"p G#1)030 +*/O1-%L r5L3٩wFڡj߲^ m 2TV#G{W8M|&&"d6bzS S  B X P")b*']%y&@+0d0s*# ^  dD-po;A]tl0{fgshH / A7Fr~9f$.!?,Mg(WxLl6?&}:oMS\f 7@ `U**5- )3&&-X571{*y)+W+' hH9 g7kduVדו'8M EIdFy{;F :e2!JC l,$5F )D b4B r&& /8;5]+]&-+c35.0#X>+&6R,ۘ<$*Bg )[<.=TV6T '\V1O"7, : !<6+#h+f.*('&+24,#"=$ N F:Pl2("-NE9:C8_D<,| N6xk+CNdt-on#Qhl  f :sX |$%#"L&-M1d-(&Z(u*(U#Av!dPYyܜڗں߯t[qa}mp]skk:~UjBA`S<.}$ @= v}!+6.*&'Y.75M5-%#&$'! e.3$e/ٹ&cܪ 8;VGI[1#g(F^[u'RipwcN`fd ) 3B7$*)$K"|'/~3.'V#$('5!  <P 5Gݢ܃މߊ Cܥ܄t*@<5E/"rhqo5[PzfFhgHoY)a: R b?vy"&%"!&h/3/(%H),+%g rW\$STpvM@BY \Di+ NE80 JE3XB2/.3!UdFW 66*F<Q!$  )02-)o)9.;20)B"g6. r'jߧ`'cߵ0saD~=M|[|)8/w"RZ LU pc#w+-\+(O(=,13a-$";$ % 1E )_rڭ~ܪݑ<~hQFe@Nf\@01$4 # V\9c.kJz2#+.+++e/6:9K5q222.1 ,o$.o:y#nrُZ_ם Iݬd |vv xOr ]L$ox{]Ek{cW$Fn3j#2 Sp[)'p./-y*h,16[7+0+(k)'B#]# 1N[(fq &CG1ޫRFOjCg`} wYhAA:4N_n 2m- !"/+33 2//;4:?;4r-m)*)%D V _zޫLw6֣]ܡ %_pz}]]m riFB; *CAtQx2 \J / &\$Y&1993/1}9_<81.+]($h :޸Jd(<ِՖҠEiTco>s;}]}%,4z\3n;;  z 2('./-)G-w265j/ .-.Q+$ ]J~x{F ٩ۋ:2fg,p,uJ51(4LH%Y {  GUW "!!&+.-))3*)'&&}$% e J 8*\SQۨ%N8O [YG zi{zRam`>t7cX  uDD\[-@#$I# "_"! EB HBCD(߉"cyhFcn6:::R/@9n;jX /  gK:u } < N L F ^ / dUy ._D71eiX.v~G_iZfjR)?0.FO5cvN zQ | p 3   J`  Y8 ]u}R W 4 Q} w st _ c)~sS| u V`iEI;qjZk,|IhwKcpFW5L + ' ?Q (v9WcP bCc 1 J 6 RhPXfe2V{ eKy ?S}_8'V w> h8s{t I " 9 k <5 Nh+ N7H iL*C  - x=8[[ L?2E`L[-nh?P]8an~ & GF (nL WA6.SWXq}a JmI.mt zaV (?IT h J h8f jX xN {J QC A6DGy&ta!MXC xZd~Zi,t gr `h1 ShC bM;]% WKd 4 _'Tzv0 MKSy}L8B u<T Y *n%a\v#V{mHOss3 ^P~?g}cR Ce ?vk5 i\iD"rm(-Yo+C58,>~8uTz VT3)u_qPFm?AiOf?~Lv< SI72T@ebF cv&?_t/XM.O;P~X `Nq;a>B$fE5v\%e.HMPOK(|p^uS@ 1FW5)(LTpum4 5>JrWrrJWV:{YI{#K~&xNd  ^TF(d)W@y@_=fMj3l(y<X * kn~8^5;Rw!x;k512jSwg8dHx{B|tY'e R0yq;KiWp66[icf`.+"$ ],v6$Xp/ Mu]e=gH3"%#bV&T9vaed.i|1#xSL :NT""{rE>@I6|J0V"[kp=Gc\q^FW-G;3Y@3=\}|c z@JjC1(v{jxyRMBx}S$cn?%41}Gt9cV-Wu<- s[v !XDCw@`DAu9M"KhNK*+{@Fp$\AS"D"!?rZqLHa?& c1ԻR&ǟ_FɋЇug|h*"8&" }ޟֽϿ̨ZbLw ?g] # dQs")fd q*0 a;>YPC.94L[aV=e'5 k i=`Nԟ*ܥ)$[)"8`p`[LPҧЅXI}߹W;?  TZYGԲ`L@H= ~jp >S=,M^bs^qN]=0j'Tj 'a޼kheڼXB*,!jVU 10=|R8˓b۫ ,Jޢgwqeg ' , @ Wa)ݾCx! +X$ 8 "81RtO8,~8SRYbh^oqi@Xb=S\YgfJٲQήdj{!7w=-b_3'YYrr @ Zr߲ސD@ ~~DIr9ΖE2TZI:=1M{cuxyRp\E0nB߅VCַhߴ*j Un D']|;@݂8;qRQFYBK \FksQulW;Q<]OGFZ<K ]H'M(pu 7|Y(^Y]J&jr  wWX-aN6P& S[4>ts6sBw5>T>RGC$K'V^c&d`Q86Ez'(۹xi׵!l\ v!lS`فPЄ֩UZ*r ISf; M*M %|pV%9M,_YL7GNyUUOkIqF~>* ӚB{b ݒY osl OJ 0XUn[߾.))R<>SQ yJ ~kW.S^mT m WH\~} !Q |VM / )Qf7P[ ^TY7%(I}z/Mp^\nPD?BH6M{NF5Y;Qޥ.8ٵGh1 1 l F?)@[ܘقڬ@ 5 "    VuuIOzf0^$ E !z>] b!&6$G&]`wUUE<:BNDWWJ|2kh܉TD,W~Ѯ}ٝ GN* #m=߿߳<%݅ NI6& 44h wAqKeh Y 3. v׵L8yOPXGUmJ@=EOUb[dJYD+ c.@9׍O֟PPwzl3 / [#3 ZO'k#ݒ 4e7&-w  F]]$ _^._ D } --L  xk"8#{ګܽ޷..N&7U\dgd|YMD:CLFZ/edfT8Q T,ٟg~՚֭Dp}k $p7;@Ҟۻ!{ A P3S&QJ ?7*,LP :(][P P^JC=ٛۻlBގܷܶ+SBSZb`YRNIQYbfb^NK:1ddOH}՝Ԇol m e MC Ub_&JեOڎ~&M!0" -hr; {+bI [6[/ " $c+k2t?3#'SF`oph^V2SR?SqRK`>*!*R٫ɫ"VAڳvJppln~ XB׭8SѠ%4 Z&N(Y&?! f:" o}. R3Թn(%XG  Ng #uAYedXNL5C9@(BvFHC6#{e5eƱ̩שWa(a۸ܻ<b%rv jzVuOe7xڛ՞#v h.T BQ[$Yfc_ ' D H - ? 11&/Z (Xj   ja?$;BV7ZNF?4y259|;X9S2'{ at?=@-9R`eD  .43kL ы|ݮiux*%~>' /+  Dy \LFs$v = } J7)",,d*DLPRb8  ?0<52a01o59X===;4o,Q!%%z_ T!H`Db  itZ6^t bqP_<+ Y_A&3`&i 5 H c y [ Eq,,NwWA`1u8TI!2 %(!4O:;:;=@rCDeD@: 1s% (Gt2)ܮo e>kY(l-U\^zPtJ>UPepU $ iW;=o|D.sza9"k6-l!#^7 :  E  ;k<gK Ir 5 !S: c owOVQlV_Kh(;5qH.Y|@C{] > p)R&*$,4/AQxph!QN)[G5 H '  :  M  #C!9tj'i`_>;aG1(S8p5[;)wDZV*CoDlpV?35x2(B4~5BWZ':2tI/Vz  q L & }> 8& Q>0+%$UyrIutUh~ttH% .F! YA~N &RWT+ @%bd)f6v!z6~W45q6q KgpM ENT~tPXZB7`E]|` .tq0$ g.N`&X#=:t !p)`z*@#O}>8vHQA#m2s _S F.HJ48O':Ne<_IH4mK. 7E`rR/5wi"% R(v*+\T!LP1a|I5j :rd@  ?v(e0Uu6.WR MQWuWalGXF'!|aE``gD=!EQNW2HrGdCSG~sbEc!R_/T.W2}>.`^6Ua{#2 I001Y:"JH[oq~0xWv*Y#-<Gr9aRm8mvK5,4 w,&*/Q P1%(B&m 44,|Oe|9VTPLXWE87_j.>{jS7iF W/ +t=!>-y0xf~`*`2A]A"$Z:+$ s<S!gNxG$YidfNlcwqZ <HP<>r8v4n5=55e MO%5V^e"XCbs1wB_PN$N8uT!-!57{8%?-{~[3-{ 0dJP[qI$`#\k9Sb6 OTdtaC2~X h!iq. xPEyxpRr(gJe z2MR}f1"z%rdk!;65m \SM0I |?dE/uY:irmB:7c]M7-*<\;SVC1WUB3sPzTX0=cid> ~U(y ^Pq_5L (hAc_8P0!V76O\@h/KM-9CcF)57'WN[!va*# mmO9C6M` ;Y",2u530*|"Z p H 6WJ; Mqc|8VxUW& !3   e %QZt x&f&Td<HOdriO4%J@Um  xjW:3nQ=V!J"i p;gHdkgOR0A/ ΰioĢ́݌Tާc] ;7CОQVS t> |JnTE xw1qd@=4Xay{-  K%_l2 i 5 o  gZ3((0I'c#MnXxn+(nD{z\  #{7GUidM82-5-HmW> *t L} )HP/>sr&i'T sh!j 1S/W_o a%}9JSgDiV{?0.Zf5ͮAGacC!D  :ϱ̅̌\6`!`g`V $\ !a 3o*/ $a   q$79\3 : In42y < bY#ssZ.k/%WjN%R,u "k@ @ j}fa!G7jqR/WS3#w? n$q8<:%ALJ0 |;g;X9+9f/ocڌ֡AcnBB#HgbL 3!PX i 2D  :[ g 0P,~nwx@9JeV(pkHo_0,qK 3)3sa=)W]@q9UBqjb>].?fA-APE;i-:3R\KUJNKA'0dqrcڱiy C rM[ S3Aoi3 4y[05xbfx; d?nvG( nB9h: [\, G  z v 8I {vgf$C }nrh6[ ,/ YZ>E/odLFxrZ%=g<5LsJVG= #.y~ ~ l%1=EB5 #^Gf# e U_ojLk @T =ZJY T \ Mrc)_ kQ ~z%O[s U 4=H % vgrPzn6c?oCeL ~n)QK|\s$ } 9 I\4/NU;;- YFq,  { i:QSF$B AR(>U0P3Uu2z+^UqCobۀ-iW* 5$sGj#L g S&=x  - 4CM [C ][d!`Ts"4^&C`n:E#}.=\twQp$'%w(  'V81!s"*+l('(&?p(خ +s]F. ' JA.6,NA+t1* a (  gz{L* ~9oh <~#SJb7b"/,;4g3X3PadT#n;86 gzI IsD " q ] ,M*m&#+--' C?>Y%L7U-aP5hhY /J 8[fzWEm/M(< VG+`SH  D   tpca\7$jS+~Ei>]oAg2<_-6! qnr& T /S r m F ( CB) !}  TwXށw?GG\kR25`WX=v >/o/vI $ (L[AhhD ! {ql z^%KI|69cuZ 0OyT`)be'b(+U;xka Jb\ F.U[ na,}?!& Jp8g5R/<\G8p\( ~R|h =?JQ#A\W's x w E$hF?i H5AueO#kU9%';3sOJbk_4)1{' R% JTE Ch@Aw2J%RTxhkk4[`;$"!aqu!1Z.{ $ x < epw*`N  9II%S_Ez/mOIIais5~ 6]jg_q|d40-bFi~IR(+%"d;xkblY"+`lU@@!(Ub'=Btea%ow:e~+} h  m!6WM~ / yC`1WJUN ^3GJYg;. Ex!ayX\DK&N_pY'{<;QDVd O03C?peZW /PCMY0 z.Wsw%((#i!#\#:I  ^";)ٹ؟ق&d9תؐ~V''-|QY 6 + ,  ; T$_QG0:Jh~ !CGl|lvUju< sde:A`%k*=;g\XJ;x!#!!#%+6??83-1,.(%"/ a/iza\FOG|Gي9V2{ ! )VrtOx$S=84  b 7c5AP  1<=?8F%: T5e~J6@ B =)w4 /axUݚ:)ތ= G"K'N)6)B($!% nr#M*0hvEY-=AS84  w!"(0%=7H;NLRVUYV[TRNF`=5+#!p3="peǡoX@µǛe  J {m(+eԛе,rd$< ] "M* 0100.)%;"kL  &C46Qnb T8;Dv?8].zEX\KP X(0BNvm PoI aNޭ֣͜7ٺ[!/eO!(-0L1I2k1i.L*&! GJE-O XV2%LA|`,SݱDh/"+7D)PXclnj@h eZ[M\A 5%@q4wՅ^ؾ締zʓx'p wn!#w%j#B$qZ/٫ 5(Y.37:93974i0*$ )Wr2/7G2[RU(^N=ܪl,! k2} %m2LBRPZRd7mrsxpjcaSC1o"*ݳF?f*ɻ AKf~$b)2,,)d$" ѳɏÏۺyԹQǺUT .M%,?3k8:8}8n72,Q(p"\{ 4CpH)`F.)hxoj i0.e rX m&O_" 8ID5!39$ZcxwD$'L *&8HN Q^P{,xDzP3O^SoS6 lZ~&}:Pp=|(`m+=fz,t/c d&7rIu {a0lz/D|U"7V3u .U&X[V,=u)D`RcU)e'#q4~eyF/eS.x_TUUrlD]+ ~IT9\mae_Si/ng4kO5~G@aCdjf6Q9>7L A591&I\yJM&$WhdD%hc~" 8" s~a0c 63VHg bU6|l]&A[b&6M3>b'tC8 J.fw9B]=&X]S.gF_X;cQ 3 S6{ >NHxYq'QQ,{G&sR97H6 o6 ,2E::dQoEuN*W FNFT t  gxuz'V^ka c D V uAULdi;;6^AT/  ,  mO r,S|a?> *6Zs#.DU]7(,((M(U|;9"~)=R3#|JM@JP(k/y8W&;fA-Fe46 FgA[Ob_L;EWa$d@6r8X?xz] RlDVIYQby%GrU0^LZ|Fn%xSr^OuMk#2*On4[ "-t Rwv% H  v) t)'L0P-HeP]qwCkBuX(]u1\"5  .D%f&^#U/0e)')-)1(4/#k~Z4} Z*X T{,&}L=w]O9)3?mO|>b%v0}ZZd1+Lp(37y\ 1  B!>'#U"'+R/.f,+* (i#qbl4cb hK%KT4SU* ;d3s .;-d.;dq-j@h5"iR>OP  t }AX'ugP67 +:=B1f~BB'QqQ58'zDv/rMg,HG6F+`;`G<rE yL "N%'p((((&#!"$ 1s=ql.+$m!]m"'= `z$nQ5_8V[kRjtyp^,GR8e~&pAv ;iW< " #! v |YG tWHs_Aji]1,' Gq%C}M'Z[iR +ul AA%/S  G |":I S8>I-k,6jVQJrq"6QFOr14.YJpicV &spy  1 Y><}U 6 ' tNI|BL7nSlqc r {L)`,Mv tY [B#9/oUK]ey*!%fDLqVJiV4V__82!irl4[HZMBZrU*.G(W^<+:Vwr Q \HSN4t%E8&%llHu d Gn\I3Z#1,ZuVd,/^VM{h 1+^,BY,0gp$ch6M,bRsSmEK2 Ey(@&O.h:K#L!?>?p_V/ 6uW`Wh^G90eVc|[[C Rn |isV :@<?r"keih~] t!* *)wzgVlow IqZhyI/z .( ncu A;}SG i7"mnW/&H+JrO.@g{/ " 4W <WYl!KvX32,{@w:&y:4,5mz[o - c s q&n_ 0~|w  H^c ES - m$ {9  k ~m 47 znNv"Xyo*'RLUuun k+{y+sD'.)kI[   ~u  Q B? E # !^Q~B}D|K`qJ.TC@o+l-4#ns bQ (-2Q*"&/6021552i/U)8nrp3YI;ӓl6l˸Ձ@tҼڤ1@ 3[ ~B 5Y1y< KR$+&/;'{%w #"!l(|/o.,3CGA?A?3P--5-j'h&BX̔\g%.# [ a:  /N{TN_A+x *>@KU@$4jK,$ %d'6NJRG!5/y;AHDLHfB76%yX9d[g՚a (jY٥݌ ڞ {K%  a #   z#QMIZjd,B_(c>0C,0]SU)'|pPim(!i# ! &9ED<2/81[9=19l-"uWb E`߅٧:D53ܹ֔5T  W|]-nH2A1]S%\NovuM(n^a\K~3rqJr(kZt. m .83o)_$R'~09?q9c+& +#c&$KFE<-"߂/GZndah JrX2BEer onxD7J1P ] d:  8/ssgu0r63]uo!c|J4)P dklX9 =##}" }!N""1>Fk IP*l*\"_OLNco/|9.Zo3Zrs})KjJ5$F/$(ipe N Y G F h,MbaLJ [  D  ^UZmc& &Yi;1 22gVdNXd]5 r6/}=[} h#jjCjFr578<}N1S"7_ M  !j GT3 Z rO9698Ac(.$':>Y8UR)C [2/ r H G k y v _ . C  H+J h::`,0kQr7F) ^l 8  a *D%{mX i Q z ^mfe 5tbYeic(f5ugN$V H&yxQ:SUu/i162upoPcmLMgs8 1"r'uB&8 h O &mOb 1 ! hsN,95{4= %s{Wq#33>Bzw p\ll  01O!A?<.e2VJu mmb# t4 *    V   f = [ m b : M | ?'k\WS0B2t#e!! 8S D|kO"w''cgTl ]iJ@h >^ *2 k  ,(?,6.82}(o%K20f!q cW oT=ԢDȼ͓Cԇ+FNCt {  / C? x "  9 % : -NBjr'AN:|CRc|d Z;C#+"R}1IPQLKG@F81^7JO\zlffJ:3IO[nZrL&Ex?+ 0 ._ˡv l[VϷۃh$?GZk\<!f!_ }ZmJs_bqC6GHb*k|a Q= ,w&: 4rYG3Rz]yAOH5 V+o( J#t+00,&M= -VFѐIV|3ޓj$jstE I/]b S10ݫڻLHNU8SM,2&tM.lw\wnW>8iIXYMUA,3PlGJPeΕr?Ɛy?4ֱrִ N$X/3332-s(?% ku ^) k߅\Q-e' Y[ms# 48 ((k}`g{@ڍvnJvXh~M &Mf|^@/]7vK^|q{bD_5572'Z)V)ťɻ.O@ z(.p14@6z2'D%, VmItlE6 (iT 3i!Ia 0A7ٟ;ߓ֧ԑעݐ?S1! $cv 6OJQoKA\>A>GMgW_\`O|@6/%Q tAμ_4~ZI޷Q Nvw) "0%%}"]f}"ޫިHE>"C+p?=i? zL?1;&ڒp*i߈$#;$F  P\('4@ED ?8e4-4w7;<91(+ 3!X߳4ٵѩ4О֣ڗ=g\q, &k . M*_ &m`JD Z&BW= h%6_l]Q \\AqY&/ GX'! (,.-"+(&'%$%# \Y /o?WNXf`\O0My eV!Nf\z,}^]bJ q V   2 _  ! z iQCKO5q'}gZ[OD>qc} { ! ;^!n 4  /8 u35T :RCV 4@/O$6Ql-Y5|1@U gh-*/s]! va70UF$ mM$NU_^0w v a,, |>%F1x-7chK%:a_q1,)}'k QvX- ,"*1t55f42T1110S/,&eP!TּKتgBW GAAPl$"%"qB 2 G$s? : -@@A?:Gs@0.o&nip K 1Q7.Q+c 4jMo"bPw%c(T/McjcrWPU2a2o*xLudH%)`Qp,J*Bҽ7XZQ &:5EB:7 )@w"%|"#0a_6ržБ-4Ga,)@4::3*Q#}U  +rKd\g<6ik?݈ޒJT u%4JJ`g\I>}D1Q ^pd_\DP#@V?[*zo̅,M(L.7-%|> W >ہwϜ£<ïŷaM(./ ,(&#&&#4 q]`&u52LPO3g8 \Q .  =PM:A9=IHS_5cRK:I(b#<&:!ce}8щWO"I_ˤX>JS85 r`( )$ "ٔѕ̆̚1ЬϑՉ,} sp& +)&{%d)\,G)"sNF U;-rL3H)V&A_|pt !0+!<"z4L\]fg6[kJD[MaVR'Ce1i"4/G DXcʗһ_ϒjz+S? F="#1) 6>-DϙƝDN*p 'X-0/a-I-,,(,$*^&Z .hU=YlI+K?&R$MQ/bY30lz] \,+<_DR@^=DP&[sa;c]!O)>]665/"lѾҾǘW`TyZ""cuyͨ((ήӶz! =~  '#K,/.*''a++ *d%OVs߽. ns\u Yy M=eu C /#/A:MQVM[A!=5)|  a@n'BQ.Ӽ߬UGK%9[J$1UtM!@$c Q^ t  `(El'|N$cbfb /e .7y%*N)%'N-[36751 -*** ([# r+ Tߊߢ&2jc VS +1P i*, l '.mQuUU a hXWB u78TRc1#K#SxE  2 ~ 3 s xO* ( ,%"9 Kdy_ X0cZO-\8E]3 J * S m _ H   &Qh@_  r C  9aLNdJ|F3;&X:E & l?  =_g/B& Wm<9A u8#cZUHqm@* zFk"-r }@3 7U[G3L `% O 22Zy2Q &"Mz mmRqwl,[~)$n v'S 2H# p J;5W'/ z|JhШ$ %?/#6; Wkr*EL&P )\ 3 KdncN'%'}}A_3 6} @h c'p>y_cHg!Lm b& Ouk x Cd G zx4@$/1 I 44 >S v < 9  r9# ]i J FXv Q'owӰ٤  5; $v* ~v2 W''K-e;||!g4*?-%Z$}GHn? qfK[$g 8si kWpq "}4)QKQw"@^F>V !{H 1s ! @J4 9RN79OaB 0ڿ =t~5T|f *7Pؼ/"l 0[1]߰g 1!d tdVNhUR!P%ʷB)-`u8  8fdp'H_!+J_H;5aǜ-#naVYl*&. ye 3 xaV۩[l*`@7 =A9  @H u ;&h!B !'7 #A+f# W˵ܸ m# 'E  I!)Z2 b.oD(Mp+% nc!_5 [r#CHAtv[e ݻtZ*#w!#8$pf8'^j# I zy\H"!f. J>-w% r R c&)'pJЃ& : y" hw3  e,YZ3( A}o |(  1P $5E B߁%#"7@l L2}% z4!w rv?  )"3AJލ4^^U- /Pg:!o 9~ݕ e& Q v Dd< 2?'\/ ߉Lk + -"  &s#B Q0Kd* g{~*E  .y"! 3qrM%<^U& <" ~ؠۢ2X(> n# YiW o Dd+ m'e+ cN7(AZ X e]^Kzk D `J+ Y "ja v,^T8 vEb n>; c% Li #*? $`Os) 4(~6 D[Qu< =i <+ 5./ n xv`D" F @ng++ &G6 EB ~)*4,x,fs4q,3. "GP }aB >AH4jD #wH83ep'oe`R /, Uxx2 TMr21k6~He{mA9*U/ay,6Zwe7+, kQ &Pw&T$[zF&_-`rQ;s 3QB _r8 r? $^VlVs^h+ #dv~i`byzy=*]p_/d( 6KUuf  _QPM@ A 7b/mX/Y jKahMl">$ w1S5'T>-XrL8_<lB%h.4 [FV8#\Om: -]>E_s!/*A4] 9,EG h:'< } 4<j !4$" kt \&QQL 6F`<;|^y&iBMFpKm2+h]aW b W 8nNI+ V)Zi>[!j8&$M.qI0lkuH&y yw1l4T$ NA/ z.LY_u#%W# ""A!Zi|c vF&c' DHkOjPFL'$6SgEndz4 ) [ ? G H z[dHYl[pe5KjIm=MascX6J"Zru96.,1w"W?+D0xe{KM6obj h/I@ f6ZiYL!!V"$Y&$ 6 _j3?h)ja~A2CMehKw oy4SB1NJ J?8{u  ^ #   [ h ;DwU8T[%6 ~>eH<2+p 4jn$C4ShF R l&rh3'D~-RpR<^|_Y~K[4L/GrXT'\& w1,  &y}T+;a:0hcW7^ t/Hfz(gCNLH\|v3H#(*a7/[ bgThNU'JC< ~inHJV)## *W6XqB!t iVa<: $c^ =MA  k r -dXrh\jj e+,l$ OGc/u,"9^^Py2jZ1v?s\iC z<dX]lMOnh<yGB<1 nn -N {Fl&,eizI~Xvskb*;dQ*+;i37Zh}Hq ],#/%sUKTQ-}c5K3$zP.&%:;L_4Fo)4UrJUyhk<-\, ImjqE|5hK&?5n!N&3@t $W>Sh*%0N&/h9HIo)'>+Qs`CzP\P,? wOW 5 [ 0 * nd\:Fy$gjgv_*/1m) peUOs 6 ]4tp_4l] q[O=6M=-6m"]|5/ \i0$9^`5B}.\csaS>1)3#{)6 1W`T<BOco,4J-M oUR9_ %H\/BB QP(q=J'xGr.|#WXM4C s  :|V_`pTnwz-4V~D\[&Pc~Xg5i7#\ Vg +nPWky|K2pnLeR0$wUc-a G#i7W~#Y8cfU^ONlUYmTYc@n*Bj3 ,Kn'1c d< ,{ _HM,0{V&"RFLGy  /  DQ38?Qx4!Ce wWT<3)obB.6-G?IwN9C3`*- <u"(% ""f  . a3 aQ=#&M^ 0  S !uo20& 7aZ8n!Z(MUR{uE>tX\>B8q &m '$%&i)+*5(y& #JN(P`^0TS5Kky= *t4e_~/POE c ry w BCOdFdt- ;-dQn~? bB5q=+j6 9 *   % I~5d:eCdd;Os/`E`%Q U2S.)j( + ' ] Uj`!~:l> ~d?x'Ls==boF &Uds5K-HG.  FqJB$P=5?D;=N'!R|>*bZ L  9 o ;t`-io"h @8a 1 - $K2 a  CCD"b#xqa?/j6 Q<iV_?x<dw'A3dL#vO *6{ O{q!< 3 # ! &   i9=PV(> E# l TB  |[nD '~PeXl$|OI0rGs:b_VpYlx*z;c! mLCrqz94u^A\1p1c>\ B R3 mv\  bW $!   sV  }>d1%B$P(%uODlm (_*pb"$b$5,k}u+FhjK\ ?e= o /Yko 3 Pt 49g  SH}= lO"! %$ ~-S0* >my)F(qCXo20^>qc'Ckr-%!v i8eGyul@,fLxzOmwu>>of#-75  :4E :r]t,c7jV;3RE'}`svW 6 I =VF}}) 1$KQGUyy5",WjKk@b?OiDpZ"7 /TD%i v*rlY\ R # JwuZ_nFpa`3q ?jM0zq zrPk6?LeQjo7_\y3vQ`k P b K4oh^ B6U q B `D{ 2 {: J e N&o o )lM C 1*v.BhV yk6h9  :WjOu_   :%n"Ln6:}"U>94i[`SFjS  *( 1>cn{AV:$@M1qeUD tv3I < E bX1 sU z T{jo |=F9AiTRc qm<h L4 j I>c=N>/GjbTc' <+-;xR|: teY vl@AQ V;6 9C]f^*`xF 9VEgG`nT|$ a .N*%^!hi^uU;-!2nAMTY ghFHDb 7z@tQ*|d, bgD<>&+6<#;MH\J- H a!MQ"j+e DtYf Px/,g_}C'W%u8+%p:#%/ 96%\!iIQu^W S-W?  4.P \ e u e>C.B 1E05"rglH KD=BGyaP ,$|e^pWHl~L_\ koCR'zMK9$njf@a?p@  z r I $ z c V & e&?.(3:4,i(#e f!$A%!GEr;@hN܃~r^8WfRJ gRYueec g&r1AQYkCRh`.dcnO*A,T5b&  > E.DJ9WJWS@PIA?=;X82.<+")M&UC)"۳ۍ+1R 6]z rL:EAؤX׊>d%b \Bgx^ (QZiX9 ) i =h(RHaV -p=4D FO >#)0A8AK&RPLJeD=4H*" (B!5n~g2WI=ajX؃ փ U߇ 3 ^# -8p]x p 9  -~G$;0߀GYT>+7 wY;6 9*=R;]\[(_aJ[OHE=@0#T N6֚!4#9ZA \s? @6ݮ ]k^ }/U&7 ! z< ]ZH`btYg  N u Z X*6~.c&k*i yuK#q@ 4'3C#Ua5c{`ad]GQGB;.B"Q +&ɳjlىZp y )\ lBv"֎l aj !N"! ,#KA@V>=<_5 R- u'Ove5!a9ez$#'1(r:[%?<  1AJLR>YKaxc\SMH@5-%_ ƦԷٵa=;" ߼Y7 &d^okg6NߩܫۈێgN $!a"v!S +-x v:BoAnst Ev7Hbi[C$t"N3_[v 6[`S`P: O#_*%19ACA@J@<5.)}"gE#Uu˩~/T64_I };\  rDW!7@b e !@dx *  R % ?V Xt%@ >QEM(p'4UC2$IX7I-Kz9N. RkV#()-26X8G8774/,)%xm//j)ޯDGcE%e AF ;Mf<0/&R  9 x w~M4#WuOj2!xUJ1&-*~Il!X' 1  l [^(Gbz) l4T -! =*/A>R%v3seK.jSEyv'o'fwkT_tJ% 4 v#e!W.[GV2O! *wHB%)AAXU (U.j^+leI   P' h +D( Y7,K{xdqvab2R(=vMU[e ]Y #{ B u z ` 3*%e ^ON5 +%Z NJP\1:/ESpCdu}Y *  e/Y> J"#V$n$$\%$R#!k 6 o~NTUI/F[}%z2g92 .4my/he7 v q M = V D GY,e!+uMyI[{e0'd#lb5FlZ & !%!?###&$G$!# X pM NCO3s}e3f%^>uPp_-uG'BlD K  {   Q @ ~ bY_Z+GS?+6P\6f\.4ynG+uB sf B}"&!h""K#~$$N#! (b ,uIQAqJO>h|149@w'ThP'g#hyurtlj,$*"!r*%@ORumTs nF!3 d)Bj-<tX  U%]x a@)F=- `2Ya`t\OH#t6}L:lzGN)9`[_}s? q  FbRsObM\6y4xu$.$`u>S:qIU=>rp:qcytNh9LF/ljSz@N At1wZe^9M^'\tI k.e-8fnLe)U.:-)$*)U_ jA(1'5a4696/f*'U"$`E+._ %  d-%z5f+9+6sxcvw)/8fz= v ` 'P%b`*9yS&o--2@sZ(gU $4y8$0.0o1],]% " j,cTۣܯ02 O \ & ] hJM!f4 a vb>T\21* 3%'iqORLR)tnZ<QZv[ ) { / -`1#v"'/3,*$#"h5.ߡ.9Nn OZQ5c+d/;],r ` ^n v04dpLRiB]sreru!7@6cW[ @  o W+ C !7b z\Ym ?ptw?Q$q!3z]P'~(!] q #+eM|pp 3@|8;_mKuBM9G/ ;~38 Bz &R : ]~I  v ]Y?5+"|" #*)W#b; 6VAqB:,i- d  R/ /B RgoNs.{:e UF B?6]" :N! c 6|uD *Ru4 H{^eW 7 @ %)j  z'K:L B{# m>* 9 s]/I;9T9{ y: \Gzv n : 3R&4|T{g| *e 0zi a%W y<f (1#_|\lEQO<xk A)BIgN  :5jI u^%>& =Dya 1Z$5 gP I% 4 F1H( fJ`ZGo|f  N JR WT DAN >#V = Q5B VzEMs C`v \  p-s b .J pET e~&cdd Y-)r"# ?.+QeZn I7 !: pH0^j 7/ /+e ~OIn SoUa`18 vQ21B$?;;i\8cd 4x[0M d5i& xo* C t 9gyIY3 |6taWYRbgbm OM |d ' ~R B`.}>4S gw 9"nN ZEXOUj _Fg 5j_ MF I(zOD" hO Sg,:b`^  sDHPb.`,`iD}N 5'6 V#toTYU aB l9:4X- pP5  0ZowlhJn 'k_6D\'yib5e 5gG[+=N6Sy 6gZXPe|c){8" %D0HNIDt5DBCDkQV[= B Prci[`v x}S "86Aoh*u ,"^1za]! T# 4(&k'%XpaqG2wv0?ZK"pp&xT' [ .  s a_2WID@WmK-FJ_9|:3Fn4G+hc[3({u`?}7r  T8 F!5&Y'h%4(*)F%: |   =Xqa 1sލ>ڽث܎|$yjI}!V x u Y  ::&%oL6'Zl^|if#;B4 u FM D E   v ]/R#Sx~_sP1Le!+m)A"@     +mc.d[YuA ' K4O}Lw_3MG/<9*<,BLf 2X~b^Dz'!C}nxu>A,[XNmz p\ )K7 B )NB 3) SW ! e,Wq jJ^N 4#iRZ4fo`^'3x@ q3 hQd4#6;Md * _M,C-= !pcw Q ; 0 )/ 8t : T d(A~ "6;xdU)=B%SL8*bHloT*oU>LZ A [eC%5 r fC\ ^&[x Ne L Pil3 /m'DrVC{* s|Mq  T S;I:+*U%z>vhL/o~g mf\ A9Wj.Kr\]Z38@ea;MD TG b   ?&H pf) =^|J@$mC( Qy cL sO76 @& [|;n^-H!0f= X w2J5Wp]u?[:7x-aW5e9Mh~0/XYH .QY BRBTme,G/*@ipT|d+v F8-T}u3kCrXRN!^c{}+>dCw0.78|**+AxhbkCtp"ZRuv9R-Zs _517'_Kw5*jeR xb 8R y w@uQN*l~=B$&< HvbY=%/\FTy9?q0MS u-oSL%e,^E.zhs^"i(E?LRTCFD[*)  d^vk<N<^kM8 !MBGrFJm4P`g:,!d*\hL;_4Lnx M ,*4[; ,X,Lte<r4h|&D\vX8Y zh0{c> bL\~ZU+} !DS r\nB(\) ~ju \$*E D];^-2`h  a&:ge#$l D6 +uz=|hJM4h B$'$04&\ z $UGQHVِ'}X:C@7 U XriN'{*V>S  N  |H( yohMDEh#<>ak (d'` OLB"J%L"Z1#2I06!;850$$"Rsx?I>E٘ށإihiܚ$xy>  PxI (} [54VFM*=(c^zu L  ,% SE)Ocz;EJ; '6jQ7=hCvvtp"(2<:f k*T&tFI%f$(G!1y4*N(&~E$! s>ieh8R aqh : 3(G^,B ) q ! kpq_ 1 o ZmwF]Y[c =xk;k9YD[&G!os*G  B|K gU]y&"gvAc* 1L?1}r6K W7ErjVu[ 8(K@_ m6(_&"&]*l%96 kYTmܷ&1F^dVu o u}.1na*m0n$$ RIj  y 1M-RSO?[]n\OC, <$9 i]!//\pr`SdZMFo>.,Q`Qm?E#btq W`y:Fex'-| Et0/d::eZ$o7r*){k8&fbm zkY6e ~ z Wvw}x?4#$Z:C2lk!&h#sFiXPYB q  V{$?/l09 j[7w(K]$}FO >:x #*r{[| t [cM\~[kQ=Fss i._qfh?S- Z|<,dA"WiZqv[}uEgoaLAw 9^RN 2 eob67l x9x{S{[qDyZ%Sc LQiYWynEyX<gKEFKevGt<~"GeFH{LxtC%sXQ@*{Z0 )Da -Mlqv~At'|%tAp " \9Wg-.{>y0~IuTx V;}BqWVso`,^v`:}; xC8k$^SZ5!y+a32oD2^6p]E$N4h2=`&=H[cn~ ";Qcstwq]<4/X^ LnG}xhOQQ?N`v>]tB!dMz!9H]fiaVD5%zjiW9nE~G:(}WD]TUc]ao|./Kx/@BIm{8h$nF wAU)lM+!SvD| 0`~~xaKC,87!$9804=QMANS7-6?9 zeW^aaes4HWnzvjg[JHBA?<0/(z}rcemf]Wt:BA\goyvmkmXD@;# {jhl\RRNF:914@FHGKVl{&<Pev{tnj[SIC9,' }~vrys~vz}r 8GW[bt{dc\BA/ aCnPv>G.A/[05)y - CmvM R`dh1+H3]`}@as}-4cu~ay:Kt S2&iOi=Lp42{c Gn p8\L-]9= zA pZx  )a iO> -| LLS CuhdNL'DEz LH%A' J$fUJ~) 1O,PNoA{f#G9 :[[ r !:4:u`vwvqJ# #`  P8 MGraxc;@0:OpSC^Le2;K`OwU14t%iZcq[XM;3U` 2ADibV!^OSj[zuY /Yrhxc:_ ?$bTMNV.t2yeuGMm&%2B,eB+ `s~e@)af, {~ p)DHs &mKS 4K%!S:6%C~>l }Lm5WId]Dq1_;0^ I hZ|nn<` [sOdNwOSWVp XT`>bI)d.<hI@-,:m@Kb FcSs:hB~y/~0Rv >E_U@)w~mN4bny` 0$,`*>)UkjjS=s{!U]l7h _HqCc>{#%/(E #FWTQ`JYjc_Ru'\qr/l|T>Q^% 4RS+8xEs8&^@mO>}LD8Ceu3,M5h`zg{s6,*>Nf*?z~;A{M\Cm<dTQ1V9F}6V_r4o<zE/$FhMq0kM!o aTm-?b (.Sx 325U8jscL1W97_`$YGJg. xb9RUp81:5VK$!Dtg Fe<:NtsA;w]$~W!r2i3_ *~I\iw |.\'YK/ 5?- *SC T}Mrf$HM Ey8FM &]?+SMA A: G2F812D%:{88h433B0408"%  *   ! 2=A(-1*.,5( %3%"+! **    "     "    ,&%)      '" #    "   #(&                                                     voip-utils-0.3.3/voip_utils/pyaudioop.py000066400000000000000000000161241503065536100204200ustar00rootroot00000000000000"""Partial implementation of the deprecated audioop module. Only supports: - widths 1, 2, and 4 - signed samples - tomono, tostereo, lin2lin, ratecv """ import math import struct from typing import Final, List, Optional, Tuple, Union BufferType = Union[bytes, bytearray] State = Tuple[int, Tuple[Tuple[int, ...], ...]] # width = (_, 1, 2, _, 4) _MAX_VALS: Final = [0, 0x7F, 0x7FFF, 0, 0x7FFFFFFF] _MIN_VALS: Final = [0, -0x80, -0x8000, 0, -0x80000000] _SIGNED_FORMATS: Final = ["", "b", "h", "", "i"] _UNSIGNED_FORMATS: Final = ["", "B", "H", "", "I"] def check_size(size: int) -> None: if size not in (1, 2, 4): raise ValueError(f"Size should be 1, 2, 4. Got {size}") def check_parameters(fragment_length: int, size: int) -> None: check_size(size) if (fragment_length % size) != 0: raise ValueError( "Not a whole number of frames: " f"fragment_length={fragment_length}, size={size}" ) def fbound(val: float, min_val: float, max_val: float) -> int: if val > max_val: val = max_val elif val < (min_val + 1): val = min_val val = math.floor(val) return int(val) def tomono( fragment: BufferType, width: int, lfactor: float, rfactor: float ) -> BufferType: fragment_length = len(fragment) check_parameters(fragment_length, width) max_val = _MAX_VALS[width] min_val = _MIN_VALS[width] struct_format = _SIGNED_FORMATS[width] result = bytearray(fragment_length // 2) for i in range(0, fragment_length, width * 2): val_left = struct.unpack_from(struct_format, fragment, i)[0] val_right = struct.unpack_from(struct_format, fragment, i + width)[0] val_mono = (val_left * lfactor) + (val_right * rfactor) sample_mono = fbound(val_mono, min_val, max_val) struct.pack_into(struct_format, result, i // 2, sample_mono) return result def tostereo( fragment: BufferType, width: int, lfactor: float, rfactor: float ) -> BufferType: fragment_length = len(fragment) check_parameters(fragment_length, width) max_val = _MAX_VALS[width] min_val = _MIN_VALS[width] struct_format = _SIGNED_FORMATS[width] result = bytearray(fragment_length * 2) for i in range(0, fragment_length, width): val_mono = struct.unpack_from(struct_format, fragment, i)[0] sample_left = fbound(val_mono * lfactor, min_val, max_val) sample_right = fbound(val_mono * rfactor, min_val, max_val) struct.pack_into(struct_format, result, i * 2, sample_left) struct.pack_into(struct_format, result, (i * 2) + width, sample_right) return result def _get_sample32(fragment: BufferType, width: int, index: int) -> int: if width == 1: return fragment[index] if width == 2: return (fragment[index] << 8) + (fragment[index + 1]) if width == 4: return ( (fragment[index] << 24) + (fragment[index + 1] << 16) + (fragment[index + 2] << 8) + fragment[index + 3] ) raise ValueError(f"Invalid width: {width}") def _set_sample32(fragment: bytearray, width: int, index: int, sample: int) -> None: if width == 1: fragment[index] = sample & 0x000000FF elif width == 2: fragment[index] = (sample >> 8) & 0x000000FF fragment[index + 1] = sample & 0x000000FF elif width == 4: fragment[index] = sample >> 24 fragment[index + 1] = (sample >> 16) & 0x000000FF fragment[index + 2] = (sample >> 8) & 0x000000FF fragment[index + 3] = sample & 0x000000FF else: raise ValueError(f"Invalid width: {width}") def lin2lin(fragment: BufferType, width: int, new_width: int) -> BufferType: if width == new_width: return fragment fragment_length = len(fragment) check_parameters(fragment_length, width) check_size(new_width) result = bytearray(int((fragment_length / width) * new_width)) j = 0 for i in range(0, fragment_length, width): sample = _get_sample32(fragment, width, i) _set_sample32(result, new_width, j, sample) j += new_width return result def ratecv( fragment: BufferType, width: int, nchannels: int, inrate: int, outrate: int, state: Optional[State], weightA: int = 1, weightB: int = 0, ) -> Tuple[bytearray, Optional[State]]: fragment_length = len(fragment) check_size(width) if nchannels < 1: raise ValueError(f"Number of channels should be >= 1, got {nchannels}") bytes_per_frame = width * nchannels if (weightA < 1) or (weightB) < 0: raise ValueError( "weightA should be >= 1, weightB should be >= 0, " f"got weightA={weightA}, weightB={weightB}" ) if (fragment_length % bytes_per_frame) != 0: raise ValueError("Not a whole number of frames") if (inrate <= 0) or (outrate <= 0): raise ValueError("Sampling rate not > 0") d = math.gcd(inrate, outrate) inrate //= d outrate //= d d = math.gcd(weightA, weightB) weightA //= d weightB //= d prev_i: List[int] = [0] * nchannels cur_i: List[int] = [0] * nchannels if state is None: d = -outrate # prev_i and cur_i are already zeroed else: d, samps = state if len(samps) != nchannels: raise ValueError("Illegal state argument") for chan_index, channel in enumerate(samps): prev_i[chan_index], cur_i[chan_index] = channel input_frames = fragment_length // bytes_per_frame output_frames = int(math.ceil(input_frames * (outrate / inrate))) # Approximate version used in C code to avoid overflow: # q = 1 + ((input_frames - 1) // inrate) # output_frames = q * outrate * bytes_per_frame result = bytearray(output_frames * bytes_per_frame) struct_format = _SIGNED_FORMATS[width] input_index = 0 output_index = 0 while True: while d < 0: if input_frames == 0: samps = tuple( (prev_i[chan], cur_i[chan]) for chan in range(0, nchannels) ) # NOTE: It's critical that result is clipped here return result[:output_index], (d, samps) for chan in range(0, nchannels): prev_i[chan] = cur_i[chan] cur_i[chan] = struct.unpack_from(struct_format, fragment, input_index)[ 0 ] input_index += width cur_i[chan] = ((weightA * cur_i[chan]) + (weightB * prev_i[chan])) // ( weightA + weightB ) input_frames -= 1 d += outrate while d >= 0: for chan in range(0, nchannels): sample = int( ( (float(prev_i[chan]) * float(d)) + (float(cur_i[chan]) * (float(outrate) - float(d))) ) / float(outrate) ) struct.pack_into(struct_format, result, output_index, sample) output_index += width d -= inrate return result, None voip-utils-0.3.3/voip_utils/rtp_audio.py000066400000000000000000000164631503065536100204030ustar00rootroot00000000000000"""Utility for converting audio to/from RTP + OPUS packets.""" try: # Use built-in audioop until it's removed in Python 3.13 import audioop # pylint: disable=deprecated-module except ImportError: from . import pyaudioop as audioop # type: ignore[no-redef] import logging import random import struct from collections.abc import Iterable from dataclasses import dataclass from typing import Any import opuslib from .const import OPUS_PAYLOAD_TYPE from .error import RtpError _LOGGER = logging.getLogger(__name__) @dataclass class RtpOpusInput: """Extracts audio from RTP packets with OPUS.""" opus_rate: int = 48000 # Hz opus_width: int = 2 # bytes opus_channels: int = 2 opus_frame_size: int = 960 # samples per channel opus_payload_type: int = OPUS_PAYLOAD_TYPE # set by GrandStream def __post_init__( self, ) -> None: """Initialize encoder and state.""" self._decoder = opuslib.api.decoder.create_state( self.opus_rate, self.opus_channels ) def process_packet( self, rtp_bytes: bytes, rate: int, width: int, channels: int, ) -> bytes: """Extract, decode, and return raw audio from RTP packet.""" if channels not in (1, 2): raise ValueError("Only mono and stereo audio is supported") # Minimum header size if len(rtp_bytes) < 12: raise RtpError("RTP packet is too small") # See: https://en.wikipedia.org/wiki/Real-time_Transport_Protocol#Packet_header flags, payload_type, _sequence_num, _timestamp, _ssrc = struct.unpack( ">BBHLL", rtp_bytes[:12] ) if flags != 0b10000000: raise RtpError("Padding and extension headers not supported") payload_type &= 0x7F # Remove marker bit if payload_type != self.opus_payload_type: raise RtpError( f"Expected payload type {self.opus_payload_type}, got {payload_type}" ) # Assume no padding, extension headers, etc. opus_bytes = rtp_bytes[12:] # Decode into raw audio. # This will always be 48Khz stereo with 16-bit samples. audio_bytes = opuslib.api.decoder.decode( self._decoder, opus_bytes, len(opus_bytes), self.opus_frame_size, False, # no forward error correction (fec) ) # Convert to target sample rate, etc. if channels == 1: # Convert to mono audio_bytes = audioop.tomono( audio_bytes, self.opus_width, 1.0, 1.0, ) if rate != self.opus_rate: # Resample audio_bytes, _state = audioop.ratecv( audio_bytes, self.opus_width, channels, self.opus_rate, rate, None, ) if width != self.opus_width: # Resize audio_bytes = audioop.lin2lin( audio_bytes, self.opus_width, width, ) return audio_bytes @dataclass class RtpOpusOutput: """Prepares audio to send to an RTP client using OPUS.""" opus_rate: int = 48000 # Hz opus_width: int = 2 # bytes opus_channels: int = 2 opus_frame_size: int = 960 # samples per channel opus_payload_type: int = OPUS_PAYLOAD_TYPE # set by GrandStream opus_bytes_per_frame: int = 960 * 2 * 2 # 16-bit x stereo _rtp_flags: int = 0b10000000 # v2, no padding/extensions/CSRCs _rtp_sequence_num: int = 0 _rtp_timestamp: int = 0 _rtp_ssrc: int = 0 _encoder: opuslib.api.encoder.Encoder = None _audio_buffer: bytes = None # type: ignore[assignment] _resample_state: Any = None def __post_init__( self, ) -> None: """Initialize encoder and state.""" self.opus_bytes_per_frame = ( self.opus_frame_size * self.opus_width * self.opus_channels ) # Set up OPUS encoder for VoIP self._encoder = opuslib.api.encoder.create_state( self.opus_rate, self.opus_width, opuslib.APPLICATION_VOIP, ) self.reset() def reset(self): """Clear audio buffer and state.""" self._audio_buffer = b"" self._resample_state = None # Recommended to start from random offsets to aid encryption self._rtp_sequence_num = random.randint(0, 2**10) self._rtp_timestamp = random.randint(1, 2**10) # Change each time self._rtp_ssrc = random.randint(0, 2**32) def process_audio( self, audio_bytes: bytes, rate: int, width: int, channels: int, is_end: bool = False, ) -> Iterable[bytes]: """Process a chunk of raw audio and yield RTP packet(s).""" if rate != self.opus_rate: # Convert to 48Khz audio_bytes, self._resample_state = audioop.ratecv( audio_bytes, width, channels, rate, self.opus_rate, self._resample_state, ) if width != self.opus_width: # Adjust sample width audio_bytes = audioop.lin2lin( audio_bytes, width, self.opus_width, ) if channels != self.opus_channels: # Convert to stereo audio_bytes = audioop.tostereo( audio_bytes, self.opus_width, 1.0, 1.0, ) self._audio_buffer += audio_bytes if is_end: # Pad with silence bytes_missing = len(self._audio_buffer) % self.opus_bytes_per_frame if bytes_missing > 0: self._audio_buffer += bytes(bytes_missing) num_frames = len(self._audio_buffer) // self.opus_bytes_per_frame # Process chunks with *exactly* the desired number of frames for i in range(num_frames): offset = i * self.opus_bytes_per_frame audio_chunk = self._audio_buffer[ offset : offset + self.opus_bytes_per_frame ] # Encode to OPUS packet opus_bytes = opuslib.api.encoder.encode( self._encoder, audio_chunk, self.opus_frame_size, 4000, # recommended in opus docs ) # Add RTP header # See: https://en.wikipedia.org/wiki/Real-time_Transport_Protocol#Packet_header rtp_bytes = struct.pack( ">BBHLL", self._rtp_flags, self.opus_payload_type, self._rtp_sequence_num, self._rtp_timestamp, self._rtp_ssrc, ) # RTP packet yield rtp_bytes + opus_bytes # Next frame self._rtp_sequence_num += 1 self._rtp_timestamp += self.opus_frame_size if num_frames > 0: # Remove audio already sent self._audio_buffer = self._audio_buffer[ num_frames * self.opus_bytes_per_frame : ] if is_end: # Clear audio buffer and state self.reset() voip-utils-0.3.3/voip_utils/sip.py000066400000000000000000000703751503065536100172120ustar00rootroot00000000000000"""Implementation of SIP (Session Initiation Protocol).""" from __future__ import annotations import asyncio import logging import re import time from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Optional from .const import OPUS_PAYLOAD_TYPE from .error import VoipError from .util import is_ipv4_address SIP_PORT = 5060 _LOGGER = logging.getLogger(__name__) _CRLF = "\r\n" VOIP_UTILS_AGENT = "voip-utils" @dataclass class SdpInfo: """Information for Session Description Protocol (SDP).""" username: str id: int session_name: str version: str @dataclass class SipEndpoint: """Information about a SIP endpoint.""" sip_header: str uri: str = field(init=False) scheme: str = field(init=False) host: str = field(init=False) port: int = field(init=False) username: str | None = field(init=False) description: str | None = field(init=False) def __post_init__(self): header_pattern = re.compile( r'\s*((?P\b\w+\b|"[^"]+")\s*)?sips?:[^>]+)>?.*' ) header_match = header_pattern.match(self.sip_header) if header_match is not None: description_token = header_match.group("description") if description_token is not None: self.description = description_token.strip('"') else: self.description = None self.uri = header_match.group("uri") uri_pattern = re.compile( r"(?Psips?):(?:(?P[^@]+)@)?(?P[^:;?]+)(?::(?P\d+))?" ) uri_match = uri_pattern.match(self.uri) if uri_match is None: raise ValueError("Invalid SIP uri") self.scheme = uri_match.group("scheme") self.username = uri_match.group("user") self.host = uri_match.group("host") self.port = ( int(uri_match.group("port")) if uri_match.group("port") else SIP_PORT ) else: raise ValueError("Invalid SIP header") @dataclass class SipMessage: """Data parsed from a SIP message.""" protocol: str method: Optional[str] request_uri: Optional[str] code: Optional[str] reason: Optional[str] headers: dict[str, str] body: str @staticmethod def parse_sip(message: str, header_lowercase: bool = True) -> SipMessage: """Parse a SIP message into a SipMessage object.""" lines = message.splitlines() method: Optional[str] = None request_uri: Optional[str] = None code: Optional[str] = None reason: Optional[str] = None headers: dict[str, str] = {} offset: int = 0 first_line = True # See: https://datatracker.ietf.org/doc/html/rfc3261 for line in lines: if first_line: if line: offset += len(line) + len(_CRLF) line_parts = line.split() if line_parts[0].startswith("SIP"): protocol = line_parts[0] code = line_parts[1] reason = line_parts[2] else: method = line_parts[0] request_uri = line_parts[1] protocol = line_parts[2] first_line = False else: offset += len(_CRLF) elif not line: offset += len(_CRLF) break else: offset += len(line) + len(_CRLF) key, value = line.split(":", maxsplit=1) headers[key.lower() if header_lowercase else key] = value.strip() body = message[offset:] return SipMessage(protocol, method, request_uri, code, reason, headers, body) @dataclass class CallInfo: """Information gathered from an INVITE message.""" caller_endpoint: SipEndpoint local_endpoint: SipEndpoint caller_rtp_port: int server_ip: str headers: dict[str, str] opus_payload_type: int = OPUS_PAYLOAD_TYPE local_rtp_ip: str | None = None local_rtp_port: int | None = None @property def caller_rtcp_port(self) -> int: """Real-time Transport Control Protocol (RTCP) port.""" return self.caller_rtp_port + 1 @property def caller_ip(self) -> str: """Get IP address of caller.""" return self.caller_endpoint.host @property def caller_sip_port(self) -> int: """SIP port of caller.""" return self.caller_endpoint.port @property def local_rtcp_port(self) -> int | None: """Get the local RTCP port.""" return self.local_rtp_port + 1 if self.local_rtp_port is not None else None @dataclass class RtpInfo: """Information about the RTP transport used for the call audio.""" rtp_ip: str | None rtp_port: int | None payload_type: int | None def get_sip_endpoint( host: str, port: Optional[int] = None, scheme: Optional[str] = "sip", username: Optional[str] = None, description: Optional[str] = None, ) -> SipEndpoint: uri = f"{scheme}:" if username: uri += f"{username}@" uri += host if port: uri += f":{port}" if description: uri = f'"{description}" <{uri}>' return SipEndpoint(uri) def get_rtp_info(body: str) -> RtpInfo: body_lines = body.splitlines() rtp_ip = None rtp_port = None opus_payload_type = None opus_payload_types_detected = [] for line in body_lines: line = line.strip() if not line: continue key, _, value = line.partition("=") if key == "m": parts = value.split() if parts[0] == "audio": rtp_port = int(parts[1]) elif key == "c": parts = value.split() if len(parts) > 2: rtp_ip = parts[2] elif key == "a" and value.startswith("rtpmap:"): # a=rtpmap:123 opus/48000/2 codec_str = value.split(":", maxsplit=1)[1] codec_parts = codec_str.split() if (len(codec_parts) > 1) and (codec_parts[1].lower().startswith("opus")): opus_payload_types_detected.append(int(codec_parts[0])) _LOGGER.debug("Detected OPUS payload type as %s", opus_payload_type) if len(opus_payload_types_detected) > 0: opus_payload_type = opus_payload_types_detected[0] _LOGGER.debug("Using first detected payload type: %s", opus_payload_type) else: opus_payload_type = OPUS_PAYLOAD_TYPE _LOGGER.debug("Using default payload type: %s", opus_payload_type) return RtpInfo(rtp_ip=rtp_ip, rtp_port=rtp_port, payload_type=opus_payload_type) def get_header(headers: dict[str, str], name: str) -> tuple[str, str] | None: """Get a header entry using a case insensitive key comparison.""" return next(((k, v) for k, v in headers.items() if k.lower() == name.lower()), None) class SipDatagramProtocol(asyncio.DatagramProtocol, ABC): """UDP server for the Session Initiation Protocol (SIP).""" def __init__(self, sdp_info: SdpInfo) -> None: """Set up SIP server.""" self.sdp_info = sdp_info self.transport = None self._outgoing_calls: dict[str, int] = {} def outgoing_call( self, source: SipEndpoint, destination: SipEndpoint, rtp_port: int ) -> CallInfo: """Make an outgoing call from the given source endpoint to the destination endpoint, using the rtp_port for the local RTP port of the call.""" if self.transport is None: raise RuntimeError("No transport available for outgoing call.") session_id = str(time.monotonic_ns()) session_version = session_id call_id = session_id self._register_outgoing_call(call_id, rtp_port) sdp_lines = [ "v=0", f"o={source.username} {session_id} {session_version} IN IP4 {source.host}", "s=Talk", f"c=IN IP4 {source.host}", "t=0 0", f"m=audio {rtp_port} RTP/AVP 123 96 101 103 104", "a=sendrecv", "a=rtpmap:96 opus/48000/2", "a=fmtp:96 useinbandfec=0", "a=rtpmap:123 opus/48000/2", "a=fmtp:123 maxplaybackrate=16000", "a=rtpmap:101 telephone-event/48000", "a=rtpmap:103 telephone-event/16000", "a=rtpmap:104 telephone-event/8000", "a=ptime:20", "", ] sdp_text = _CRLF.join(sdp_lines) sdp_bytes = sdp_text.encode("utf-8") invite_lines = [ f"INVITE {destination.uri} SIP/2.0", f"Via: SIP/2.0/UDP {source.host}:{source.port}", f"From: {source.sip_header}", f"Contact: {source.sip_header}", f"To: {destination.sip_header}", f"Call-ID: {call_id}", "CSeq: 50 INVITE", f"User-Agent: {VOIP_UTILS_AGENT} 1.0", "Allow: INVITE, ACK, OPTIONS, CANCEL, BYE, SUBSCRIBE, NOTIFY, INFO, REFER, UPDATE", "Accept: application/sdp, application/dtmf-relay", "Content-Type: application/sdp", f"Content-Length: {len(sdp_bytes)}", "", ] invite_text = _CRLF.join(invite_lines) + _CRLF invite_bytes = invite_text.encode("utf-8") msg_bytes = invite_bytes + sdp_bytes _LOGGER.debug(msg_bytes) self.transport.sendto( msg_bytes, (destination.host, destination.port), ) invite_msg = SipMessage.parse_sip(invite_text, False) return CallInfo( caller_endpoint=destination, local_endpoint=source, caller_rtp_port=rtp_port, server_ip=source.host, headers=invite_msg.headers, ) def hang_up(self, call_info: CallInfo): """Hang up the call when finished""" if self.transport is None: raise RuntimeError("No transport available for sending hangup.") call_id = get_header(call_info.headers, "call-id")[1] bye_lines = [ f"BYE {call_info.caller_endpoint.uri} SIP/2.0", f"Via: SIP/2.0/UDP {call_info.local_endpoint.host}:{call_info.local_endpoint.port}", f"From: {call_info.local_endpoint.sip_header}", f"To: {call_info.caller_endpoint.sip_header}", f"Call-ID: {call_id}", "CSeq: 51 BYE", f"User-Agent: {VOIP_UTILS_AGENT} 1.0", "Content-Length: 0", "", ] _LOGGER.debug("Hanging up...") bye_text = _CRLF.join(bye_lines) + _CRLF bye_bytes = bye_text.encode("utf-8") self.transport.sendto( bye_bytes, (call_info.caller_endpoint.host, call_info.caller_endpoint.port) ) self._end_outgoing_call(call_info.headers["call-id"]) self.on_hangup(call_info) def cancel_call(self, call_info: CallInfo): """Cancel an outgoing call while it's still ringing.""" if self.transport is None: raise RuntimeError("No transport available for sending cancel.") required_headers = ("via", "from", "to", "call-id") cancel_headers = [ f"{k}: {v}" for k, v in call_info.headers.items() if k.lower() in required_headers ] cseq_header, cseq_value = get_header(call_info.headers, "cseq") cseq_num = cseq_value.split()[0] cancel_lines = ( [f"CANCEL {call_info.caller_endpoint.uri} SIP/2.0"] + cancel_headers + [ f"{cseq_header}: {cseq_num} CANCEL", f"User-Agent: {VOIP_UTILS_AGENT} 1.0", "Content-Length: 0", "", ] ) _LOGGER.debug("Canceling call...") cancel_text = _CRLF.join(cancel_lines) + _CRLF cancel_bytes = cancel_text.encode("utf-8") self.transport.sendto( cancel_bytes, (call_info.caller_endpoint.host, call_info.caller_endpoint.port), ) self._end_outgoing_call(get_header(call_info.headers, "call-id")[1]) self.on_hangup(call_info) def _register_outgoing_call(self, call_id: str, rtp_port: int): """Register the RTP port associated with an outgoing call.""" self._outgoing_calls[call_id] = rtp_port def _get_call_rtp_port(self, call_id: str) -> int | None: """Get the RTP port associated with an outgoing call.""" return self._outgoing_calls.get(call_id) def _end_outgoing_call(self, call_id: str): """Register the end of an outgoing call.""" self._outgoing_calls.pop(call_id, None) def connection_made(self, transport): """Server ready.""" self.transport = transport def datagram_received(self, data: bytes, addr): """Handle INVITE SIP messages.""" try: if self.transport is None: _LOGGER.warning("No transport for exchanging SIP message") return caller_ip, caller_sip_port = addr message = data.decode("utf-8") smsg = SipMessage.parse_sip(message) _LOGGER.debug( "Received datagram protocol=[%s], method=[%s], ruri=[%s], code=[%s], reason=[%s], headers=[%s], body=[%s]", smsg.protocol, smsg.method, smsg.request_uri, smsg.code, smsg.reason, smsg.headers, smsg.body, ) method = smsg.method if method is not None: method = method.lower() if method == "invite": # An invite message means someone called HA _LOGGER.debug("Received invite message") if not smsg.request_uri: raise ValueError("Empty receiver URI") caller_endpoint = None # The From header should give us the URI used for sending SIP messages to the device if smsg.headers.get("from") is not None: caller_endpoint = SipEndpoint(smsg.headers.get("from", "")) # We can try using the Contact header as a fallback elif smsg.headers.get("contact") is not None: caller_endpoint = SipEndpoint(smsg.headers.get("contact", "")) # If all else fails try to generate a URI based on the IP and port from the address the message came from else: caller_endpoint = get_sip_endpoint(caller_ip, port=caller_sip_port) local_endpoint = None if smsg.headers.get("to") is not None: local_endpoint = SipEndpoint(smsg.headers.get("to", "")) else: local_ip, local_port = self.transport.get_extra_info("sockname") local_endpoint = get_sip_endpoint(local_ip, port=local_port) _LOGGER.debug("Incoming call from endpoint=%s", caller_endpoint) # Extract caller's RTP port from SDP. # See: https://datatracker.ietf.org/doc/html/rfc2327 caller_rtp_port: Optional[int] = None opus_payload_type = OPUS_PAYLOAD_TYPE body_lines = smsg.body.splitlines() for line in body_lines: line = line.strip() if line: key, value = line.split("=", maxsplit=1) if key == "m": parts = value.split() if parts[0] == "audio": caller_rtp_port = int(parts[1]) elif key == "a" and value.startswith("rtpmap:"): # a=rtpmap:123 opus/48000/2 codec_str = value.split(":", maxsplit=1)[1] codec_parts = codec_str.split() if (len(codec_parts) > 1) and ( codec_parts[1].lower().startswith("opus") ): opus_payload_type = int(codec_parts[0]) _LOGGER.debug( "Detected OPUS payload type as %s", opus_payload_type, ) if caller_rtp_port is None: raise VoipError("No caller RTP port") # Extract host from ruri # sip:user@123.123.123.123:1234 re_splituri = re.compile( r"(?P\w+):" # Scheme + r"(?:(?P[\w\.]+):?(?P[\w\.]+)?@)?" # User:Password + r"\[?(?P" # Begin group host + r"(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|" # IPv4 address Host Or + r"(?:(?:[0-9a-fA-F]{1,4}):){7}[0-9a-fA-F]{1,4}|" # IPv6 address Host Or + r"(?:(?:[0-9A-Za-z]+\.)+[0-9A-Za-z]+)" # Hostname string + r")\]?:?" # End group host + r"(?P\d{1,6})?" # port + r"(?:\;(?P[^\?]*))?" # parameters + r"(?:\?(?P.*))?" # headers ) re_uri = re_splituri.search(smsg.request_uri) if re_uri is None: raise ValueError("Receiver URI did not match expected pattern") server_ip = re_uri.group("host") if not is_ipv4_address(server_ip): raise VoipError(f"Invalid IPv4 address in {smsg.request_uri}") self.on_call( CallInfo( caller_endpoint=caller_endpoint, local_endpoint=local_endpoint, caller_rtp_port=caller_rtp_port, server_ip=server_ip, headers=smsg.headers, opus_payload_type=opus_payload_type, ) ) elif method is None: # Reply message means we must have received a response to someone we called # TODO: Verify that the call / sequence IDs match our outgoing INVITE _LOGGER.debug("Received response [%s]", message) is_ok = smsg.code == "200" and smsg.reason == "OK" if smsg.code == "487": # A 487 Request Terminated will be sent in response to a Cancel message _LOGGER.debug("Got 487 Request Terminated") caller_endpoint = None if smsg.headers.get("to") is not None: caller_endpoint = SipEndpoint(smsg.headers.get("to", "")) else: caller_endpoint = get_sip_endpoint( caller_ip, port=caller_sip_port ) cseq_num = get_header(smsg.headers, "cseq")[1].split()[0] ack_lines = [ f"ACK {caller_endpoint.uri} SIP/2.0", f"Via: {smsg.headers['via']}", f"From: {smsg.headers['from']}", f"To: {smsg.headers['to']}", f"Call-ID: {smsg.headers['call-id']}", f"CSeq: {cseq_num} ACK", f"User-Agent: {VOIP_UTILS_AGENT} 1.0", "Content-Length: 0", ] ack_text = _CRLF.join(ack_lines) + _CRLF ack_bytes = ack_text.encode("utf-8") self.transport.sendto(ack_bytes, (caller_ip, caller_sip_port)) return if not is_ok: _LOGGER.debug("Received non-OK response [%s]", message) return _LOGGER.debug("Got OK message") if not self._is_response_type(smsg, "invite"): # This will happen if/when we hang up. _LOGGER.debug("Got response for non-invite message") return _LOGGER.debug("Got invite response") rtp_info = get_rtp_info(smsg.body) remote_rtp_ip = rtp_info.rtp_ip remote_rtp_port = rtp_info.rtp_port opus_payload_type = rtp_info.payload_type caller_endpoint = None if smsg.headers.get("to") is not None: caller_endpoint = SipEndpoint(smsg.headers.get("to", "")) else: caller_endpoint = get_sip_endpoint(caller_ip, port=caller_sip_port) # The From header should give us the URI used for sending SIP messages to the device local_endpoint = None if smsg.headers.get("from") is not None: local_endpoint = SipEndpoint(smsg.headers.get("from", "")) else: local_endpoint = get_sip_endpoint(caller_ip, port=caller_sip_port) _LOGGER.debug("Outgoing call to endpoint=%s", caller_endpoint) ack_lines = [ f"ACK {caller_endpoint.uri} SIP/2.0", f"Via: SIP/2.0/UDP {local_endpoint.host}:{local_endpoint.port}", f"From: {local_endpoint.sip_header}", f"To: {smsg.headers['to']}", f"Call-ID: {smsg.headers['call-id']}", "CSeq: 50 ACK", f"User-Agent: {VOIP_UTILS_AGENT} 1.0", "Content-Length: 0", ] ack_text = _CRLF.join(ack_lines) + _CRLF ack_bytes = ack_text.encode("utf-8") self.transport.sendto(ack_bytes, (caller_ip, caller_sip_port)) # The call been answered, proceed with desired action here local_rtp_port = self._get_call_rtp_port(smsg.headers["call-id"]) self.on_call( CallInfo( caller_endpoint=caller_endpoint, local_endpoint=local_endpoint, caller_rtp_port=remote_rtp_port, server_ip=remote_rtp_ip, headers=smsg.headers, opus_payload_type=opus_payload_type, # Should probably update this to eventually support more codecs local_rtp_ip=local_endpoint.host, local_rtp_port=local_rtp_port, ) ) elif method == "bye": # Acknowlege the BYE message when the remote party hangs up _LOGGER.debug("Received BYE message: %s", message) if self.transport is None: _LOGGER.debug("Skipping message: %s", message) return # The From header should give us the URI used for sending SIP messages to the device if smsg.headers.get("from") is not None: caller_endpoint = SipEndpoint(smsg.headers.get("from", "")) # We can try using the Contact header as a fallback elif smsg.headers.get("contact") is not None: caller_endpoint = SipEndpoint(smsg.headers.get("contact", "")) # If all else fails try to generate a URI based on the IP and port from the address the message came from else: caller_endpoint = get_sip_endpoint(caller_ip, port=caller_sip_port) local_endpoint = None if smsg.headers.get("to") is not None: local_endpoint = SipEndpoint(smsg.headers.get("to", "")) else: local_ip, local_port = self.transport.get_extra_info("sockname") local_endpoint = get_sip_endpoint(local_ip, port=local_port) _LOGGER.debug("Incoming BYE from endpoint=%s", caller_endpoint) # Acknowledge the BYE message, otherwise the phone will keep sending it rtp_info = get_rtp_info(smsg.body) remote_rtp_ip = rtp_info.rtp_ip remote_rtp_port = rtp_info.rtp_port opus_payload_type = rtp_info.payload_type # We should remove the call from the outgoing calls dict now if it is there self._end_outgoing_call(smsg.headers["call-id"]) ok_lines = [ "SIP/2.0 200 OK", f"Via: {smsg.headers['via']}", f"From: {smsg.headers['from']}", f"To: {smsg.headers['to']}", f"Call-ID: {smsg.headers['call-id']}", f"CSeq: {smsg.headers['cseq']}", f"User-Agent: {VOIP_UTILS_AGENT} 1.0", "Content-Length: 0", ] ok_text = _CRLF.join(ok_lines) + _CRLF ok_bytes = ok_text.encode("utf-8") # We should probably tell the associated RTP server to shutdown at this point, assuming we aren't reusing it for other calls _LOGGER.debug("Sending OK for BYE message: %s", ok_text) self.transport.sendto( ok_bytes, (caller_ip, caller_sip_port), ) # The transport might be used for incoming calls # as well, so we should leave it open. # Cleanup any necessary call state self.on_hangup( CallInfo( caller_endpoint=caller_endpoint, local_endpoint=local_endpoint, caller_rtp_port=remote_rtp_port, server_ip=remote_rtp_ip, headers=smsg.headers, ) ) except Exception: _LOGGER.exception("Unexpected error handling SIP message") @abstractmethod def on_call(self, call_info: CallInfo): """Handle incoming calls.""" def on_hangup(self, call_info: CallInfo): """Handle the end of a call.""" def _is_response_type(self, msg: SipMessage, resp_type: str) -> bool: """Return whether or not the response message is for the given type.""" return ( msg is not None and "cseq" in msg.headers and resp_type.lower() in msg.headers["cseq"].lower() ) def answer( self, call_info: CallInfo, server_rtp_port: int, ): """Send OK message to caller with our IP and RTP port.""" if self.transport is None: return # SDP = Session Description Protocol # See: https://datatracker.ietf.org/doc/html/rfc2327 body_lines = [ "v=0", f"o={self.sdp_info.username} {self.sdp_info.id} 1 IN IP4 {call_info.server_ip}", f"s={self.sdp_info.session_name}", f"c=IN IP4 {call_info.server_ip}", "t=0 0", f"m=audio {server_rtp_port} RTP/AVP {call_info.opus_payload_type}", f"a=rtpmap:{call_info.opus_payload_type} opus/48000/2", "a=ptime:20", "a=maxptime:150", "a=sendrecv", _CRLF, ] body = _CRLF.join(body_lines) response_headers = { "Via": call_info.headers["via"], "From": call_info.headers["from"], "To": call_info.headers["to"], "Call-ID": call_info.headers["call-id"], "Content-Type": "application/sdp", "Content-Length": len(body), "CSeq": call_info.headers["cseq"], "Contact": call_info.headers["contact"], "User-Agent": f"{self.sdp_info.username} {self.sdp_info.id} {self.sdp_info.version}", "Allow": "INVITE, ACK, BYE, CANCEL, OPTIONS", } response_lines = ["SIP/2.0 200 OK"] for key, value in response_headers.items(): response_lines.append(f"{key}: {value}") response_lines.append(_CRLF) response_str = _CRLF.join(response_lines) + body response_bytes = response_str.encode() self.transport.sendto( response_bytes, (call_info.caller_ip, call_info.caller_sip_port), ) _LOGGER.debug( "Sent OK to ip=%s, port=%s with rtp_port=%s", call_info.caller_ip, call_info.caller_sip_port, server_rtp_port, ) voip-utils-0.3.3/voip_utils/util.py000066400000000000000000000004011503065536100173530ustar00rootroot00000000000000"""Utilities for VoIP""" from ipaddress import IPv4Address def is_ipv4_address(address: str) -> bool: """Check if a given string is an IPv4 address.""" try: IPv4Address(address) except ValueError: return False return True voip-utils-0.3.3/voip_utils/voip.py000066400000000000000000000233731503065536100173700ustar00rootroot00000000000000"""Voice over IP (VoIP) implementation.""" import asyncio import logging import socket import struct import time from abc import ABC, abstractmethod from dataclasses import dataclass from functools import partial from typing import Any, Callable, Optional, Set from .const import OPUS_PAYLOAD_TYPE from .rtp_audio import RtpOpusInput, RtpOpusOutput from .sip import CallInfo, SdpInfo, SipDatagramProtocol _LOGGER = logging.getLogger(__name__) _RTCP_BYE = 203 @dataclass class RtcpState: """State of a call according to RTCP packets received.""" bye_callback: Optional[Callable[[], None]] = None CallProtocolFactory = Callable[[CallInfo, RtcpState], asyncio.Protocol] class VoipDatagramProtocol(SipDatagramProtocol): """UDP server for Voice over IP (VoIP).""" def __init__( self, sdp_info: SdpInfo, valid_protocol_factory: CallProtocolFactory, invalid_protocol_factory: Optional[CallProtocolFactory] = None, ) -> None: """Set up VoIP call handler.""" super().__init__(sdp_info) self.valid_protocol_factory = valid_protocol_factory self.invalid_protocol_factory = invalid_protocol_factory self._tasks: Set[asyncio.Future[Any]] = set() def is_valid_call(self, call_info: CallInfo) -> bool: """Filter calls.""" return True def on_call(self, call_info: CallInfo): """Answer incoming calls and start RTP server on a random port.""" protocol_factory = ( self.valid_protocol_factory if self.is_valid_call(call_info) else self.invalid_protocol_factory ) if protocol_factory is None: _LOGGER.debug("Call rejected: %s", call_info) return rtp_ip = "" if call_info.local_rtp_port is None: # Find free RTP/RTCP ports rtp_port = 0 while True: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setblocking(False) # Bind to a random UDP port sock.bind(("", 0)) rtp_ip, rtp_port = sock.getsockname() # Close socket to free port for re-use sock.close() # Check that the next port up is available for RTCP sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: sock.bind(("", rtp_port + 1)) # Will be opened again below sock.close() # Found our ports break except OSError: # RTCP port is taken pass else: rtp_ip = call_info.local_rtp_ip if call_info.local_rtp_ip else "" rtp_port = call_info.local_rtp_port _LOGGER.debug( "Starting RTP server on ip=%s, rtp_port=%s, rtcp_port=%s", rtp_ip, rtp_port, rtp_port + 1, ) # Handle RTP packets in RTP server rtp_task = asyncio.create_task( self._create_rtp_server(protocol_factory, call_info, rtp_ip, rtp_port) ) self._tasks.add(rtp_task) rtp_task.add_done_callback(self._tasks.remove) # Tell caller to start sending/receiving RTP audio self.answer(call_info, rtp_port) async def _create_rtp_server( self, protocol_factory: CallProtocolFactory, call_info: CallInfo, rtp_ip: str, rtp_port: int, ): # Shared state between RTP/RTCP servers rtcp_state = RtcpState() loop = asyncio.get_running_loop() # RTCP server await loop.create_datagram_endpoint( lambda: RtcpDatagramProtocol(rtcp_state), (rtp_ip, rtp_port + 1), ) # RTP server await loop.create_datagram_endpoint( partial(protocol_factory, call_info, rtcp_state), (rtp_ip, rtp_port), ) class RtpDatagramProtocol(asyncio.DatagramProtocol, ABC): """Handle RTP audio input/output for a VoIP call.""" def __init__( self, rate: int = 16000, width: int = 2, channels: int = 1, opus_payload_type: int = OPUS_PAYLOAD_TYPE, rtcp_state: Optional[RtcpState] = None, ) -> None: """Set up RTP server.""" self.rtcp_state = rtcp_state if self.rtcp_state is not None: # Automatically disconnect when BYE is received over RTCP self.rtcp_state.bye_callback = self.disconnect # Desired format for input audio self.rate = rate self.width = width self.channels = channels self.transport = None self.addr = None self._audio_queue: "asyncio.Queue[bytes]" = asyncio.Queue() self._rtp_input = RtpOpusInput(opus_payload_type=opus_payload_type) self._rtp_output = RtpOpusOutput(opus_payload_type=opus_payload_type) self._is_connected: bool = False def disconnect(self): self._is_connected = False if self.transport is not None: self.transport.close() self.transport = None def connection_made(self, transport): """Server is ready.""" self.transport = transport self._is_connected = True def datagram_received(self, data, addr): """Decode RTP + OPUS into raw audio.""" if not self._is_connected: return self.addr = addr try: # STT expects 16Khz mono with 16-bit samples audio_bytes = self._rtp_input.process_packet( data, self.rate, self.width, self.channels, ) self.on_chunk(audio_bytes) except Exception as err: self.disconnect() raise err @abstractmethod def on_chunk(self, audio_bytes: bytes) -> None: """Handle raw audio chunk.""" def send_audio( self, audio_bytes: bytes, rate: int, width: int, channels: int, addr: Any = None, sleep_ratio: float = 1.0, silence_before: float = 0.0, ) -> None: """Send audio from WAV file in chunks over RTP.""" if not self._is_connected: _LOGGER.debug("Not connected, can't send audio") return addr = addr or self.addr if addr is None: _LOGGER.debug("No destination address, can't send audio") raise ValueError("Destination address not set") bytes_per_sample = width * channels bytes_per_frame = self._rtp_output.opus_frame_size * bytes_per_sample # Generate all RTP packets up front sample_offset = 0 samples_left = len(audio_bytes) // bytes_per_sample rtp_packets: list[bytes] = [] while samples_left > 0: _LOGGER.debug("Preparing audio chunk to send") bytes_offset = sample_offset * bytes_per_sample chunk = audio_bytes[bytes_offset : bytes_offset + bytes_per_frame] samples_in_chunk = len(chunk) // bytes_per_sample samples_left -= samples_in_chunk for rtp_bytes in self._rtp_output.process_audio( chunk, rate, width, channels, is_end=samples_left <= 0, ): rtp_packets.append(rtp_bytes) sample_offset += samples_in_chunk # Pause before sending to allow time for user to pick up phone. _LOGGER.debug("Pause before sending") time.sleep(silence_before) # Send RTP in a steady stream, delaying between each packet to simulate real-time audio seconds_per_rtp = self._rtp_output.opus_frame_size / self._rtp_output.opus_rate for rtp_bytes in rtp_packets: if not self._is_connected: break if self.transport is not None: self.transport.sendto(rtp_bytes, addr) # Wait almost the full amount of time for the chunk. # # Sending too fast will cause the phone to skip chunks, # since it doesn't seem to have a very large buffer. # # Sending too slow will cause audio artifacts if there is # network jitter, which is why programs like GStreamer are # much better at this. time.sleep(seconds_per_rtp * sleep_ratio) class RtcpDatagramProtocol(asyncio.DatagramProtocol, ABC): """UDP server for the Real-time Transport Control Protocol (RTCP).""" def __init__(self, state: RtcpState) -> None: """Set up RTCP server.""" self.transport = None self.state = state self._is_connected = False def connection_made(self, transport): """Server ready.""" self.transport = transport self._is_connected = True def disconnect(self): self._is_connected = False if self.transport is not None: self.transport.close() self.transport = None def datagram_received(self, data: bytes, addr): """Handle INVITE SIP messages.""" if not self._is_connected: return try: if len(data) < 8: raise ValueError("RTCP packet is too small") # See: https://en.wikipedia.org/wiki/RTP_Control_Protocol#Packet_header _flags, packet_type, _packet_length, _ssrc = struct.unpack( ">BBHL", data[:8] ) if packet_type == _RTCP_BYE: _LOGGER.debug("Received BYE message via RTCP from %s", addr) self.disconnect() if self.state.bye_callback is not None: self.state.bye_callback() except Exception: _LOGGER.exception("Unexpected error handling RTCP packet")