From 1a580d1c1e86e1208397a7d9b06da94f893e0876 Mon Sep 17 00:00:00 2001 From: Duo Ho Date: Wed, 22 Mar 2023 09:20:17 +0000 Subject: [PATCH] Add G722 decoder with pure python implementation --- bumble/decoder.py | 428 +++++++++++++++++++++++++++++++++++++++++ tests/decoder_test.py | 47 +++++ tests/g722_sample.g722 | 1 + 3 files changed, 476 insertions(+) create mode 100644 bumble/decoder.py create mode 100644 tests/decoder_test.py create mode 100644 tests/g722_sample.g722 diff --git a/bumble/decoder.py b/bumble/decoder.py new file mode 100644 index 0000000..d466c80 --- /dev/null +++ b/bumble/decoder.py @@ -0,0 +1,428 @@ +# Copyright 2023 Google LLC +# +# 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 +# +# https://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. + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +# fmt: off + +WL = [-60, -30, 58, 172, 334, 538, 1198, 3042] +RL42 = [0, 7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1, 0] +ILB = [ + 2048, + 2093, + 2139, + 2186, + 2233, + 2282, + 2332, + 2383, + 2435, + 2489, + 2543, + 2599, + 2656, + 2714, + 2774, + 2834, + 2896, + 2960, + 3025, + 3091, + 3158, + 3228, + 3298, + 3371, + 3444, + 3520, + 3597, + 3676, + 3756, + 3838, + 3922, + 4008, +] +WH = [0, -214, 798] +RH2 = [2, 1, 2, 1] +# Values in QM2/QM4/QM6 left shift three bits than original g722 specification. +QM2 = [-7408, -1616, 7408, 1616] +QM4 = [ + 0, + -20456, + -12896, + -8968, + -6288, + -4240, + -2584, + -1200, + 20456, + 12896, + 8968, + 6288, + 4240, + 2584, + 1200, + 0, +] +QM6 = [ + -136, + -136, + -136, + -136, + -24808, + -21904, + -19008, + -16704, + -14984, + -13512, + -12280, + -11192, + -10232, + -9360, + -8576, + -7856, + -7192, + -6576, + -6000, + -5456, + -4944, + -4464, + -4008, + -3576, + -3168, + -2776, + -2400, + -2032, + -1688, + -1360, + -1040, + -728, + 24808, + 21904, + 19008, + 16704, + 14984, + 13512, + 12280, + 11192, + 10232, + 9360, + 8576, + 7856, + 7192, + 6576, + 6000, + 5456, + 4944, + 4464, + 4008, + 3576, + 3168, + 2776, + 2400, + 2032, + 1688, + 1360, + 1040, + 728, + 432, + 136, + -432, + -136, +] +QMF_COEFFS = [3, -11, 12, 32, -210, 951, 3876, -805, 362, -156, 53, -11] + +# fmt: on + + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- +class G722Decoder(object): + """G.722 decoder with bitrate 64kbit/s. + + For the Blocks in the sub-band decoders, please refer to the G.722 + specification for the required information. G722 specification: + https://www.itu.int/rec/T-REC-G.722-201209-I + """ + + def __init__(self): + self._x = [0] * 24 + self._band = [Band(), Band()] + # The initial value in BLOCK 3L + self._band[0].det = 32 + # The initial value in BLOCK 3H + self._band[1].det = 8 + + def decode_frame(self, encoded_data) -> bytearray: + result_array = bytearray(len(encoded_data) * 4) + self.g722_decode(result_array, encoded_data) + return result_array + + def g722_decode(self, result_array, encoded_data) -> int: + """Decode the data frame using g722 decoder.""" + rlow = 0 + rhigh = 0 + xout1 = 0 + xout2 = 0 + code = 0 + result_length = 0 + j = 0 + + for j in range(0, len(encoded_data)): + code = encoded_data[j] + higher_bits = (code >> 6) & 0x03 + lower_bits = code & 0x3F + + rlow = self.lower_sub_band_decoder(lower_bits) + rhigh = self.higher_sub_band_decoder(higher_bits) + + # Apply the receive QMF + for i in range(22): + self._x[i] = self._x[i + 2] + + self._x[22] = rlow + rhigh + self._x[23] = rlow - rhigh + xout1 = 0 + xout2 = 0 + + for i in range(12): + xout2 += self._x[2 * i] * QMF_COEFFS[i] + xout1 += self._x[2 * i + 1] * QMF_COEFFS[11 - i] + + result_length = self.update_decoded_result( + xout1, result_length, result_array + ) + result_length = self.update_decoded_result( + xout2, result_length, result_array + ) + + return result_length + + def update_decoded_result(self, xout, byte_length, byte_array) -> int: + result = (int)(xout >> 11) + bytes_result = result.to_bytes(2, 'little', signed=True) + byte_array[byte_length] = bytes_result[0] + byte_array[byte_length + 1] = bytes_result[1] + return byte_length + 2 + + def lower_sub_band_decoder(self, lower_bits) -> int: + """Lower sub-band decoder for last six bits.""" + + # Block 5L + # INVQBL + wd1 = lower_bits + wd2 = QM6[wd1] + wd1 >>= 2 + wd2 = (self._band[0].det * wd2) >> 15 + # RECONS + rlow = self._band[0].s + wd2 + + # Block 6L + # LIMIT + if rlow > 16383: + rlow = 16383 + elif rlow < -16384: + rlow = -16384 + + # Block 2L + # INVQAL + wd2 = QM4[wd1] + dlowt = (self._band[0].det * wd2) >> 15 + + # Block 3L + # LOGSCL + wd2 = RL42[wd1] + wd1 = (self._band[0].nb * 127) >> 7 + wd1 += WL[wd2] + + if wd1 < 0: + wd1 = 0 + elif wd1 > 18432: + wd1 = 18432 + + self._band[0].nb = wd1 + + # SCALEL + wd1 = (self._band[0].nb >> 6) & 31 + wd2 = 8 - (self._band[0].nb >> 11) + + if wd2 < 0: + wd3 = ILB[wd1] << -wd2 + else: + wd3 = ILB[wd1] >> wd2 + + self._band[0].det = wd3 << 2 + + # Block 4L + self._band[0].block4(dlowt) + + return rlow + + def higher_sub_band_decoder(self, higher_bits) -> int: + """Higher sub-band decoder for first two bits.""" + + # Block 2H + # INVQAH + wd2 = QM2[higher_bits] + dhigh = (self._band[1].det * wd2) >> 15 + + # Block 5H + # RECONS + rhigh = dhigh + self._band[1].s + + # Block 6H + # LIMIT + if rhigh > 16383: + rhigh = 16383 + elif rhigh < -16384: + rhigh = -16384 + + # Block 3H + # LOGSCH + wd2 = RH2[higher_bits] + wd1 = (self._band[1].nb * 127) >> 7 + wd1 += WH[wd2] + + if wd1 < 0: + wd1 = 0 + elif wd1 > 22528: + wd1 = 22528 + self._band[1].nb = wd1 + + # SCALEH + wd1 = (self._band[1].nb >> 6) & 31 + wd2 = 10 - (self._band[1].nb >> 11) + + if wd2 < 0: + wd3 = ILB[wd1] << -wd2 + else: + wd3 = ILB[wd1] >> wd2 + self._band[1].det = wd3 << 2 + + # Block 4H + self._band[1].block4(dhigh) + + return rhigh + + +# ----------------------------------------------------------------------------- +class Band(object): + """Structure for G722 decode proccessing.""" + + s: int = 0 + nb: int = 0 + det: int = 0 + + def __init__(self): + self._sp = 0 + self._sz = 0 + self._r = [0] * 3 + self._a = [0] * 3 + self._ap = [0] * 3 + self._p = [0] * 3 + self._d = [0] * 7 + self._b = [0] * 7 + self._bp = [0] * 7 + self._sg = [0] * 7 + + def saturate(self, amp: int) -> int: + if amp > 32767: + return 32767 + elif amp < -32768: + return -32768 + else: + return amp + + def block4(self, d: int) -> None: + """Block4 for both lower and higher sub-band decoder.""" + wd1 = 0 + wd2 = 0 + wd3 = 0 + + # RECONS + self._d[0] = d + self._r[0] = self.saturate(self.s + d) + + # PARREC + self._p[0] = self.saturate(self._sz + d) + + # UPPOL2 + for i in range(3): + self._sg[i] = (self._p[i]) >> 15 + wd1 = self.saturate((self._a[1]) << 2) + wd2 = -wd1 if self._sg[0] == self._sg[1] else wd1 + + if wd2 > 32767: + wd2 = 32767 + + wd3 = 128 if self._sg[0] == self._sg[2] else -128 + wd3 += wd2 >> 7 + wd3 += (self._a[2] * 32512) >> 15 + + if wd3 > 12288: + wd3 = 12288 + elif wd3 < -12288: + wd3 = -12288 + self._ap[2] = wd3 + + # UPPOL1 + self._sg[0] = (self._p[0]) >> 15 + self._sg[1] = (self._p[1]) >> 15 + wd1 = 192 if self._sg[0] == self._sg[1] else -192 + wd2 = (self._a[1] * 32640) >> 15 + + self._ap[1] = self.saturate(wd1 + wd2) + wd3 = self.saturate(15360 - self._ap[2]) + + if self._ap[1] > wd3: + self._ap[1] = wd3 + elif self._ap[1] < -wd3: + self._ap[1] = -wd3 + + # UPZERO + wd1 = 0 if d == 0 else 128 + self._sg[0] = d >> 15 + for i in range(1, 7): + self._sg[i] = (self._d[i]) >> 15 + wd2 = wd1 if self._sg[i] == self._sg[0] else -wd1 + wd3 = (self._b[i] * 32640) >> 15 + self._bp[i] = self.saturate(wd2 + wd3) + + # DELAYA + for i in range(6, 0, -1): + self._d[i] = self._d[i - 1] + self._b[i] = self._bp[i] + + for i in range(2, 0, -1): + self._r[i] = self._r[i - 1] + self._p[i] = self._p[i - 1] + self._a[i] = self._ap[i] + + # FILTEP + self._sp = 0 + for i in range(1, 3): + wd1 = self.saturate(self._r[i] + self._r[i]) + self._sp += (self._a[i] * wd1) >> 15 + self._sp = self.saturate(self._sp) + + # FILTEZ + self._sz = 0 + for i in range(6, 0, -1): + wd1 = self.saturate(self._d[i] + self._d[i]) + self._sz += (self._b[i] * wd1) >> 15 + self._sz = self.saturate(self._sz) + + # PREDIC + self.s = self.saturate(self._sp + self._sz) diff --git a/tests/decoder_test.py b/tests/decoder_test.py new file mode 100644 index 0000000..3be2c63 --- /dev/null +++ b/tests/decoder_test.py @@ -0,0 +1,47 @@ +# Copyright 2023 Google LLC +# +# 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 +# +# https://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. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import hashlib +import os +from bumble.decoder import G722Decoder + + +# ----------------------------------------------------------------------------- +def test_decode_file(): + decoder = G722Decoder() + output_bytes = bytearray() + + with open( + os.path.join(os.path.dirname(__file__), 'g722_sample.g722'), 'rb' + ) as file: + file_content = file.read() + frame_length = 80 + data_length = int(len(file_content) / frame_length) + + for i in range(0, data_length): + decoded_data = decoder.decode_frame( + file_content[i * frame_length : i * frame_length + frame_length] + ) + output_bytes.extend(decoded_data) + + result = hashlib.md5(output_bytes).hexdigest() + assert result == 'b58e0cdd012d12f5633fc796c3b0fbd4' + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + test_decode_file() diff --git a/tests/g722_sample.g722 b/tests/g722_sample.g722 new file mode 100644 index 0000000..432cd6c --- /dev/null +++ b/tests/g722_sample.g722 @@ -0,0 +1 @@ + , /1"9)1 ,/Y_~|~x|Y^VZX|_Wx:Tpz۳{}^~zo}n}n?x]oZ_:08Λ\upN߽VzY\w]qS{:Y\zYޗx^~~~s.jw]xyuoxqz0{6ny:{X^YkTx^~V_ڙޮO0|[0\2sW[vuuxp;l|pܻZ>>XՎ_YUUR>ޖpRxZ_tu2.Ymr~^_^uPqox޴=mzx}޵Z}Zk8Zivvuu_sX~ۓUyvw{s_xrpx{ty?YjY޳XtL^Ԑss[.n5ם־ zQߚT|syqmust^w޴;s[zX{޴RmW_z\rUtp^s\Y]~~su{/Yo؟w_Utzq|v^8u5;UZ]~TX5[kt{{vk,otp]^s}WU}Yx[tzߺ_oo]4v_vv|}+s?{zwVz}Q{t}sr}uTxu[ڻ[{\S=s9whtw2Wrytqts\\yMPݟӜ[_2|[][s_ر9]o[q^.|yyx5zYw˵[vϞֹsTz4row6tyso}ܝ>ySwW/][zY?TVZsYwx\~_ې}ܘ{Xg\_xq/][n~~x]rm\jpmzl}ZW9]^{ܞ{Qtל}vqwY6/q߼[.}[PҲ__83ۼ~1_[[voxn?NZМT֗Swww}^tyw|Xxi?lsu_8w՜\RVWUZZ]u]Y\ОYZVu׾?\s9{ϵ]ow~t|zjnzݞ2qooW__(w_snzڿX}ݺy]mُ oys|rtR_y۱ux_ySzui_z|ږ6u]yr}jYW]y}].6UWp'wp{uZ}X_ҟ[P[ x^R\ؗ<[~XUݴQ6NpwYo[t7qܝ{rpn^غozT^rU-jwu]oXY>\pq\ޟ|2x~o{}zݳk{|k2\ZrֶV{vsvݾY_XVWWrSS\UיҰ]\Smw7_T~}U~}rrzstu?plpv|=nx2rk|p3n^ry|s:z7u]6xVۗUy^X؛XwSٱz|6_WTW9ݸiu-sUwX>vZ_Xؒ_T/zzsTܼYRݖ^_ޙ\U.].Yy[--kx1q}xwwprY?[7}T{Z:oTRY}]Wx.^{Olt>|{yֿvxlWwoXlۺwY}uxQs6mXt}}STY8}SRRXUVzP\>xZ|T}R[xZZX]0똱5yyr?x79[zުnn\^s}w6Y_WZ]^8ywQsVْq[=TSԘu{\U׸МYoYY^Xs|]TTy|\q]r/]3qlݷuw_:xt]\{r z ڐwZ~[R1TtXR־[7SmuYuuvv43vuֶ_rܼpqݭjrӽZW xXX]ZJ^}S[~T|XxtZX\wq_3^xZ]Z[xu[T+]U}XZyui\W~Z*X~\^7:]XXv}].^ZL_qm:qpアmsx2_xwxvnvVVuKt:_q]٘SJ=v::pzsN]yVpxwv[v^|Ҙ˜]tym<}pnXVߜW7y9+zWsXSӜ~w6v޻]5jzɜ}xUz_9[SMS<՛,y:}0zwR39\_|uuVuyؓVlxPݐoR[\R;OUZ>|zr^\[_]VYXPV[s{rt/ݐ^V3[U~Xru۲{YV}Ֆ{vW}<\љ4XZP}Zxvr_utڽz2z[Z|yq~5W<__7M_m{T۸;ws|qظv]tuؐM~|rZX8TP[srv^6:Z~w7]ZSWsqnYמvS|tXLVmsx{tWP|QZԛԚ]vׯupѸY_^vn}uv|1z^sXSZz{0v1~ޞqyqw<|{ryW\w{TSR_vnpqi_vS6}VXyҚv|uwrW_\z֛>qT_r:v^|t}0uw]8yRYνwotzr6u9UQ](Vn/Z}__W=Ҙpyxf|]xWSz]Nz[v|nuUQTܙ~RX}5mys;\ܾVsu{\uolr:ZݛV~T^S}\Vluot{Wx~ڻߟ{Vn-Yp{xW2t\yxmXx]9q-Zu8^xnvW|6Tro_=mZ]vzkԹڶ4WmzY}\]~uX6tw&ݱq<9pp:n~tY9]s|U:\^xTۛY{z}V)2l~{Nݼw1_p|l5V|p3*v=r׮pmZRU8Z۹z[>~V}y/SVЙ7\}_Y\xwx^.ҫv_j18]wz.sqZl+5[2uqٛkڶ}rW؟n{:׹=~gZmvNXZ8pr^tث\fݿo\uv۴{kޜ(h}t~MUyYg}R[җ^93:-~kQxN}y{pZ9)=euyQr<|1r(RיГ~Xv~wؖWtSQR^:sWw:xc[|>Xwlvxxk}0YZپ}<7u~~:/u/vUZ\Y<6u.Pؾ{w>4nէsR~lu~2o}~-W\\Q;{sz]vd2kZ\җ{o3u_]8|~^ЯWLQ޼vqrzpv<QxՕ]vy<||~t^3W֚y>ruݙzv6fXt4xtqtz~qp֤/{uۚZ\YZs>Y0|1M|RٔPV^twsorh{SXwv5nu|67pߔVڛz/x^t{Wpw>LU=Z2z3xt{X؏yj&sxwWWR_n4uySVT}ܛ\\75sv?rqyпYUx~}rvuW7toۙ^_W__K<з7y5pz_~X8|VZW6n-0}|PsU~xlz\w^{X\v~n|]k^y_YTz]qor߿z]\9r0wxy{6\,v]3L{pӸW:R[7uZ=U~]=WntZf?wwXX;U֏[ܗ>Tuk|:ޜ\Tv^VvҞixqu|~OT{q][pY~\[ Xfr4/n[x{uKm.ߞ\pqssڽw[WXW~_un}46Z|O{}nXNVqtwvtؖ4xo_\YWxvWm{\{s6Yt2vnt[ZnO{Vܟq_w9,mTVqsڹ0/m]uzؔxSYvn0q:}{WxUy]+UW~]ry{7XSmj~T^}x|norwּ7]gP_֜\z|sq>qn9V|-~=Zvr}XؙߙlYҟvZtVڼliUיչ6wo_vw7w_sս\zq:pxui\Rn_z\W~\sSVT9jz/?}\]o~ѿ9~[y\T޾i_νZV0oYUp3+s.Uv/zw{u־ڻjv_TY~zU[^j=Z=-N=MO_[xrt|-ݟvu}Zؘxxm9psy<]pSV[\{޵up1yZ9e\XZr^6;RzmחuwnX6u>s6Unpz[wܒyrxkܰwWn:UVִyvuXXpz?RUuvY8kWZsxКzl3{z/~vUXQVqR0Wn{^z.Z[ROݒ.]|mrw]5poSԞvTtp]0ӿQ^52z:Vvou_TZws-]q]4yYRqp-__ѽtvXړXp{p[4t3W[[Rٚ_^^qrtXZtthot{6\]~_Toy:~usTVYnq|w~z8PПP\]VY}ګx]tX\2vy7^uVswx_ݲi\^8NR|lrxuܚo6VqW>[ѓۖq}ujnt[}q>O=WMYҔz=qtwZ\~XrvXT|9;v־Y\{w\mU[yJ{~-.\~~\~=sR[4zr;SZ}3xXі[YYvrtw]URtוS{~m9֖6xwp]tkߪ8{\~Zxۍuo_ny]XvZtTt޷yW>^yq{vn\}\VQ\x1*s{]5T_]n7v\[^Z~_ښ\m|_xҾ\_tϱU|_ov:2-{PuPW߲Uxp-}ZQX:jytzڝQrZ|[Y.mz~pTLRW{[4wvZWYOSz{2sp,VwrXvuZ_Z2qqotXճZ[\^t7\yrӱQ|YOSy4sUsR5^x:|u6{uv̕Wx[owsvTVxxtVO8X.w|=|{TYuY_WV6޴_p\pTOR78:j^|U^XO|4yjvӞozWw\]4l7]{ڏRqY]r4XmUW^zwxutoXTun/z=qwom|_ڷXrUyq1^RuMv}\Z~r6뻼|vZ<^v0R^pj{_y{\qSkywSkz;7{ԚwSVTXqr\:6VvL^]?kX[p\?M;ztvX_Wrxk}zVYmX|t;1S\txyL{Pu[5\}7}_X|l~]K[^ݽ0ry_vZsvSz1{uvvZ]\_X~w/4_VyYwPYYm4س8^=S]XXu[97zuVU̐ݙozxwXo[׿P{Uwy:^yu}m[TWי|tnq]-xzMSpV}p/u|]+x[ySvo2uwt|ַܑ|4W=tV~4Trqu߱]Qo{֕վou;_~[7]UV]n^Zz,~Qtty{\TXܟښRZ_tqtv+].tp6z)y^]knU<[r~_ZϸwZq^pٲ1q_wu{\^XظQ^zq]yQy^[wvx׸^.\qwsu07Txk2{Z~yؽsX_ۛ_UޑT{gyט[[]o20zuo\R}korڝPWr85v{QUl\\_r[Xtqo6\wyu9QUS~ou^VUpWx']5RZ_z\2}duWsypYyxh{[Q0u]w{_p]_V9xStRu[4w8tn_v^wZlYw[~v8ڕ~Rpwu9zqUpy>]v۔;]wX\}}_}V3{߭wnռ?믖PQ;]q_M]pzl_y67wlYwhS[XTq>u%Y?=~5|s|QWUsZvvX]TV^os[vjnYVytsZu'7[ߛzvw{&vyRzuq^{Yj3\|Yttv]~n4URoUU9{[ZЛu0]qv[wrwR~7kn7uvlt[~T^_u3s_[t.\zғ~|pmzTwVqn~U~,7z1x>QX\ڵm|Vs~pە_Yx|w8u9ZUWOqt9_Y[ָ3|RRj\~Yvxݙo|m\ؿtu7jszw|XXy*^8Y^7n/uZ]ZuuWMV\n=ܽZ{OVטݲ0u6YX\tOR}lo8{9kVМrtt՚]vq]Uxs/v\uTZTtl1Xu83]}r1oqZZS_wovQ];Q\]9q[y[sqxPݿ/w~>yj\PZZv_<_+vwNXsp0Z|7Vq~<[ؓr<]lYT{\ZxTVѾV_Xܞv$OwTWvz^{<|W6rv{ܟu[^v\ٸyxqxݽv}xޟ{WZ[6x^d^Sy}|]1<d]Q~ںww|ݞu0d^TvWXvvxxޜ{wZ|SZRu|6^|gٟ[yxx6Ym[^^0?|ڝ\]ur{t"{Zٺ_s\w|\fkUؒ\ӛY[\z\Q\OQ_ws:zdq\\_6=xYws%Xݘu{}]~\~i8YҜ[}ײx0>wtw|s\\SP\؏_Y{ZepWOSZXܷ_Wx޾kg{ڜq}vu2_gԗy_[qsoxvv[k>TOzWZ\{qZR{}[XiUZ\_q_w_{Y~dTS|uzuܷ~P{[}-z}2?qkSXz01;LRߜ_tvZ^[jKTX^3|[>(jL[Y؝o]vt~ߴtpdRԗ~ۛ3xuZ߳\umNQ_[w[X}lj;ӻz}~v22֕zpq\u4n۬y[U[6z7_.nx؏Y]ۭsyOTpy;wVnzYO>W|Pyz{^}X,XԴ:W]vUyrqqzwp_P}WZv{}%:~Pzߞy_oqn_ӞXxtuZo7us]Uڙ^5}طtzZz_qq5ޱlOo]\wztXޝz10<8kz<]~7Wv~suoT2xٞ|9z[|T8=ɘWL}2UYtt^[wYm/^tp/\zzvtpqTz]_^9wtP^YV]݊T:Z];_;Y_^tR7~;[t,*wpm|r3imwWmw_}m;~v^ww~ܬy]qֿtq׽:ҶR_0xո{q^S8y}|xgx3]~_ۻTUߕ^U9O^^P]WVtUtyyr|tunrvpxn{swoߵyۭtU]v2X{_֛Uy]3O:Ү[;J&U[y}=x8|p?[W~Ѯp}7Z_~syV4[ZnZ4vv6s42_z^_/[[[r;zWVܻWYtXS|X^ZrZx9N75oךP=ԓO~\[o]\6>h6^_p*}o-q|q^]{^{QQTӜWu1WymoZ4/t}vo_puXv[vԟ9ӽͿ\URM~\~X_Rݷs^qּq|/8pW,~1qڹnwYx6tpwnR2;\p>]TpN292\s^Y2q}28v_zXn[qZ}=pپR[?xZXSxڑ{zTZϿ}3tzT\?kQT^6wUR^s3zqrxh2sz*t7Xlmpp27Xo~}>uZ[sؕZ}ݿQTQzZ_VtU9Λ;3|{w۶vW~Qd P[^y4UQ~&[ܾ>NΖۻ[s[ַ?]ޔdlOVZw|2^|X$O\w;4x5_^UҕY}<^ܘz[R4fWPMZzz<{twV}{z>٫exPX_v~;wWWђV4~2+mmx|r7oxz땼pW]ם^|ܼzqTqԎYQ^^zwxTNvוWTџؙ7޴~*y]zՎXWvvyu.p6g{?trZ]2_qnڻ>{s[p||sVn߱uS nݾ]2WVSP[\X[[;ޛz<T5~W2|ֶwwUUsn.Ry_~0pV_:w}xyټU[}Vݏx^R_5Ts]_XݒVښ{StVwS5Z9 {\=4ne:ٞ{Uo{}_T|;qsٮոZu[Z3:[ֽ|xxT\T~YP^_zY0o|7p5p]OW=TN{T\R{3xط0nw>Y?Y;v4t:.vpzX|7ԔixW[9ou?{x\ZvqRn2llsvuz3|q/{sޞ8]YՕWWЮؾRVs6Ovxp<\wYߔun֘Y|jZz}p>tnPW۷m2hԖk5v|u[hp-8sy8Zp:oZ^o]y1ZZܓSUҿSR^~ZR:fZ8?x9qrtpv{ptt]xYy;z\[o:Tښ[WYOR^p|X;X_UP_\_^.XZ\5|2Tslh8r~xu=^2:Z6\^5UR^4_sjt߳5nzx\zT~u5sٹrݙМvMXѐXX3}}yܗHӒVp7ZZ9ܐΒR]kvWu7sUQqmlw^v5stwVғuo6֟{|\QX|o5zܛqWOovkt]uտtXQҞwywWwsk\Q{nuٗplؚؓZ{m|tzZWRҝywv9tNxjS?Sy귖>^tTګo^\w\-*X>vmV_j|~rz4Y3}t{O9SUҿ1{ܔr<_prlTu[mlp[R?߮wVX;В{qT/{NpnW2}P6{WnY~\P~kyvϜX^W\zR(uvZ6_u|]$p}WqnS}O|j6_T8ӻ}mf\^Ԍ^2[S׷Տ4:uVhxU|uzmV&k}ߟt{-|X>*k?zWO~z\Гgu[wQU۱8mzZ\xؘ5zw]7knR~۰]֩pQZw7y8Wt$l]uQ_8}\O]f?]_Ov^X#ׯӵwN{q]sWW~vt_&y;Qھr{z>%r~y\Ps߻~[L_UyӒ^}٬\]{Tz|{\SezySV>uq2W|e]Y>Yp;yp^\[r&_;RWi{W_yR]}=׫n[ҟyywڙxRdyT}:8u;U9v[|[4{ttzgڹZV[Qp]SVt]ٳW j.|zPR:[{^[Qj4_QUw؛Oi;p_yzowpzTxrSܔ\_km{rZ:YJk_~NV~7|| :+nPXpV}yqgt~tVjtr;V|61ryY꫾RzVy~T,orxҕXYoqyטUl{|qvzSؕZX\x_ϸ.+[+YWSW^q챺wm[{}xsPXN]~ה29r{X|poWsr/U^>pzs+2yW[U?p:_sқXwUxX|v\wswV~S_ynTQޗvyntїUoU.tu>zoٞ-Lu?wXRYZ<:umQ\Txpul,kWzV[_w5vvTxQܿ[t]Yq5^-QYV{:{ZPP|^[^wTҙZQ뛴X>~t?y{^R?:Qy_mX|~zy]r6pXk\ږRv;uWLPYݙui/_u{zzYv֫s9ZYzpu_||wp5vskZ~Xxt|ӭ]r>NY{Vl:Y1/s2YTսxrspW|yp{߮OPunޘ9^7[WZY\s2,ZX~z[PNڴ{3YzvzΖ؜|4xnpU|oZQQo,VyvVTع{.7UuyOԚ]t.\;Ty{oIt6ޜ=ZpZyО^sq:.{}kUtOzR[{/:~y+PzP^t28{xpzrٙR^OsY~o~:z]5]oKuY^2Z4t5z]]vr]vtsm4>߳X-[R[]yok~]qoXNXVuu.1q|mzsYNXظ_5w}tʛRнZoy81Z?|Z[W|zwx1_~3|\pZi^=ro28\r_T^]?t73~~>UuuYNYyx2[Svzwݔ|ֹ;~nl<}|~LxZ*lz}m{ZVZݝ\h~5V]xpTվ9XzW]5\l^Қӝ~tu:XغY}p{~VtYv[Spr5{Xv\SVX~)R{u6l>/wXYp^ڐq|=޴wڴWTtu3ֱ{ѯY|s~W}{1:\Vyw8~Zq_t~}yz[MY|:ܽݿWs{\<nyw^vؒP|Rp,s5out]ZnY]4,t2XZVZ~\vp9rz~VSw\xt7o[[u{\p14>pt}ZՒ}qvW<xrԎSYvsowX}wv}~VZzsuyOVYxs520]XyYwTk87wq]mtNZTRuuVqtu3W6^zVӕw~=yt-|W~?w>vthqrORxp\4vnV=SVP8tx2YݗPZklxWvU]{u\^Z{l[R[|it-Rwwxzr\Q^~k=o=ּvv2uWV3p0TUOq52ZZ92{YT{qm2UuuwrvRpjq}:lߴs\ё|t==׷ֲpyuRT|onqW|;{Mzmly^vzstU{zܜlvTPL16_o{yUYӺԯxֽ5O<\[ӷq2UuRZ{zSs^nZV<zЙYrs|8T7Yojx]|rRڜ}o](^yzZ >nyt{Z{ROrlvZ^x;|Z6?]s]vWxcwwVw^_>)jz|S||wWٞ_7km]|TL_twX}Rq_Tv=:V|xy;8xtRWgۜxZxX%\SOxv{<^~|Tz|ߙMhz}]N\4yZ}}~x)k{LMywr\d:zUԘ5V(s7X{{tZL5|^U\~PtOvzxsxXQ8wQ}{R~<]T۪tyyΒ^}yMQvLxy3nrt5wxzURu84s޶|y:KQ~YwvzZv{oۜoR;;kR?,|46YSxY;kq__1p?o?{U}YrS[uzmvՑs]gwk[_O\/V^lon[Yw~}qspy~8P^9-nyY}5 sq>WYZ}2yvZ[Иy<[Zq{lT5ZqsnVxvwq=s^vsoqy}WU|y0v^>Sٔ~\={ӷu5R?w|T֓Zw]nutTְ.yy_sx]R2[ur^Zt]RryoQ~~ZU-2xwt6R[^UYշu{Yy0ڶuP\R+X/u߰^yw[}U[|qo\UXUXSW3tVw:]?Թ8uu^[Y[vL6^ZW^{vu״X~1n}Uv:tp{Uݽ=xxs>pWPxm]\ps;Ot8X{xqQxz7ݕmov~}OZwwY}xtwNP>\3'|{oX2=nvpm.[z<}\\0^vL}^tݘqu߶XYՋ4Y}]y_{XђU^]x6}pxv}p3T6}y0p~n^[tw^{8vWZїz7Xn{_Sٰ|TwXZN~x}n_>pUtR8y7wr[qx{su9^pu_pqU>yRtpstwvryvU1w4pm|^V|7p1^_40^qrnܙ~]W?nZ[5ZQ>Nؽ1Wtwv߸V_Qvyh:ټs:uXRo}ywUԖ|4t4Q^yvXR:o,pr;zN܍zxT]q]s{}Z֯wV^SwnssXY^yo=ٕVzt>]uuq]xMMZN{q8rҩSY[\-4luR]TwWUo5Ws{۝tn{v^[N^S_W:;\XY{y]x|]>^xZ[Xw%xvZWXu^6կr\YY9^sΚNVWZtl}n=mwwn<_nurԑ[װw8tqV]TSܭ*\t]TUv5W޽w{yKQS{{48[UՒu|>n\ٽukدsqv>\\qqs5[t~y<ӟxtt}6sz_˜[t]zpYSWx;qzO]=pksuus_L~oUvZ?ӛx3ps[j~|STԷxR\t]_Ov~Yq0wZxݰ~un|t6VT1ؐhWOswtW]_ZPTroquNvԝtPz~|h4:xQ~Q_qYMl^O֙xtVљrY{ߝy}۶6nhm:VyYqptoѳ^Y_{w^߶VSu~X?\|9MX]R|_w9M)g;\Ք[ٶ~;kzV^z~_ޑe?]Ք]_zX \fuyږ{zwks;SY}|tx;ڲ|W|x{ysuz>|]{RZYTZ]wyϖji[Uw|~+*^]u ry[s_}~~]\^=|tOqgy|Zzv]uۓs'\^|9OR(_=ys?jk8U}W|vz2yNu~WT0ywYf:Zxsth,]]ZTxt4 d_zV|v/TXؚ} ~w{[4}xQrZ_x{\2x>inܚӚ}zuzw,^??^]T}?tnWV_tMWjn߼]Z:}rPp\|TmS[>MM~t\x?rZY۷sx}t_QU{|r_N+s{߷vpQ|1]lqߝyUMJZ3w:]^yxZSX|q][x[ O4t7o[z^[N^q9YyY~[;qV:yؓ\4z[spZ\OT|ypj[4:Srіvouo{؟ܻptoxX׌ ^wusTr{z1UmuXtXK^{5rKN_p5ִnxsYuW~)t{Zv0y| _lߘr~xY Ԗ:v~;Z]NQZxr;xZJ^wmZ46\tTϼZuoly^u]s1oZ^|Mʖ|vr6x~X Rk|Y/}N>^p5~Nwrs}w[K9qt-?_v̏vqWp45}W8swӱrxݺ،tz1\W}p}R\q6sptPY6X.s5xWN|q}qru]}͋~nS3srWqOغwn{ֶnoux]?96o{Nz1t|}yzך62YxV~yN]o}QLX{ڎZtX}y;l}wpls?.VWxslvv8PQ3x2][lrt}y6V~;vX[Rx{2^wڼY׏uvqPNmxTXvRtw^uuTq>~9?LR.Rx_3twhz}{nS[o:kz_x8ZӕixVWXxRnq~P|xULTnUZy}uL9.=xyylٻl긹^Ustu[n?\x~МlxV5SW޺ߚYmZYSwXNipԾv֓:rztV:rZ^wV]vl\{-yvQgV2޴i/޸_4R߰lxTxU]xZkޚTp\yXX;n?Yj:[YwV\i-|ەw]y[0{Pxҗml+|S}{]יio[z]VԔ|ٟzNsjv}S^Uרl\[~xtz)]5^Y6Zjxv{՜}[;hU>Q\[՜Z9r[_\OYpu]u)jv9__q\d\ٽY[Y{\v6_җQWw^~ivۖYRW]]zr]^ؓgyZ_srr|*μ؛rX[i~rx߯uپUQUSY_^nQ]W_|}/uگ~xSq^z1\5r[|5wxXw\vzq[y.oZzrq>{p[Xtyv|;S_{ٔqޔZ~|ט1XsY\ytnr}0n~{^|Z=rx_sݒpt{ZUu[kXW^[RםUPZY}TnٷTV^,ޗ|ym׵ڪx^|׮xU_5ߛs}twXڵXzYtyQ?U{Իz|86qq{mTt]4߿ܞgplwtW*kR}w]\RZ7,[}~z|[NmY9nyx]XRVԜ]Q[\[]PTP_\|יl5:r{{zssoq:?m]t1[w[>=Vk\Ӿ>}X~[|ztt^Lr9tt:_t11p~ZZ<~|җ=]Z۟[7^ەӼp~3UQZ_єWPx͞67zsWnrYڞy~zmxn+mvt_x7~8Wpr{ذw^sM;YϹ2W;Z|ڞ~>]\]{zY]}ٲ{^ \ No newline at end of file