From f787f8dd5c1346f92e5bfde905281c97eb25ee97 Mon Sep 17 00:00:00 2001 From: Volker Fischer Date: Sat, 28 Jan 2006 11:29:22 +0000 Subject: [PATCH] initial version --- AUTHORS | 1 + COPYING | 280 +++++++++ ChangeLog | 0 INSTALL | 54 ++ Makefile.am | 3 + NEWS | 0 README | 75 +++ TODO | 1 + bootstrap | 8 + configure.in | 70 +++ linux/Makefile.am | 106 ++++ linux/sound.cpp | 480 ++++++++++++++++ linux/sound.h | 105 ++++ src/aboutdlgbase.ui | 281 +++++++++ src/audiocompr.cpp | 255 +++++++++ src/audiocompr.h | 104 ++++ src/buffer.cpp | 381 +++++++++++++ src/buffer.h | 100 ++++ src/channel.cpp | 448 +++++++++++++++ src/channel.h | 169 ++++++ src/client.cpp | 317 +++++++++++ src/client.h | 135 +++++ src/global.h | 154 +++++ src/llconclientdlg.cpp | 328 +++++++++++ src/llconclientdlg.h | 96 ++++ src/llconclientdlgbase.ui | 1125 +++++++++++++++++++++++++++++++++++++ src/llconserverdlg.cpp | 177 ++++++ src/llconserverdlg.h | 78 +++ src/llconserverdlgbase.ui | 272 +++++++++ src/main.cpp | 106 ++++ src/multicolorled.cpp | 182 ++++++ src/multicolorled.h | 139 +++++ src/resample.cpp | 184 ++++++ src/resample.h | 75 +++ src/resamplefilter.h | 157 ++++++ src/resamplefilter.m | 50 ++ src/server.cpp | 113 ++++ src/server.h | 78 +++ src/settings.cpp | 378 +++++++++++++ src/settings.h | 88 +++ src/socket.cpp | 121 ++++ src/socket.h | 78 +++ src/util.cpp | 276 +++++++++ src/util.h | 381 +++++++++++++ windows/MocQT.bat | 48 ++ windows/llcon.dsp | 313 +++++++++++ windows/llcon.dsw | 29 + windows/llcon.rc | 72 +++ windows/mainicon.ico | Bin 0 -> 1078 bytes windows/resource.h | 16 + windows/sound.cpp | 601 ++++++++++++++++++++ windows/sound.h | 119 ++++ 52 files changed, 9207 insertions(+) create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 ChangeLog create mode 100755 INSTALL create mode 100644 Makefile.am create mode 100644 NEWS create mode 100755 README create mode 100644 TODO create mode 100755 bootstrap create mode 100755 configure.in create mode 100755 linux/Makefile.am create mode 100755 linux/sound.cpp create mode 100755 linux/sound.h create mode 100755 src/aboutdlgbase.ui create mode 100755 src/audiocompr.cpp create mode 100755 src/audiocompr.h create mode 100755 src/buffer.cpp create mode 100755 src/buffer.h create mode 100755 src/channel.cpp create mode 100755 src/channel.h create mode 100755 src/client.cpp create mode 100755 src/client.h create mode 100755 src/global.h create mode 100755 src/llconclientdlg.cpp create mode 100755 src/llconclientdlg.h create mode 100755 src/llconclientdlgbase.ui create mode 100755 src/llconserverdlg.cpp create mode 100755 src/llconserverdlg.h create mode 100755 src/llconserverdlgbase.ui create mode 100755 src/main.cpp create mode 100755 src/multicolorled.cpp create mode 100755 src/multicolorled.h create mode 100755 src/resample.cpp create mode 100755 src/resample.h create mode 100644 src/resamplefilter.h create mode 100755 src/resamplefilter.m create mode 100755 src/server.cpp create mode 100755 src/server.h create mode 100755 src/settings.cpp create mode 100755 src/settings.h create mode 100755 src/socket.cpp create mode 100755 src/socket.h create mode 100755 src/util.cpp create mode 100755 src/util.h create mode 100755 windows/MocQT.bat create mode 100755 windows/llcon.dsp create mode 100755 windows/llcon.dsw create mode 100755 windows/llcon.rc create mode 100755 windows/mainicon.ico create mode 100755 windows/resource.h create mode 100755 windows/sound.cpp create mode 100755 windows/sound.h diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..4d4cf802 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Volker Fischer diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..c7aea189 --- /dev/null +++ b/COPYING @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 675 Mass Ave, Cambridge, MA 02139, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..e69de29b diff --git a/INSTALL b/INSTALL new file mode 100755 index 00000000..869c4042 --- /dev/null +++ b/INSTALL @@ -0,0 +1,54 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + + +Windows: +-------- + +Rquired software: QT 2 non-commercial, Visual C++ 6.0 + +- run llcon/windows/MocQT.bat +- open llcon.dsw in your Visual C++ 6.0 environment and compile +- run llcon/windows/Release/llcon.exe + + +Linux: +------ + +Required packages: QT (devel packages, too!), ALSA (devel packages, too!) + +- cd llcon +- sh bootstrap +- ./configure +- make +- run llcon/linux/llcon + +NOTES: +- it may be required by your Linux distrubution that you set the QTDIR + environment variable prior to the "./configure" call. E.g., for SUSE: + export QTDIR=/usr/lib/qt3 + or for Debian: + export QTDIR=/usr/share/qt3 +- if the file "bootstrap" is not available, skip this step + diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 00000000..a15ebeb7 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,3 @@ +AUTOMAKE_OPTIONS = foreign + +SUBDIRS = linux diff --git a/NEWS b/NEWS new file mode 100644 index 00000000..e69de29b diff --git a/README b/README new file mode 100755 index 00000000..3ee5eb0d --- /dev/null +++ b/README @@ -0,0 +1,75 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +llcon +----- + +Low-Latency Connection / client and server + + +OBJECTIVES: +The task is to build a client/server software to enable musicians to play together +over the internet. Target internet connection is DSL with 256 kbps upstream and +1 Mbit downstream. The server software must be located at a server with a very +fast internet connection (at least 1 Mbps for up- and downstream). + +To get sufficient results, a sample rate of 24 kHz (mono channel) was chosen. An +audio compression algorithm with very low delay is IMA-ADPCM (delay is just one +sample). This gives a raw compressed audio data rate of 96 kbps. + +Target hardware setup at the client is stereo audio input signal with one channel +is the instrument and the other channel is a microphone signal. On the microphne +channel a reverberation effect can be applied (maybe at a later time other audio +effects are added). + + +MANUAL: +For starting server type ./llcon -s + +Start the llcon server on a remote computer with fast internet access. Start the +llcon client on your local computer and connect your sound card with your +instrument/microphone and headphone and type in the IP address of the server. +There are levelers for adjusting the sound card (in/out) and network buffer sizes. +It seems that 2 blocks for network buffer is optimal choice. For the sound card +buffer, try to make them as short as possible by watching the LEDs below the +levelers (they should stay green) and the timing standard deviation (should be +as low as approx. 0.5 ms). + +For test purpose it is possible to run server and client on the same computer. For +this setup firstly start server and then the client. Type in 127.0.0.1 for the +IP address in the client. + + +EXTERNAL CODE: +This code contains open source code from different sources. The developer(s) want +to thank the developer of this code for making their efforts available under open +source: + +- audio reverberation code: by Perry R. Cook and Gary P. Scavone, 1995 - 2004 + (taken from "The Synthesis ToolKit in C++ (STK)") + +- IMA-ADPCM: by Erik de Castro Lopo + +- some parts are taken from the project "Dream: a PC-based Digital Radio Mondiale + (DRM) receiver" written by one of the llcon authors, Volker Fischer diff --git a/TODO b/TODO new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/TODO @@ -0,0 +1 @@ + diff --git a/bootstrap b/bootstrap new file mode 100755 index 00000000..fea2bbbc --- /dev/null +++ b/bootstrap @@ -0,0 +1,8 @@ +#! /bin/bash + +aclocal -I . && \ +autoheader && \ +libtoolize --automake --copy && \ +automake --add-missing --copy && \ +autoconf +echo "Ready to run ./configure" diff --git a/configure.in b/configure.in new file mode 100755 index 00000000..0df728d7 --- /dev/null +++ b/configure.in @@ -0,0 +1,70 @@ +dnl Process this file with autoconf to produce a configure script. +AC_PREREQ(2.50) +AC_INIT(src/main.cpp) + +AM_INIT_AUTOMAKE(llcon,0.9.1) + + +AM_CONFIG_HEADER(config.h) + +dnl Checks for programs. +AC_PROG_LIBTOOL +AC_PROG_CC +AC_PROG_CXX +AC_PROG_CPP +AC_PROG_INSTALL +AC_SUBST(LIBTOOL_DEPS) +AC_PROG_MAKE_SET +AC_CHECK_PROGS(RPMBUILD, rpmbuild, rpm) + +dnl Configuration Arguments + +AC_ARG_ENABLE( sound,[ --enable-sound generic sound support], enable_sound=$enableval, enable_sound=yes) + +AC_ARG_WITH( qtdir,[ --with-qtdir=path to QT],QTDIR=$withval) + + + +AC_CHECK_HEADER(sys/asoundlib.h, , enable_sound=no) +AC_CHECK_LIB(asound, snd_pcm_open, , enable_sound=no) + +if test "$enable_sound" = "yes"; then + AC_DEFINE(WITH_SOUND, 1, [Define if you want to use sound]) +fi + +if test "$enable_client" = "yes"; then + AC_DEFINE(APPL_TYPE_CLIENT, 1, [Define if you want to use client]) +fi + + +dnl Checks for header files. +AC_HEADER_STDC +AC_LANG_CPLUSPLUS + +dnl Checks for some external libraries that need to be installed +AC_LANG(C++) + + +dnl QT -------------------------------------------------------------------------- +if test "no$QTDIR" = "no"; then + AC_MSG_ERROR( "*** Please set QTDIR ***" ) +fi + +saved_ldflags="$LDFLAGS" +LDFLAGS="$LDFLAGS -L$QTDIR/lib" + +AC_HAVE_LIBRARY(qt-mt,,exit 1) + +AC_PATH_PROG(MOC, moc,, "$QTDIR/bin") +if test -z "$MOC"; then + AC_MSG_ERROR("No moc found in $QTDIR/bin"); +fi +AC_PATH_PROG(UIC, uic,, "$QTDIR/bin") +if test -z "$UIC"; then + AC_MSG_ERROR("No uic found in $QTDIR/bin"); +fi +AC_SUBST(QTDIR) + + +AC_CONFIG_FILES(Makefile linux/Makefile) +AC_OUTPUT diff --git a/linux/Makefile.am b/linux/Makefile.am new file mode 100755 index 00000000..6e0360b0 --- /dev/null +++ b/linux/Makefile.am @@ -0,0 +1,106 @@ +bin_PROGRAMS = llcon + + +llcon_SOURCES = ../src/buffer.cpp \ + ../src/main.cpp \ + ../src/socket.cpp \ + ../src/audiocompr.cpp \ + ../src/resample.cpp \ + ../src/channel.cpp \ + ../src/util.cpp \ + ../src/llconclientdlg.cpp \ + ../src/client.cpp \ + ../src/llconserverdlg.cpp \ + ../src/server.cpp \ + ../src/settings.cpp \ + ../src/multicolorled.cpp \ + sound.cpp \ + ../src/buffer.h \ + ../src/global.h \ + ../src/socket.h \ + ../src/audiocompr.h \ + ../src/resample.h \ + ../src/resamplefilter.h \ + ../src/channel.h \ + ../src/util.h \ + ../src/client.h \ + ../src/server.h \ + ../src/settings.h \ + ../src/multicolorled.h \ + ../src/llconserverdlg.h \ + ../src/llconclientdlg.h \ + ../src/llconclientdlgbase.ui \ + ../src/llconserverdlgbase.ui \ + ../src/aboutdlgbase.ui \ + sound.h + +# these need to be generated before the rest can be compiled + +BUILT_SOURCES=moc/moc_server.cpp \ + moc/moc_socket.cpp \ + moc/moc_multicolorled.cpp \ + moc/moc_util.cpp \ + moc/moc_llconclientdlg.cpp moc/moc_llconclientdlgbase.cpp moc/llconclientdlgbase.h moc/llconclientdlgbase.cpp \ + moc/moc_llconserverdlg.cpp moc/moc_llconserverdlgbase.cpp moc/llconserverdlgbase.h moc/llconserverdlgbase.cpp \ + moc/moc_aboutdlgbase.cpp moc/aboutdlgbase.h moc/aboutdlgbase.cpp + +# and should be cleaned by make clean + +CLEANFILES=$(BUILT_SOURCES) +nodist_llcon_SOURCES=$(BUILT_SOURCES) + +dist-hook: + mkdir $(distdir)/moc + +moc/moc_server.cpp: ../src/server.h + $(MOC) ../src/server.h -o moc/moc_server.cpp + +moc/moc_socket.cpp: ../src/socket.h + $(MOC) ../src/socket.h -o moc/moc_socket.cpp + +moc/moc_multicolorled.cpp: ../src/multicolorled.h + $(MOC) ../src/multicolorled.h -o moc/moc_multicolorled.cpp + +moc/moc_util.cpp: ../src/util.h + $(MOC) ../src/util.h -o moc/moc_util.cpp + + + +moc/moc_aboutdlgbase.cpp: moc/aboutdlgbase.h + $(MOC) moc/aboutdlgbase.h -o moc/moc_aboutdlgbase.cpp + +moc/aboutdlgbase.h: ../src/aboutdlgbase.ui + $(UIC) ../src/aboutdlgbase.ui -o moc/aboutdlgbase.h + +moc/aboutdlgbase.cpp: ../src/aboutdlgbase.ui moc/aboutdlgbase.h + $(UIC) ../src/aboutdlgbase.ui -i moc/aboutdlgbase.h -o moc/aboutdlgbase.cpp + + +moc/moc_llconclientdlg.cpp: ../src/llconclientdlg.h + $(MOC) ../src/llconclientdlg.h -o moc/moc_llconclientdlg.cpp + +moc/moc_llconclientdlgbase.cpp: moc/llconclientdlgbase.h + $(MOC) moc/llconclientdlgbase.h -o moc/moc_llconclientdlgbase.cpp + +moc/llconclientdlgbase.h: ../src/llconclientdlgbase.ui + $(UIC) ../src/llconclientdlgbase.ui -o moc/llconclientdlgbase.h + +moc/llconclientdlgbase.cpp: ../src/llconclientdlgbase.ui moc/llconclientdlgbase.h + $(UIC) ../src/llconclientdlgbase.ui -i moc/llconclientdlgbase.h -o moc/llconclientdlgbase.cpp + + +moc/moc_llconserverdlg.cpp: ../src/llconserverdlg.h + $(MOC) ../src/llconserverdlg.h -o moc/moc_llconserverdlg.cpp + +moc/moc_llconserverdlgbase.cpp: moc/llconserverdlgbase.h + $(MOC) moc/llconserverdlgbase.h -o moc/moc_llconserverdlgbase.cpp + +moc/llconserverdlgbase.h: ../src/llconserverdlgbase.ui + $(UIC) ../src/llconserverdlgbase.ui -o moc/llconserverdlgbase.h + +moc/llconserverdlgbase.cpp: ../src/llconserverdlgbase.ui moc/llconserverdlgbase.h + $(UIC) ../src/llconserverdlgbase.ui -i moc/llconserverdlgbase.h -o moc/llconserverdlgbase.cpp + + + +llcon_CXXFLAGS=$(QWTINCL) -I../src -I$(QTDIR)/include -DQT_THREAD_SUPPORT -D_REENTRANT -g diff --git a/linux/sound.cpp b/linux/sound.cpp new file mode 100755 index 00000000..5f149bd6 --- /dev/null +++ b/linux/sound.cpp @@ -0,0 +1,480 @@ +/******************************************************************************\ + * Copyright (c) 2004-2005 + * + * Author(s): + * Volker Fischer, Alexander Kurpiers + * + * This code is based on the Open-Source sound interface implementation of + * the Dream DRM Receiver project. + * +\******************************************************************************/ + +#include "sound.h" + +#ifdef WITH_SOUND +/* Wave in ********************************************************************/ +void CSound::InitRecording(int iNewBufferSize, bool bNewBlocking) +{ + int err; + + /* set internal buffer size for read */ + iBufferSizeIn = iNewBufferSize / NUM_IN_OUT_CHANNELS; /* mono size */ + + /* if recording device was already open, close it first */ + if (rhandle != NULL) + snd_pcm_close(rhandle); + + /* record device: The most important ALSA interfaces to the PCM devices are + the "plughw" and the "hw" interface. If you use the "plughw" interface, + you need not care much about the sound hardware. If your soundcard does + not support the sample rate or sample format you specify, your data will + be automatically converted. This also applies to the access type and the + number of channels. With the "hw" interface, you have to check whether + your hardware supports the configuration you would like to use */ + /* either "hw:0,0" or "plughw:0,0" */ + if (err = snd_pcm_open(&rhandle, "hw:0,0", SND_PCM_STREAM_CAPTURE, 0) != 0) + { + qDebug("open error: %s", snd_strerror(err)); +// throw CGenErr("alsa CSound::Init_HW record"); + } + + /* recording should be blocking */ + if (err = snd_pcm_nonblock(rhandle, FALSE) != 0) + { + qDebug("cannot set blocking: %s", snd_strerror(err)); +// throw CGenErr("alsa CSound::Init_HW record"); + } + + /* set hardware parameters */ + SetHWParams(rhandle, true, iBufferSizeIn, iCurPeriodSizeIn); + + + /* sw parameters --------------------------------------------------------- */ + snd_pcm_sw_params_t* swparams; + + if (err = snd_pcm_sw_params_malloc (&swparams) != 0) + { + qDebug("snd_pcm_sw_params_malloc: %s", snd_strerror (err)); +// return NULL ; + } + + /* Get the current swparams */ + if (err = snd_pcm_sw_params_current(rhandle, swparams) < 0) + { + qDebug("Unable to determine current swparams : %s", snd_strerror(err)); +// throw CGenErr("alsa CSound::Init_HW "); + } + + /* Start the transfer when the buffer immediately */ + err = snd_pcm_sw_params_set_start_threshold(rhandle, swparams, 0); + if (err < 0) { + qDebug("Unable to set start threshold mode : %s", snd_strerror(err)); +// throw CGenErr("alsa CSound::Init_HW "); + } + + /* Align all transfers to 1 sample */ + err = snd_pcm_sw_params_set_xfer_align(rhandle, swparams, 1); + if (err < 0) { + qDebug("Unable to set transfer align : %s", snd_strerror(err)); +// throw CGenErr("alsa CSound::Init_HW "); + } + + +// TEST + /* Allow the transfer when at least period_size samples can be processed */ +// /* round up to closest transfer boundary */ +// start_threshold = (buffer_size / xfer_align) * xfer_align ; +// if (start_threshold < 1) +// start_threshold = 1 ; + + +// TEST +snd_pcm_uframes_t period_size = iBufferSizeIn; + + err = snd_pcm_sw_params_set_avail_min(rhandle, swparams, period_size); + if (err < 0) { + qDebug("Unable to set avail min : %s", snd_strerror(err)); +// throw CGenErr("alsa CSound::Init_HW "); + } + + + + /* Write the parameters to the record/playback device */ + err = snd_pcm_sw_params(rhandle, swparams); + if (err < 0) { + qDebug("Unable to set sw params : %s", snd_strerror(err)); +// throw CGenErr("alsa CSound::Init_HW "); + } + + /* clean-up */ + snd_pcm_sw_params_free(swparams); + + snd_pcm_reset(rhandle); + snd_pcm_start(rhandle); + + qDebug("alsa init record done"); +} + +bool CSound::Read(CVector& psData) +{ + /* Check if device must be opened or reinitialized */ + if (bChangParamIn == true) + { + InitRecording ( iBufferSizeIn * NUM_IN_OUT_CHANNELS ); + + /* Reset flag */ + bChangParamIn = false; + } + + int ret = snd_pcm_readi(rhandle, &psData[0], iBufferSizeIn); +//qDebug("ret: %d, iBufferSizeIn: %d", ret, iBufferSizeIn); + + if (ret < 0) + { + if (ret == -EPIPE) + { + /* Under-run */ + qDebug ( "rprepare" ); + + ret = snd_pcm_prepare ( rhandle ); + + if ( ret < 0 ) + { + qDebug ( "Can't recover from underrun, prepare failed: %s", snd_strerror ( ret ) ); + } + + ret = snd_pcm_start ( rhandle ); + + if (ret < 0) + { + qDebug ( "Can't recover from underrun, start failed: %s", snd_strerror ( ret ) ); + } + + return true; + + } + else if ( ret == -ESTRPIPE ) + { + qDebug ( "strpipe" ); + + /* Wait until the suspend flag is released */ + while ( ( ret = snd_pcm_resume ( rhandle ) ) == -EAGAIN ) + { + sleep(1); + } + + if ( ret < 0 ) + { + ret = snd_pcm_prepare(rhandle); + + if (ret < 0) + { + qDebug ( "Can't recover from suspend, prepare failed: %s", snd_strerror ( ret ) ); + } + throw CGenErr ( "CSound:Read" ); + } + + return true; + } + else + { + qDebug ( "CSound::Read: %s", snd_strerror ( ret ) ); + throw CGenErr ( "CSound:Read" ); + } + } + else + { + return false; + } +} + +void CSound::SetInNumBuf ( int iNewNum ) +{ + /* check new parameter */ + if ( ( iNewNum >= MAX_SND_BUF_IN ) || ( iNewNum < 1 ) ) + { + iNewNum = NUM_PERIOD_BLOCKS_IN; + } + + /* Change only if parameter is different */ + if ( iNewNum != iCurPeriodSizeIn ) + { + iCurPeriodSizeIn = iNewNum; + bChangParamIn = true; + } +} + + +/* Wave out *******************************************************************/ +void CSound::InitPlayback ( int iNewBufferSize, bool bNewBlocking ) +{ + int err; + + /* Save buffer size */ + iBufferSizeOut = iNewBufferSize / NUM_IN_OUT_CHANNELS; /* mono size */ + + /* if playback device was already open, close it first */ + if ( phandle != NULL ) + { + snd_pcm_close ( phandle ); + } + + /* playback device */ + /* either "hw:0,0" or "plughw:0,0" */ + if ( err = snd_pcm_open ( &phandle, "hw:0,0", SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK ) != 0) + { + qDebug ( "open error: %s", snd_strerror ( err ) ); +// throw CGenErr("alsa CSound::Init_HW playback"); + } + + /* non-blocking playback */ + if ( err = snd_pcm_nonblock ( phandle, TRUE ) != 0 ) + { + qDebug ( "cannot set blocking: %s", snd_strerror ( err ) ); +// throw CGenErr("alsa CSound::Init_HW record"); + } + + /* set hardware parameters */ + SetHWParams ( phandle, false, iBufferSizeOut, iCurPeriodSizeOut ); + + snd_pcm_start ( phandle ); + qDebug ( "alsa init playback done" ); +} + +bool CSound::Write ( CVector& psData ) +{ + int size = iBufferSizeIn; + int start = 0; + int ret; + + /* Check if device must be opened or reinitialized */ + if ( bChangParamOut == true ) + { + InitPlayback ( iBufferSizeOut * NUM_IN_OUT_CHANNELS ); + + /* Reset flag */ + bChangParamOut = false; + } + + while ( size ) + { + ret = snd_pcm_writei ( phandle, &psData[start], size ); +//qDebug("start: %d, iBufferSizeIn: %d", start, iBufferSizeIn); + + if ( ret < 0 ) + { + if ( ret == -EAGAIN ) + { + if ( ( ret = snd_pcm_wait ( phandle, 1 ) ) < 0 ) + { + qDebug ( "poll failed (%s)", snd_strerror ( ret ) ); + break; + } + + continue; + } + else if ( ret == -EPIPE ) + { + /* under-run */ + qDebug ( "wunderrun" ); + + ret = snd_pcm_prepare ( phandle ); + + if ( ret < 0 ) + { + qDebug ( "Can't recover from underrun, prepare failed: %s", snd_strerror ( ret ) ); + } + continue; + } + else if ( ret == -ESTRPIPE ) + { + qDebug("wstrpipe"); + + /* wait until the suspend flag is released */ + while ( (ret = snd_pcm_resume ( phandle ) ) == -EAGAIN ) + { + sleep(1); + } + + if ( ret < 0 ) + { + ret = snd_pcm_prepare ( phandle ); + + if ( ret < 0 ) + { + qDebug ( "Can't recover from suspend, prepare failed: %s", snd_strerror ( ret ) ); + } + } + continue; + } + else + { + qDebug ( "Write error: %s", snd_strerror ( ret ) ); +// throw CGenErr ( "Write error" ); + } + break; /* skip one period */ + } + + size -= ret; + start += ret; + } + + return false; +} + +void CSound::SetOutNumBuf(int iNewNum) +{ + /* check new parameter */ + if ( ( iNewNum >= MAX_SND_BUF_OUT ) || ( iNewNum < 1 ) ) + { + iNewNum = NUM_PERIOD_BLOCKS_OUT; + } + + /* Change only if parameter is different */ + if ( iNewNum != iCurPeriodSizeOut ) + { + iCurPeriodSizeOut = iNewNum; + bChangParamOut = true; + } +} + + +/* common **********************************************************************/ +bool CSound::SetHWParams(snd_pcm_t* handle, const bool bIsRecord, + const int iBufferSizeIn, const int iNumPeriodBlocks) +{ + int err; + snd_pcm_hw_params_t* hwparams; + + if (err = snd_pcm_hw_params_malloc(&hwparams) < 0) + { + qDebug("cannot allocate hardware parameter structure (%s)\n", snd_strerror(err)); + return true; + } + + if (err = snd_pcm_hw_params_any(handle, hwparams) < 0) + { + qDebug("cannot initialize hardware parameter structure (%s)\n", snd_strerror(err)); + return true; + } + + /* get configuration */ + if (err = snd_pcm_hw_params_any(handle, hwparams) < 0) + { + qDebug("Broken configuration : no configurations available: %s", snd_strerror(err)); + return true; + } + + /* Set the interleaved read/write format */ + if (err = snd_pcm_hw_params_set_access(handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) + { + qDebug("Access type not available : %s", snd_strerror(err)); + return true; + } + + /* Set the sample format */ + if (err = snd_pcm_hw_params_set_format(handle, hwparams, SND_PCM_FORMAT_S16) < 0) + { + qDebug("Sample format not available : %s", snd_strerror(err)); + return true; + } + + /* Set the count of channels */ + if (err = snd_pcm_hw_params_set_channels(handle, hwparams, NUM_IN_OUT_CHANNELS) < 0) + { + qDebug("Channels count (%i) not available s: %s", NUM_IN_OUT_CHANNELS, snd_strerror(err)); + return true; + } + + /* Set the sample-rate */ + unsigned int rrate = SND_CRD_SAMPLE_RATE; + if ( err = snd_pcm_hw_params_set_rate_near ( handle, hwparams, &rrate, 0 ) < 0 ) + { + qDebug("Rate %iHz not available : %s", rrate, snd_strerror(err)); + return true; + } + if ( rrate != SND_CRD_SAMPLE_RATE ) + { + qDebug ( "Rate doesn't match (requested %iHz, get %iHz)", rrate, err ); + return true; + } + + + + + + /* set the buffer and period size */ + +// TEST +snd_pcm_uframes_t BufferFrames = iBufferSizeIn * iNumPeriodBlocks; + + if (err = snd_pcm_hw_params_set_buffer_size_near(handle, hwparams, &BufferFrames) < 0) + { + qDebug("cannot set buffer size (%s)\n", snd_strerror (err)); + return true; + } + +// TEST +snd_pcm_uframes_t PeriodSize = iBufferSizeIn; + + if (err = snd_pcm_hw_params_set_period_size_near(handle, hwparams, &PeriodSize, 0) < 0) +//if (err = snd_pcm_hw_params_set_period_size_max(handle, hwparams, &PeriodSize, 0) < 0) + { + qDebug("cannot set period size (%s)\n", snd_strerror (err)); + return true; + } + + + + /* Write the parameters to device */ + if (err = snd_pcm_hw_params(handle, hwparams) < 0) + { + qDebug("Unable to set hw params : %s", snd_strerror(err)); + return true; + } + +/* check period and buffer size */ +qDebug("desired block size: %d / desired buffer size: %d", iBufferSizeIn, iBufferSizeIn * iNumPeriodBlocks); + +// TEST +snd_pcm_uframes_t buffer_size; +if (err = snd_pcm_hw_params_get_buffer_size(hwparams, &buffer_size) < 0) { + qDebug("Unable to get buffer size for playback: %s\n", snd_strerror(err)); +// throw CGenErr("alsa CSound::Init_HW "); +} +qDebug("buffer size %d", buffer_size); + +snd_pcm_uframes_t period_size; +err = snd_pcm_hw_params_get_period_size(hwparams, &period_size, 0); +if (err < 0) +{ + qDebug("Unable to get period size for playback: %s\n", snd_strerror(err)); +// throw CGenErr("alsa CSound::Init_HW "); +} +qDebug("period size %d", period_size); + + + + + /* clean-up */ + snd_pcm_hw_params_free(hwparams); + + return false; +} + +void CSound::Close() +{ + /* read */ + if (rhandle != NULL) + snd_pcm_close(rhandle); + + rhandle = NULL; + + /* playback */ + if (phandle != NULL) + snd_pcm_close(phandle); + + phandle = NULL; +} + +#endif /* WITH_SOUND */ diff --git a/linux/sound.h b/linux/sound.h new file mode 100755 index 00000000..78a25e1b --- /dev/null +++ b/linux/sound.h @@ -0,0 +1,105 @@ +/******************************************************************************\ + * Copyright (c) 2004-2005 + * + * Author(s): + * Volker Fischer, Alexander Kurpiers + * + * This code is based on the Open-Source sound interface implementation of + * the Dream DRM Receiver project. + * +\******************************************************************************/ + +#if !defined(_SOUND_H__9518A621345F78_3634567_8C0D_EEBF182CF549__INCLUDED_) +#define _SOUND_H__9518A621345F78_3634567_8C0D_EEBF182CF549__INCLUDED_ + +#include +#include +#include +#include +#include +#include + +#include +#include "util.h" +#include "global.h" + +#if WITH_SOUND +# define ALSA_PCM_NEW_HW_PARAMS_API +# define ALSA_PCM_NEW_SW_PARAMS_API +# include +#endif + + +/* Definitions ****************************************************************/ +#define NUM_IN_OUT_CHANNELS 2 /* always stereo */ + +/* the number of periods is critical for latency */ +#define NUM_PERIOD_BLOCKS_IN 2 +#define NUM_PERIOD_BLOCKS_OUT 2 + +#define MAX_SND_BUF_IN 200 +#define MAX_SND_BUF_OUT 200 + +/* Classes ********************************************************************/ +class CSound +{ +public: + CSound() +#if WITH_SOUND + : rhandle(NULL), phandle(NULL), iCurPeriodSizeIn(NUM_PERIOD_BLOCKS_IN), + iCurPeriodSizeOut(NUM_PERIOD_BLOCKS_OUT) +#endif + {} + virtual ~CSound() {Close();} + + /* Not implemented yet, always return one device and default string */ + int GetNumDev() {return 1;} + void SetOutDev(int iNewDev) {} + void SetInDev(int iNewDev) {} + + /* Return invalid device ID which is the same as using "wave mapper" which + we assume here to be used */ + int GetOutDev() {return 1;} + int GetInDev() {return 1;} + +#if WITH_SOUND + void SetInNumBuf(int iNewNum); + int GetInNumBuf() {return iCurPeriodSizeIn;} + void SetOutNumBuf(int iNewNum); + int GetOutNumBuf() {return iCurPeriodSizeOut;} + void InitRecording(int iNewBufferSize, bool bNewBlocking = true); + void InitPlayback(int iNewBufferSize, bool bNewBlocking = false); + bool Read(CVector& psData); + bool Write(CVector& psData); + + void Close(); + +protected: + snd_pcm_t* rhandle; + snd_pcm_t* phandle; + + bool SetHWParams(snd_pcm_t* handle, const bool bIsRecord, + const int iBufferSizeIn, const int iNumPeriodBlocks); + + int iBufferSizeOut; + int iBufferSizeIn; + bool bChangParamIn; + int iCurPeriodSizeIn; + bool bChangParamOut; + int iCurPeriodSizeOut; +#else + /* Dummy definitions */ + void SetInNumBuf(int iNewNum) {} + int GetInNumBuf() {return 1;} + void SetOutNumBuf(int iNewNum) {} + int GetOutNumBuf() {return 1;} + void InitRecording(int iNewBufferSize, bool bNewBlocking = true) {printf("no sound!");} + void InitPlayback(int iNewBufferSize, bool bNewBlocking = false) {printf("no sound!");} + bool Read(CVector& psData) {printf("no sound!"); return false;} + bool Write(CVector& psData) {printf("no sound!"); return false;} + void Close() {} +#endif +}; + + +#endif // !defined(_SOUND_H__9518A621345F78_3634567_8C0D_EEBF182CF549__INCLUDED_) diff --git a/src/aboutdlgbase.ui b/src/aboutdlgbase.ui new file mode 100755 index 00000000..5e6b2202 --- /dev/null +++ b/src/aboutdlgbase.ui @@ -0,0 +1,281 @@ + +CAboutDlgBase + + QDialog + + name + CAboutDlgBase + + + geometry + + 0 + 0 + 546 + 423 + + + + sizePolicy + + 1 + 1 + + + + caption + About llcon + + + icon + image0 + + + sizeGripEnabled + true + + + layoutMargin + + + layoutSpacing + + + + margin + 11 + + + spacing + 6 + + + QLayoutWidget + + name + Layout21 + + + + margin + 0 + + + spacing + 6 + + + QLabel + + name + PixmapLabelDreamLogo + + + sizePolicy + + 0 + 0 + + + + pixmap + image1 + + + scaledContents + true + + + + QLabel + + name + TextLabelVersion + + + sizePolicy + + 7 + 5 + + + + text + TextLabelVersion + + + alignment + AlignCenter + + + hAlign + + + + + + QLayoutWidget + + name + Layout8 + + + + margin + 0 + + + spacing + 6 + + + QLayoutWidget + + name + Layout16 + + + + margin + 0 + + + spacing + 6 + + + QLabel + + name + TextLabelAuthorNames + + + text + Author: Volker Fischer + + + + QLabel + + name + TextLabelCopyright + + + text + Copyright (C) 2005 - 2006 + + + + + + + name + Spacer10 + + + orientation + Horizontal + + + sizeType + Expanding + + + sizeHint + + 20 + 20 + + + + + + + QTextView + + name + TextViewCredits + + + text + TextViewCredits + + + + QLayoutWidget + + name + Layout6 + + + + margin + 0 + + + spacing + 6 + + + + name + Spacer12 + + + orientation + Horizontal + + + sizeType + Expanding + + + sizeHint + + 20 + 20 + + + + + QPushButton + + name + buttonOk + + + text + &OK + + + autoDefault + true + + + default + true + + + + + + + + + image0 + 789cd3d7528808f055d0d2e72a2e492cc94c5648ce482c52d04a29cdcdad8c8eb5ade6523234530022130543251d2ea5248564056503300071f5205c0b2004719541dcb434986c22840b0260c56800454c9918b1c444e54454b1c4c4a424e5a4c4442431a0085008081231c4949511621021656565b042843a908032bade24a832547b21c6a1ba0f08d0fda18ccd6fd8c2009f58ad351700407358e1 + + + image1 + 789cd3d7528808f055d0d2e72a2e492cc94c5648ce482c52d04a29cdcdad8c8eb5ade6523234530022130543251d2ea5248564056503300071f5205c0b2004719541dcb434986c22840b0260c56800454c9918b1c444e54454b1c4c4a424e5a4c4442431a0085008081231c4949511621021656565b042843a908032bade24a832547b21c6a1ba0f08d0fda18ccd6fd8c2009f58ad351700407358e1 + + + + + buttonOk + clicked() + CAboutDlgBase + accept() + + + diff --git a/src/audiocompr.cpp b/src/audiocompr.cpp new file mode 100755 index 00000000..c90fe72c --- /dev/null +++ b/src/audiocompr.cpp @@ -0,0 +1,255 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer, Erik de Castro Lopo + * + * This code is based on the Open-Source implementation of IMA-ADPCM written + * by Erik de Castro Lopo in 1999-2004 + * + * Changes: + * - only support for one channel + * - put 2 audio samples in header to get even number of audio samples encoded + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "audiocompr.h" + + +/* Implementation *************************************************************/ +int CAudioCompression::Init(const int iNewAudioLen, + const EAudComprType eNewAuCoTy) +{ + eAudComprType = eNewAuCoTy; + + switch (eNewAuCoTy) + { + case CT_NONE: return iCodeSize = 2 * iNewAudioLen; /* short = 2 * byte */ + case CT_IMAADPCM: return ImaAdpcm.Init(iNewAudioLen); + default: return 0; + } +} + +CVector CAudioCompression::Encode(const CVector& vecsAudio) +{ + if (eAudComprType == CT_NONE) + { + /* no compression, simply ship pure samples */ + CVector vecbyOut(iCodeSize); + const int iAudSize = iCodeSize / 2; + + for (int i = 0; i < iAudSize; i++) + { + vecbyOut[2 * i] = vecsAudio[i] & 0xFF; + vecbyOut[2 * i + 1] = (vecsAudio[i] >> 8) & 0xFF; + } + return vecbyOut; + } + else + { + switch (eAudComprType) + { + case CT_IMAADPCM: return ImaAdpcm.Encode(vecsAudio); /* IMA-ADPCM */ + default: return CVector(0); + } + } +} + +CVector CAudioCompression::Decode(const CVector& vecbyAdpcm) +{ + if (eAudComprType == CT_NONE) + { + /* no compression, reassemble pure samples */ + const int iAudSize = iCodeSize / 2; + CVector vecsOut(iAudSize); + + for (int i = 0; i < iAudSize; i++) + { + int current = vecbyAdpcm[2 * i] | (vecbyAdpcm[2 * i + 1] << 8); + if (current & 0x8000) + current -= 0x10000; + + vecsOut[i] = (short) current; + } + return vecsOut; + } + else + { + switch (eAudComprType) + { + case CT_IMAADPCM: return ImaAdpcm.Decode(vecbyAdpcm); /* IMA-ADPCM */ + default: return CVector(0); + } + } +} + + +/* IMA-ADPCM implementation ------------------------------------------------- */ +int CImaAdpcm::Init(const int iNewAudioLen) +{ + /* set lengths for audio and compressed data */ + iAudSize = iNewAudioLen; + iAdpcmSize = 4 /* bytes header */ + (int) ceil( + (double) (iAudSize - 2 /* first two samples are in header */) / 2); + + iStepindEnc = 0; + + return iAdpcmSize; +} + +CVector CImaAdpcm::Encode(const CVector& vecsAudio) +{ + int i; + CVector vecbyAdpcm; + CVector vecbyAdpcmTemp; + + /* init size */ + vecbyAdpcm.Init(iAdpcmSize); + vecbyAdpcmTemp.Init(iAudSize); + + /* encode the block header ----------------------------------------------- */ + vecbyAdpcm[0] = vecsAudio[0] & 0xFF; + vecbyAdpcm[1] = (vecsAudio[0] >> 8) & 0xFF; + vecbyAdpcm[2] = iStepindEnc; + + int iPrevAudio = vecsAudio[0]; + + + /* encode the samples as 4 bit ------------------------------------------- */ + for (i = 1; i < iAudSize; i++) + { + /* init diff and step */ + int diff = vecsAudio[i] - iPrevAudio; + int step = ima_step_size[iStepindEnc]; + + short bytecode = 0; + + int vpdiff = step >> 3; + if (diff < 0) + { + bytecode = 8; + diff = -diff; + } + short mask = 4; + while (mask) + { + if (diff >= step) + { + bytecode |= mask; + diff -= step; + vpdiff += step; + } + step >>= 1; + mask >>= 1; + } + + if (bytecode & 8) + iPrevAudio -= vpdiff; + else + iPrevAudio += vpdiff; + + /* adjust step size */ + iStepindEnc += ima_indx_adjust[bytecode]; + + /* check that values do not exceed the bounds */ + iPrevAudio = CheckBounds(iPrevAudio, _MINSHORT, _MAXSHORT); + iStepindEnc = CheckBounds(iStepindEnc, 0, IMA_STEP_SIZE_TAB_LEN - 1); + + /* use the input buffer as an intermediate result buffer */ + vecbyAdpcmTemp[i] = bytecode; + } + + + /* pack the 4 bit encoded samples ---------------------------------------- */ + /* The first encoded audio sample is in header */ + vecbyAdpcm[3] = vecbyAdpcmTemp[1] & 0x0F; + + for (i = 4; i < iAdpcmSize; i++) + { + vecbyAdpcm[i] = vecbyAdpcmTemp[2 * i - 6] & 0x0F; + vecbyAdpcm[i] |= (vecbyAdpcmTemp[2 * i - 5] << 4) & 0xF0; + } + + return vecbyAdpcm; +} + +CVector CImaAdpcm::Decode(const CVector& vecbyAdpcm) +{ + int i; + CVector vecsAudio; + + vecsAudio.Init(iAudSize); + + + /* read and check the block header --------------------------------------- */ + int current = vecbyAdpcm[0] | (vecbyAdpcm[1] << 8); + if (current & 0x8000) + current -= 0x10000; + + /* get and bound step index */ + int iStepindDec = CheckBounds(vecbyAdpcm[2], 0, IMA_STEP_SIZE_TAB_LEN - 1); + + /* set first sample which was delivered in the header */ + vecsAudio[0] = current; + + + /* -------------------------------------------------------------------------- + pull apart the packed 4 bit samples and store them in their correct sample + positions */ + /* The first encoded audio sample is in header */ + vecsAudio[1] = vecbyAdpcm[3] & 0x0F; + + for (i = 4; i < iAdpcmSize; i++) + { + const short bytecode = vecbyAdpcm[i]; + vecsAudio[2 * i - 6] = bytecode & 0x0F; + vecsAudio[2 * i - 5] = (bytecode >> 4) & 0x0F; + } + + + /* decode the encoded 4 bit samples -------------------------------------- */ + for (i = 1; i < iAudSize; i++) + { + const short bytecode = vecsAudio[i] & 0xF ; + + short step = ima_step_size[iStepindDec]; + int current = vecsAudio[i - 1]; + + int diff = step >> 3; + if (bytecode & 1) + diff += step >> 2; + if (bytecode & 2) + diff += step >> 1; + if (bytecode & 4) + diff += step; + if (bytecode & 8) + diff = -diff; + + current += diff; + iStepindDec += ima_indx_adjust[bytecode]; + + /* check that values do not exceed the bounds */ + current = CheckBounds(current, _MINSHORT, _MAXSHORT); + iStepindDec = CheckBounds(iStepindDec, 0, IMA_STEP_SIZE_TAB_LEN - 1); + + vecsAudio[i] = current; + } + + return vecsAudio; +} diff --git a/src/audiocompr.h b/src/audiocompr.h new file mode 100755 index 00000000..8117c846 --- /dev/null +++ b/src/audiocompr.h @@ -0,0 +1,104 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer, Erik de Castro Lopo + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(AUDIOCOMPR_H_OIHGE76GEKJH3249_GEG98EG3_43441912__INCLUDED_) +#define AUDIOCOMPR_H_OIHGE76GEKJH3249_GEG98EG3_43441912__INCLUDED_ + +#include "util.h" +#include "global.h" +#include "buffer.h" + + +/* Definitions ****************************************************************/ +/* tables */ +static int ima_indx_adjust[16] = +{ -1, -1, -1, -1, /* +0 - +3, decrease the step size */ + 2, 4, 6, 8, /* +4 - +7, increase the step size */ + -1, -1, -1, -1, /* -0 - -3, decrease the step size */ + 2, 4, 6, 8, /* -4 - -7, increase the step size */ +}; + +#define IMA_STEP_SIZE_TAB_LEN 89 +static int ima_step_size[IMA_STEP_SIZE_TAB_LEN] = +{ 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, + 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, + 253, 279, 307, 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, + 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, 3327, + 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, + 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 +}; + + +/* Classes ********************************************************************/ +/* IMA-ADPCM ---------------------------------------------------------------- */ +class CImaAdpcm +{ +public: + CImaAdpcm() : iStepindEnc(0) {} + virtual ~CImaAdpcm() {} + + int Init(const int iNewAudioLen); + CVector Encode(const CVector& vecsAudio); + CVector Decode(const CVector& vecbyAdpcm); + +protected: + int iAudSize; + int iAdpcmSize; + int iStepindEnc; + + /* inline functions must be declared in the header */ + inline int CheckBounds(const int iData, const int iMin, const int iMax) + { + if (iData > iMax) + return iMax; + if (iData < iMin) + return iMin; + + return iData; + } +}; + + +/* Audio compression class -------------------------------------------------- */ +class CAudioCompression +{ +public: + enum EAudComprType {CT_NONE, CT_IMAADPCM}; + + CAudioCompression() {} + virtual ~CAudioCompression() {} + + int Init(const int iNewAudioLen, const EAudComprType eNewAuCoTy); + CVector Encode(const CVector& vecsAudio); + CVector Decode(const CVector& vecbyAdpcm); + +protected: + EAudComprType eAudComprType; + CImaAdpcm ImaAdpcm; + int iCodeSize; +}; + + +#endif /* !defined(AUDIOCOMPR_H_OIHGE76GEKJH3249_GEG98EG3_43441912__INCLUDED_) */ diff --git a/src/buffer.cpp b/src/buffer.cpp new file mode 100755 index 00000000..57d16fd8 --- /dev/null +++ b/src/buffer.cpp @@ -0,0 +1,381 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + * Note: we assuming here that put and get operations are secured by a mutex + * and do not take place at the same time + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "buffer.h" + + +/* Implementation *************************************************************/ +void CNetBuf::Init ( const int iNewBlockSize, const int iNewNumBlocks ) +{ + /* total size -> size of one block times number of blocks */ + iBlockSize = iNewBlockSize; + iMemSize = iNewBlockSize * iNewNumBlocks; + + /* fade in first block added to the buffer */ + bFadeInNewPutData = true; + + /* allocate and clear memory for actual data buffer */ + vecdMemory.Init(iMemSize); + + /* use the "get" flag to make sure the buffer is cleared */ + Clear(CT_GET); + + /* initialize number of samples for fading effect */ + if (FADE_IN_OUT_NUM_SAM < iBlockSize) + iNumSamFading = iBlockSize; + else + iNumSamFading = FADE_IN_OUT_NUM_SAM; + + if (FADE_IN_OUT_NUM_SAM_EXTRA > iBlockSize) + iNumSamFadingExtra = iBlockSize; + else + iNumSamFadingExtra = FADE_IN_OUT_NUM_SAM; + + /* init variables for extrapolation (in case a fade out is needed) */ + dExPDiff = 0.0; + dExPLastV = 0.0; +} + +bool CNetBuf::Put(CVector& vecdData) +{ +#ifdef _DEBUG_ +static FILE* pFileBI = fopen("bufferin.dat", "w"); +fprintf(pFileBI, "%d %d\n", GetAvailSpace() / iBlockSize, iMemSize / iBlockSize); +fflush(pFileBI); +#endif + + bool bPutOK = true; + + /* get size of data to be added to the buffer */ + const int iInSize = vecdData.Size(); + + /* Check if there is not enough space available -> correct */ + if (GetAvailSpace() < iInSize) + { + /* not enough space in buffer for put operation, correct buffer to + prepare for new data */ + Clear(CT_PUT); + + /* set flag to fade in new block to avoid clicks */ + bFadeInNewPutData = true; + + bPutOK = false; /* return error flag */ + } + + /* fade in new block if required */ + if (bFadeInNewPutData) + FadeInAudioDataBlock(vecdData); + + /* copy new data in internal buffer */ + int iCurPos = 0; + if (iPutPos + iInSize > iMemSize) + { + /* remaining space size for second block */ + const int iRemSpace = iPutPos + iInSize - iMemSize; + + /* data must be written in two steps because of wrap around */ + while (iPutPos < iMemSize) + vecdMemory[iPutPos++] = vecdData[iCurPos++]; + + for (iPutPos = 0; iPutPos < iRemSpace; iPutPos++) + vecdMemory[iPutPos] = vecdData[iCurPos++]; + } + else + { + /* data can be written in one step */ + const int iEnd = iPutPos + iInSize; + while (iPutPos < iEnd) + vecdMemory[iPutPos++] = vecdData[iCurPos++]; + } + + /* set buffer state flag */ + if (iPutPos == iGetPos) + eBufState = CNetBuf::BS_FULL; + else + eBufState = CNetBuf::BS_OK; + + return bPutOK; +} + +bool CNetBuf::Get(CVector& vecdData) +{ + bool bGetOK = true; /* init return value */ + bool bFadeOutExtrap = false; + + /* get size of data to be get from the buffer */ + const int iInSize = vecdData.Size(); + + /* Check if there is not enough data available -> correct */ + if (GetAvailData() < iInSize) + { + /* not enough data in buffer for get operation, correct buffer to + prepare for getting data */ + Clear(CT_GET); + + /* set flag to fade in next new block in buffer and fade out last + block by extrapolation to avoid clicks */ + bFadeInNewPutData = true; + bFadeOutExtrap = true; + + bGetOK = false; /* return error flag */ + } + + /* copy data from internal buffer in output buffer */ + int iCurPos = 0; + if (iGetPos + iInSize > iMemSize) + { + /* remaining data size for second block */ + const int iRemData = iGetPos + iInSize - iMemSize; + + /* data must be read in two steps because of wrap around */ + while (iGetPos < iMemSize) + vecdData[iCurPos++] = vecdMemory[iGetPos++]; + + for (iGetPos = 0; iGetPos < iRemData; iGetPos++) + vecdData[iCurPos++] = vecdMemory[iGetPos]; + } + else + { + /* data can be read in one step */ + const int iEnd = iGetPos + iInSize; + while (iGetPos < iEnd) + vecdData[iCurPos++] = vecdMemory[iGetPos++]; + } + + /* set buffer state flag */ + if (iPutPos == iGetPos) + eBufState = CNetBuf::BS_EMPTY; + else + eBufState = CNetBuf::BS_OK; + + + /* extrapolate data from old block to avoid "clicks" + we have to do this method since we cannot fade out the old block + anymore since it is already gone (processed or send through the + network) */ + if (bFadeOutExtrap) + FadeOutExtrapolateAudioDataBlock(vecdData, dExPDiff, dExPLastV); + + /* save some paramters from last block which is needed in case we do not + have enough data for next "get" operation and need to extrapolate the + signal to avoid "clicks" + we assume here that "iBlockSize" is larger than 1! */ + dExPDiff = vecdData[iInSize - 1] - vecdData[iInSize - 2]; + dExPLastV = vecdData[iInSize - 1]; + + return bGetOK; +} + +int CNetBuf::GetAvailSpace() const +{ + /* calculate available space in buffer */ + int iAvSpace = iGetPos - iPutPos; + + /* check for special case and wrap around */ + if (iAvSpace < 0) + iAvSpace += iMemSize; /* wrap around */ + else if ((iAvSpace == 0) && (eBufState == BS_EMPTY)) + iAvSpace = iMemSize; + + return iAvSpace; +} + +int CNetBuf::GetAvailData() const +{ + /* calculate available data in buffer */ + int iAvData = iPutPos - iGetPos; + + /* check for special case and wrap around */ + if (iAvData < 0) + iAvData += iMemSize; /* wrap around */ + else if ((iAvData == 0) && (eBufState == BS_FULL)) + iAvData = iMemSize; + + return iAvData; +} + +void CNetBuf::Clear(const EClearType eClearType) +{ + + int iMiddleOfBuffer; + +#if 0 + /* with the following operation we set the new get pos to a block + boundary (one block below the middle of the buffer in case of odd + number of blocks, e.g.: + [buffer size]: [get pos] + 1: 0 / 2: 0 / 3: 1 / 4: 1 / ... */ + iMiddleOfBuffer = (((iMemSize - iBlockSize) / 2) / iBlockSize) * iBlockSize; +#else +// old code + +// somehow the old code seems to work better than the sophisticated new one....? + /* 1: 0 / 2: 1 / 3: 1 / 4: 2 / ... */ + iMiddleOfBuffer = ((iMemSize / 2) / iBlockSize) * iBlockSize; +#endif + + + /* different behaviour for get and put corrections */ + if (eClearType == CT_GET) + { + /* clear buffer */ + vecdMemory.Reset(0.0); + + /* correct buffer so that after the current get operation the pointer + are at maximum distance */ + iPutPos = 0; + iGetPos = iMiddleOfBuffer; + + /* check for special case */ + if (iPutPos == iGetPos) + eBufState = CNetBuf::BS_FULL; + else + eBufState = CNetBuf::BS_OK; + } + else + { + /* in case of "put" correction, do not delete old data but only shift + the pointers */ + iPutPos = iMiddleOfBuffer; + + /* adjust put pointer relative to current get pointer, take care of + wrap around */ + iPutPos += iGetPos; + if (iPutPos > iMemSize) + iPutPos -= iMemSize; + + /* fade out old data right before new put pointer */ + int iCurPos = iPutPos - iNumSamFading; + int i = iNumSamFading; + + if (iCurPos < 0) + { + /* wrap around */ + iCurPos += iMemSize; + + /* data must be processed in two steps because of wrap around */ + while (iCurPos < iMemSize) + { + vecdMemory[iCurPos++] *= ((double) i / iNumSamFading); + i--; + } + + for (iCurPos = 0; iCurPos < iPutPos; iCurPos++) + { + vecdMemory[iCurPos] *= ((double) i / iNumSamFading); + i--; + } + } + else + { + /* data can be processed in one step */ + while (iCurPos < iPutPos) + { + vecdMemory[iCurPos++] *= ((double) i / iNumSamFading); + i--; + } + } + + /* check for special case */ + if (iPutPos == iGetPos) + { + eBufState = CNetBuf::BS_EMPTY; + } + else + { + eBufState = CNetBuf::BS_OK; + } + } +} + +void CNetBuf::FadeInAudioDataBlock(CVector& vecdData) +{ + /* apply linear fading */ + for (int i = 0; i < iNumSamFading; i++) + { + vecdData[i] *= ((double) i / iNumSamFading); + } + + /* reset flag */ + bFadeInNewPutData = false; +} + +void CNetBuf::FadeOutExtrapolateAudioDataBlock(CVector& vecdData, + const double dExPDiff, + const double dExPLastV) +{ + /* apply linear extrapolation and linear fading */ + for (int i = 0; i < iNumSamFadingExtra; i++) + { + /* calculate extrapolated value */ + vecdData[i] = ((i + 1) * dExPDiff + dExPLastV); + + /* linear fading */ + vecdData[i] *= ((double) (iNumSamFadingExtra - i) / iNumSamFadingExtra); + } +} + + + +/* conversion buffer implementation *******************************************/ +void CConvBuf::Init ( const int iNewMemSize ) +{ + /* set memory size */ + iMemSize = iNewMemSize; + + /* allocate and clear memory for actual data buffer */ + vecsMemory.Init(iMemSize); + + iPutPos = 0; +} + +bool CConvBuf::Put ( const CVector& vecsData) +{ + const int iVecSize = vecsData.Size(); + + /* copy new data in internal buffer */ + int iCurPos = 0; + const int iEnd = iPutPos + iVecSize; + while ( iPutPos < iEnd ) + { + vecsMemory[iPutPos++] = vecsData[iCurPos++]; + } + + bool bBufOk = false; + if ( iEnd == iMemSize ) + { + bBufOk = true; + } + + return bBufOk; +} + +CVector CConvBuf::Get() +{ + iPutPos = 0; + return vecsMemory; +} diff --git a/src/buffer.h b/src/buffer.h new file mode 100755 index 00000000..7d4c52f6 --- /dev/null +++ b/src/buffer.h @@ -0,0 +1,100 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(BUFFER_H__3B123453_4344_BB23945IUHF1912__INCLUDED_) +#define BUFFER_H__3B123453_4344_BB23945IUHF1912__INCLUDED_ + +#include "util.h" +#include "global.h" + + +/* Definitions ****************************************************************/ +/* time for fading effect for masking drop outs */ +#define FADE_IN_OUT_TIME ((double) 0.3) /* ms */ +#define FADE_IN_OUT_NUM_SAM ((int) (SAMPLE_RATE * FADE_IN_OUT_TIME) / 1000) + +/* for extrapolation a shorter time for fading */ +#define FADE_IN_OUT_NUM_SAM_EXTRA 5 /* samples */ + + +/* Classes ********************************************************************/ +class CNetBuf +{ +public: + CNetBuf() {} + virtual ~CNetBuf() {} + + void Init(const int iNewBlockSize, const int iNewNumBlocks); + int GetSize() {return iMemSize / iBlockSize;} + + bool Put(CVector& vecdData); + bool Get(CVector& vecdData); + +protected: + enum EBufState {BS_OK, BS_FULL, BS_EMPTY}; + enum EClearType {CT_PUT, CT_GET}; + void Clear(const EClearType eClearType); + int GetAvailSpace() const; + int GetAvailData() const; + void FadeInAudioDataBlock(CVector& vecdData); + void FadeOutExtrapolateAudioDataBlock(CVector& vecdData, + const double dExPDiff, const double dExPLastV); + + CVector vecdMemory; + int iMemSize; + int iBlockSize; + int iGetPos, iPutPos; + EBufState eBufState; + bool bFadeInNewPutData; + int iNumSamFading; + int iNumSamFadingExtra; + + /* extrapolation parameters */ + double dExPDiff; + double dExPLastV; +}; + + +/* conversion buffer (very simple buffer) */ +class CConvBuf +{ +public: + CConvBuf () {} + virtual ~CConvBuf () {} + + void Init ( const int iNewMemSize ); + int GetSize() { return iMemSize; } + + bool Put ( const CVector& vecsData ); + CVector Get (); + +protected: + CVector vecsMemory; + int iMemSize; + int iBlockSize; + int iPutPos; +}; + + +#endif /* !defined(BUFFER_H__3B123453_4344_BB23945IUHF1912__INCLUDED_) */ diff --git a/src/channel.cpp b/src/channel.cpp new file mode 100755 index 00000000..17790821 --- /dev/null +++ b/src/channel.cpp @@ -0,0 +1,448 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "channel.h" + + +/******************************************************************************\ +* CChannelSet * +\******************************************************************************/ +int CChannelSet::GetFreeChan() +{ + /* look for a free channel */ + for (int i = 0; i < MAX_NUM_CHANNELS; i++) + { + if (!vecChannels[i].IsConnected()) + return i; + } + + /* no free channel found, return invalid ID */ + return INVALID_CHANNEL_ID; +} + +int CChannelSet::CheckAddr(const CHostAddress& Addr) +{ + CHostAddress InetAddr; + + /* Check for all possible channels if IP is already in use */ + for (int i = 0; i < MAX_NUM_CHANNELS; i++) + { + if (vecChannels[i].GetAddress(InetAddr)) + { + /* IP found, return channel number */ + if (InetAddr == Addr) + return i; + } + } + + /* IP not found, return invalid ID */ + return INVALID_CHANNEL_ID; +} + +bool CChannelSet::PutData(const CVector& vecbyRecBuf, + const int iNumBytesRead, const CHostAddress& HostAdr) +{ + Mutex.lock(); + + /* get channel ID ------------------------------------------------------- */ + bool bChanOK = true; + + /* check address */ + int iCurChanID = CheckAddr(HostAdr); + + if (iCurChanID == INVALID_CHANNEL_ID) + { + /* a new client is calling, look for free channel */ + iCurChanID = GetFreeChan(); + + if (iCurChanID != INVALID_CHANNEL_ID) + vecChannels[iCurChanID].SetAddress(HostAdr); + else + bChanOK = false; /* no free channel available */ + } + + + /* put received data in jitter buffer ----------------------------------- */ + if (bChanOK) + { + /* put packet in socket buffer */ + if (vecChannels[iCurChanID].PutData(vecbyRecBuf, iNumBytesRead)) + PostWinMessage(MS_JIT_BUF_PUT, MUL_COL_LED_GREEN, iCurChanID); + else + PostWinMessage(MS_JIT_BUF_PUT, MUL_COL_LED_RED, iCurChanID); + } + + Mutex.unlock(); + + return !bChanOK; /* return 1 if error */ +} + +void CChannelSet::GetBlockAllConC(CVector& vecChanID, + CVector >& vecvecdData) +{ + /* init temporal data vector and clear input buffers */ + CVector vecdData(BLOCK_SIZE_SAMPLES); + + vecChanID.Init(0); + vecvecdData.Init(0); + + /* make put and get calls thread safe. Do not forget to unlock mutex + afterwards! */ + Mutex.lock(); + + /* Check all possible channels */ + for (int i = 0; i < MAX_NUM_CHANNELS; i++) + { + /* read out all input buffers to decrease timeout counter on + disconnected channels */ + bool bGetOK = vecChannels[i].GetData(vecdData); + + if (vecChannels[i].IsConnected()) + { + /* add ID and data */ + vecChanID.Add(i); + + const int iOldSize = vecvecdData.Size(); + vecvecdData.Enlarge(1); + vecvecdData[iOldSize].Init(vecdData.Size()); + vecvecdData[iOldSize] = vecdData; + + /* send message for get status (for GUI) */ + if (bGetOK) + PostWinMessage(MS_JIT_BUF_GET, MUL_COL_LED_GREEN, i); + else + PostWinMessage(MS_JIT_BUF_GET, MUL_COL_LED_RED, i); + } + } + + Mutex.unlock(); /* release mutex */ +} + +void CChannelSet::GetConCliParam(CVector& vecHostAddresses, + CVector& vecdSamOffs) +{ + CHostAddress InetAddr; + + /* init return values */ + vecHostAddresses.Init(MAX_NUM_CHANNELS); + vecdSamOffs.Init(MAX_NUM_CHANNELS); + + /* Check all possible channels */ + for (int i = 0; i < MAX_NUM_CHANNELS; i++) + { + if (vecChannels[i].GetAddress(InetAddr)) + { + /* add new address and sample rate offset to vectors */ + vecHostAddresses[i] = InetAddr; + vecdSamOffs[i] = vecChannels[i].GetResampleOffset(); + } + } +} + +void CChannelSet::SetSockBufSize ( const int iNewBlockSize, const int iNumBlocks ) +{ + /* this opperation must be done with mutex */ + Mutex.lock (); + +/* as a test we adjust the buffers of all channels to the new value. Maybe later + do change only for some channels -> take care to set value back to default if + channel is disconnected, afterwards! */ +for ( int i = 0; i < MAX_NUM_CHANNELS; i++ ) + vecChannels[i].SetSockBufSize ( iNewBlockSize, iNumBlocks ); + + Mutex.unlock (); +} + + +/******************************************************************************\ +* CChannel * +\******************************************************************************/ +CChannel::CChannel() +{ + /* init time stamp activation counter */ + iTimeStampActCnt = NUM_BL_TIME_STAMPS; + + /* init time stamp index counter */ + byTimeStampIdxCnt = 0; + + /* init the socket buffer */ + SetSockBufSize ( BLOCK_SIZE_SAMPLES, DEF_NET_BUF_SIZE_NUM_BL ); + + /* init conversion buffer */ + ConvBuf.Init ( BLOCK_SIZE_SAMPLES ); + + /* init audio compression unit */ + iAudComprSize = AudioCompression.Init ( BLOCK_SIZE_SAMPLES, + CAudioCompression::CT_IMAADPCM ); + + /* init time-out for the buffer with zero -> no connection */ + iConTimeOut = 0; + + /* init sample rate offset estimation object */ + SampleOffsetEst.Init(); +} + +void CChannel::SetSockBufSize ( const int iNewBlockSize, const int iNumBlocks ) +{ + /* this opperation must be done with mutex */ + Mutex.lock (); + + SockBuf.Init ( iNewBlockSize, iNumBlocks ); + + Mutex.unlock (); +} + +int CChannel::GetTimeStampIdx () +{ + /* only send time stamp index after a pre-defined number of packets */ + if ( iTimeStampActCnt > 0 ) + { + iTimeStampActCnt--; + return INVALID_TIME_STAMP_IDX; + } + else + { + /* reset time stamp activation counter */ + iTimeStampActCnt = NUM_BL_TIME_STAMPS - 1; + + /* wraps around automatically */ + byTimeStampIdxCnt++; + return byTimeStampIdxCnt; + } +} + +bool CChannel::GetAddress(CHostAddress& RetAddr) +{ + if (IsConnected()) + { + RetAddr = InetAddr; + return true; + } + else + { + RetAddr = CHostAddress(); + return false; + } +} + +bool CChannel::PutData(const CVector& vecbyData, + int iNumBytes) +{ + bool bRet = true; + + Mutex.lock(); /* put mutex lock */ + + /* only process if packet has correct size */ + if (iNumBytes == iAudComprSize) + { + /* decompress audio */ + CVector vecsDecomprAudio(BLOCK_SIZE_SAMPLES); + vecsDecomprAudio = AudioCompression.Decode(vecbyData); + + /* do resampling to compensate for sample rate offsets in the + different sound cards of the clients */ +// we should not do resampling here since we already have resampling +// in the client audio path, we could use this resampling for this +// sample rate correction, too +/* +for (int i = 0; i < BLOCK_SIZE_SAMPLES; i++) + vecdResInData[i] = (double) vecsData[i]; + +const int iInSize = ResampleObj.Resample(vecdResInData, vecdResOutData, + (double) SAMPLE_RATE / (SAMPLE_RATE - dSamRateOffset)); +*/ + +vecdResOutData.Init(BLOCK_SIZE_SAMPLES); +for (int i = 0; i < BLOCK_SIZE_SAMPLES; i++) + vecdResOutData[i] = (double) vecsDecomprAudio[i]; + + + bRet = SockBuf.Put(vecdResOutData); + + /* reset time-out counter */ + iConTimeOut = CON_TIME_OUT_CNT_MAX; + } + else if (iNumBytes == 1) + { + /* time stamp packet */ + SampleOffsetEst.AddTimeStampIdx(vecbyData[0]); + } + else + bRet = false; /* wrong packet size */ + + Mutex.unlock(); /* put mutex unlock */ + + return bRet; +} + +bool CChannel::GetData(CVector& vecdData) +{ + Mutex.lock(); /* get mutex lock */ + + const bool bGetOK = SockBuf.Get(vecdData); + + if (!bGetOK) + { + /* decrease time-out counter */ + if (iConTimeOut > 0) + { + iConTimeOut--; + + /* if time out is reached, re-init resample offset estimation + module */ + if (iConTimeOut == 0) + SampleOffsetEst.Init(); + } + } + + Mutex.unlock(); /* get mutex unlock */ + + return bGetOK; +} + +CVector CChannel::PrepSendPacket(const CVector& vecsNPacket) +{ + /* if the block is not ready we have to initialize with zero length to + tell the following network send routine that nothing should be sent */ + CVector vecbySendBuf ( 0 ); + + /* use conversion buffer to convert sound card block size in network + block size */ + if ( ConvBuf.Put ( vecsNPacket ) ) + { + /* a packet is ready, compress audio */ + vecbySendBuf.Init ( iAudComprSize ); + vecbySendBuf = AudioCompression.Encode ( ConvBuf.Get () ); + } + + return vecbySendBuf; +} + + + +/******************************************************************************\ +* CSampleOffsetEst * +\******************************************************************************/ +void CSampleOffsetEst::Init() +{ + /* init sample rate estimation */ + dSamRateEst = SAMPLE_RATE; + + /* init vectors storing the data */ + veciTimeElapsed.Init(VEC_LEN_SAM_OFFS_EST); + veciTiStIdx.Init(VEC_LEN_SAM_OFFS_EST); + + /* start reference time (the counter wraps to zero 24 hours after the last + call to start() or restart, but this should not concern us since this + software will most probably not be used that long) */ + RefTime.start(); + + /* init accumulated time stamp variable */ + iAccTiStVal = 0; + + /* init count (do not ship any result in init phase) */ + iInitCnt = VEC_LEN_SAM_OFFS_EST + 1; +} + +void CSampleOffsetEst::AddTimeStampIdx(const int iTimeStampIdx) +{ + int i; + + const int iLastIdx = VEC_LEN_SAM_OFFS_EST - 1; + + /* take care of wrap of the time stamp index (byte wrap) */ + if (iTimeStampIdx < veciTiStIdx[iLastIdx] - iAccTiStVal) + iAccTiStVal += _MAXBYTE + 1; + + /* add new data pair to the FIFO */ + for (i = 1; i < VEC_LEN_SAM_OFFS_EST; i++) + { + /* move old data */ + veciTimeElapsed[i - 1] = veciTimeElapsed[i]; + veciTiStIdx[i - 1] = veciTiStIdx[i]; + } + + /* add new data */ + veciTimeElapsed[iLastIdx] = RefTime.elapsed(); + veciTiStIdx[iLastIdx] = iAccTiStVal + iTimeStampIdx; + +/* +static FILE* pFile = fopen("v.dat", "w"); +for (i = 0; i < VEC_LEN_SAM_OFFS_EST; i++) + fprintf(pFile, "%d\n", veciTimeElapsed[i]); +fflush(pFile); +*/ + + + + + /* calculate linear regression for sample rate estimation */ + /* first, calculate averages */ + double dTimeAv = 0; + double dTiStAv = 0; + for (i = 0; i < VEC_LEN_SAM_OFFS_EST; i++) + { + dTimeAv += veciTimeElapsed[i]; + dTiStAv += veciTiStIdx[i]; + } + dTimeAv /= VEC_LEN_SAM_OFFS_EST; + dTiStAv /= VEC_LEN_SAM_OFFS_EST; + + /* calculate gradient */ + double dNom = 0; + double dDenom = 0; + for (i = 0; i < VEC_LEN_SAM_OFFS_EST; i++) + { + const double dCurTimeNoAv = veciTimeElapsed[i] - dTimeAv; + dNom += dCurTimeNoAv * (veciTiStIdx[i] - dTiStAv); + dDenom += dCurTimeNoAv * dCurTimeNoAv; + } + + /* final sample rate offset estimation calculation */ + if (iInitCnt > 0) + iInitCnt--; + else + { + dSamRateEst = dNom / dDenom * NUM_BL_TIME_STAMPS * BLOCK_SIZE_SAMPLES * 1000; + +/* +static FILE* pFile = fopen("v.dat", "w"); +for (i = 0; i < VEC_LEN_SAM_OFFS_EST; i++) + fprintf(pFile, "%d %d\n", veciTimeElapsed[i], veciTiStIdx[i]); +fflush(pFile); +*/ + } + + + +/* +static FILE* pFile = fopen("v.dat", "w"); +fprintf(pFile, "%e\n", dSamRateEst); +fflush(pFile); +*/ + + + + +} diff --git a/src/channel.h b/src/channel.h new file mode 100755 index 00000000..e535c95f --- /dev/null +++ b/src/channel.h @@ -0,0 +1,169 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(CHANNEL_HOIH9345KJH98_3_4344_BB23945IUHF1912__INCLUDED_) +#define CHANNEL_HOIH9345KJH98_3_4344_BB23945IUHF1912__INCLUDED_ + +#include +#include "global.h" +#include "buffer.h" +#include "audiocompr.h" +#include "util.h" +#include "resample.h" +#include "qdatetime.h" + + +/* Definitions ****************************************************************/ +/* Set the time-out for the input buffer until the state changes from + connected to not-connected (the actual time depends on the way the error + correction is implemented) */ +#define CON_TIME_OUT_CNT_MAX 50 + +/* maximum number of internet connections (channels) */ +#define MAX_NUM_CHANNELS 10 /* max number channels for server */ + +/* no valid channel number */ +#define INVALID_CHANNEL_ID (MAX_NUM_CHANNELS + 1) + +/* no valid time stamp index */ +#define INVALID_TIME_STAMP_IDX -1 + + +/* Classes ********************************************************************/ +class CSampleOffsetEst +{ +public: + CSampleOffsetEst() {Init();} + virtual ~CSampleOffsetEst() {} + + void Init(); + void AddTimeStampIdx(const int iTimeStampIdx); + double GetSamRate() {return dSamRateEst;} + +protected: + QTime RefTime; + int iAccTiStVal; + double dSamRateEst; + CVector veciTimeElapsed; + CVector veciTiStIdx; + int iInitCnt; +}; + + +/* CChannel ----------------------------------------------------------------- */ +class CChannel +{ +public: + CChannel(); + virtual ~CChannel() {} + + bool PutData(const CVector& vecbyData, + int iNumBytes); + bool GetData(CVector& vecdData); + + CVector PrepSendPacket(const CVector& vecsNPacket); + + bool GetAddress(CHostAddress& RetAddr); + CHostAddress GetAddress() {return InetAddr;} + int GetTimeStampIdx(); + void SetAddress(const CHostAddress NAddr) {InetAddr = NAddr;} + bool IsConnected() const {return iConTimeOut > 0;} + int GetComprAudSize() {return iAudComprSize;} + double GetResampleOffset() {return SampleOffsetEst.GetSamRate();} + void SetSockBufSize ( const int iNewBlockSize, const int iNumBlocks ); + int GetSockBufSize() {return SockBuf.GetSize();} + +protected: + /* audio compression */ + CAudioCompression AudioCompression; + int iAudComprSize; + + /* resampling */ + CResample ResampleObj; + double dSamRateOffset; + CVector vecdResInData; + CVector vecdResOutData; + + CSampleOffsetEst SampleOffsetEst; + + /* connection parameters */ + CHostAddress InetAddr; + + /* network jitter-buffer */ + CNetBuf SockBuf; + + /* network output conversion buffer */ + CConvBuf ConvBuf; + + /* time stamp index counter */ + Q_UINT8 byTimeStampIdxCnt; + int iTimeStampActCnt; + + int iConTimeOut; + + QMutex Mutex; +}; + + +/* CChannelSet (for server) ------------------------------------------------- */ +class CChannelSet +{ +public: + CChannelSet() {} + virtual ~CChannelSet() {} + + bool PutData(const CVector& vecbyRecBuf, + const int iNumBytesRead, const CHostAddress& HostAdr); + + int GetFreeChan(); + int CheckAddr(const CHostAddress& Addr); + + void GetBlockAllConC(CVector& vecChanID, + CVector >& vecvecdData); + void GetConCliParam(CVector& vecHostAddresses, + CVector& vecdSamOffs); + + /* access functions for actual channels */ + bool IsConnected(const int iChanNum) + {return vecChannels[iChanNum].IsConnected();} + CVector PrepSendPacket(const int iChanNum, + const CVector& vecsNPacket) + {return vecChannels[iChanNum].PrepSendPacket(vecsNPacket);} + CHostAddress GetAddress(const int iChanNum) + {return vecChannels[iChanNum].GetAddress();} + int GetTimeStampIdx(const int iChanNum) + {return vecChannels[iChanNum].GetTimeStampIdx();} + + void SetSockBufSize ( const int iNewBlockSize, const int iNumBlocks); + int GetSockBufSize() {return vecChannels[0].GetSockBufSize();} + +protected: + /* do not use the vector class since CChannel does not have appropriate + copy constructor/operator */ + CChannel vecChannels[MAX_NUM_CHANNELS]; + QMutex Mutex; +}; + + +#endif /* !defined(CHANNEL_HOIH9345KJH98_3_4344_BB23945IUHF1912__INCLUDED_) */ diff --git a/src/client.cpp b/src/client.cpp new file mode 100755 index 00000000..0d2dceb8 --- /dev/null +++ b/src/client.cpp @@ -0,0 +1,317 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "client.h" + + +/* Implementation *************************************************************/ +bool CClient::SetServerAddr(QString strNAddr) +{ + QHostAddress InetAddr; + + if (InetAddr.setAddress(strNAddr)) + { + /* The server port is fixed and always the same */ + Channel.SetAddress(CHostAddress(InetAddr, LLCON_PORT_NUMBER)); + + return true; + } + else + return false; /* invalid address */ +} + +void CClient::Init() +{ + /* set block sizes (in samples) */ + iBlockSizeSam = MIN_BLOCK_SIZE_SAMPLES; + iSndCrdBlockSizeSam = MIN_SND_CRD_BLOCK_SIZE_SAMPLES; + + vecsAudioSndCrd.Init(iSndCrdBlockSizeSam * 2); /* stereo */ + vecdAudioSndCrdL.Init(iSndCrdBlockSizeSam); + vecdAudioSndCrdR.Init(iSndCrdBlockSizeSam); + + vecdAudioL.Init(iBlockSizeSam); + vecdAudioR.Init(iBlockSizeSam); + + Sound.InitRecording(iSndCrdBlockSizeSam * 2 /* stereo */); + Sound.InitPlayback(iSndCrdBlockSizeSam * 2 /* stereo */); + + /* resample objects are always initialized with the input block size */ + /* record */ + ResampleObjDownL.Init(iSndCrdBlockSizeSam, + (double) SAMPLE_RATE / SND_CRD_SAMPLE_RATE); + ResampleObjDownR.Init(iSndCrdBlockSizeSam, + (double) SAMPLE_RATE / SND_CRD_SAMPLE_RATE); + + /* playback */ + ResampleObjUpL.Init(iBlockSizeSam, + (double) SND_CRD_SAMPLE_RATE / SAMPLE_RATE); + ResampleObjUpR.Init(iBlockSizeSam, + (double) SND_CRD_SAMPLE_RATE / SAMPLE_RATE); + + /* init network buffers */ + vecsNetwork.Init(iBlockSizeSam); + vecdNetwData.Init(iBlockSizeSam); + + /* init moving average buffer for response time evaluation */ + RespTimeMoAvBuf.Init(LEN_MOV_AV_RESPONSE); + + /* init time for response time evaluation */ + TimeLastBlock = QTime::currentTime(); + + AudioReverb.Clear(); +} + +void CClient::run() +{ + int i, iInCnt; + + /* Set thread priority (The working thread should have a higher + priority than the GUI) */ +#ifdef _WIN32 + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); +#else + /* set the process to realtime privs */ + struct sched_param schp; + memset(&schp, 0, sizeof(schp)); + schp.sched_priority = sched_get_priority_max(SCHED_FIFO); + sched_setscheduler(0, SCHED_FIFO, &schp); +#endif + + /* init object */ + Init(); + + + /* runtime phase --------------------------------------------------------- */ + bRun = true; + + /* main loop of working thread */ + while (bRun) + { + /* get audio from sound card (blocking function) */ + if (Sound.Read(vecsAudioSndCrd)) + PostWinMessage(MS_SOUND_IN, MUL_COL_LED_RED); + else + PostWinMessage(MS_SOUND_IN, MUL_COL_LED_GREEN); + + /* copy data from one stereo buffer in two separate buffers */ + iInCnt = 0; + for (i = 0; i < iSndCrdBlockSizeSam; i++) + { + vecdAudioSndCrdL[i] = (double) vecsAudioSndCrd[iInCnt++]; + vecdAudioSndCrdR[i] = (double) vecsAudioSndCrd[iInCnt++]; + } + + /* resample data for each channel seaparately */ + ResampleObjDownL.Resample(vecdAudioSndCrdL, vecdAudioL); + ResampleObjDownR.Resample(vecdAudioSndCrdR, vecdAudioR); + + /* update signal level meters */ + SignalLevelMeterL.Update(vecdAudioL); + SignalLevelMeterR.Update(vecdAudioR); + + /* add reverberation effect if activated */ + if (iReverbLevel != 0) + { + /* first attenuation amplification factor */ + const double dRevLev = (double) iReverbLevel / AUD_REVERB_MAX / 2; + + if (bReverbOnLeftChan) + { + for (i = 0; i < iBlockSizeSam; i++) + { + /* left channel */ + vecdAudioL[i] += + dRevLev * AudioReverb.ProcessSample(vecdAudioL[i]); + } + } + else + { + for (i = 0; i < iBlockSizeSam; i++) + { + /* right channel */ + vecdAudioR[i] += + dRevLev * AudioReverb.ProcessSample(vecdAudioR[i]); + } + } + } + + /* mix both signals depending on the fading setting */ + const int iMiddleOfFader = AUD_FADER_IN_MAX / 2; + const double dAttFact = + (double) (iMiddleOfFader - abs(iMiddleOfFader - iAudioInFader)) / + iMiddleOfFader; + for (i = 0; i < iBlockSizeSam; i++) + { + double dMixedSignal; + + if (iAudioInFader > iMiddleOfFader) + dMixedSignal = vecdAudioL[i] + dAttFact * vecdAudioR[i]; + else + dMixedSignal = vecdAudioR[i] + dAttFact * vecdAudioL[i]; + + vecsNetwork[i] = Double2Short(dMixedSignal); + } + + /* send it through the network */ + Socket.SendPacket(Channel.PrepSendPacket(vecsNetwork), + Channel.GetAddress(), Channel.GetTimeStampIdx()); + + /* receive a new block */ + if (Channel.GetData(vecdNetwData)) + PostWinMessage(MS_JIT_BUF_GET, MUL_COL_LED_GREEN); + else + PostWinMessage(MS_JIT_BUF_GET, MUL_COL_LED_RED); + +#ifdef _DEBUG_ +#if 0 +#if 0 +/* Determine network delay. We can do this very simple if only this client is + connected to the server. In this case, exactly the same audio material is + coming back and we can simply compare the samples */ +/* store send data instatic buffer (may delay is 100 ms) */ +const int iMaxDelaySamples = (int) ((float) 0.3 /*0.1*/ * SAMPLE_RATE); +static CVector vecsOutBuf(iMaxDelaySamples); + +/* update buffer */ +const int iBufDiff = iMaxDelaySamples - iBlockSizeSam; +for (i = 0; i < iBufDiff; i++) + vecsOutBuf[i + iBlockSizeSam] = vecsOutBuf[i]; +for (i = 0; i < iBlockSizeSam; i++) + vecsOutBuf[i] = vecsNetwork[i]; + +/* now search for equal samples */ +int iDelaySamples = 0; +for (i = 0; i < iMaxDelaySamples - 1; i++) +{ + /* compare two successive samples */ + if ((vecsOutBuf[i] == (short) vecdNetwData[0]) && + (vecsOutBuf[i + 1] == (short) vecdNetwData[1])) + { + iDelaySamples = i; + } +} + +static FILE* pFileDelay = fopen("delay.dat", "w"); +fprintf(pFileDelay, "%d\n", iDelaySamples); +fflush(pFileDelay); +#else +/* just store both, input and output, streams */ +// fid=fopen('v.dat','r');x=fread(fid,'int16');fclose(fid); +static FILE* pFileDelay = fopen("v.dat", "wb"); +short sData[2]; +for (i = 0; i < iBlockSizeSam; i++) +{ + sData[0] = vecsNetwork[i]; + sData[1] = (short) vecdNetwData[i]; + fwrite(&sData, size_t(2), size_t(2), pFileDelay); +} +fflush(pFileDelay); +#endif +#endif +#endif + + +/* +// fid=fopen('v.dat','r');x=fread(fid,'int16');fclose(fid); +static FILE* pFileDelay = fopen("v.dat", "wb"); +short sData[2]; +for (i = 0; i < iBlockSizeSam; i++) +{ + sData[0] = (short) vecdNetwData[i]; + fwrite(&sData, size_t(2), size_t(1), pFileDelay); +} +fflush(pFileDelay); +*/ + + + /* check if channel is connected */ + if (Channel.IsConnected()) + { + /* write mono input signal in both sound-card channels */ + for (i = 0; i < iBlockSizeSam; i++) + vecdAudioL[i] = vecdAudioR[i] = vecdNetwData[i]; + } + else + { + /* if not connected, clear data */ + for (i = 0; i < iBlockSizeSam; i++) + vecdAudioL[i] = vecdAudioR[i] = 0.0; + } + + /* resample data for each channel separately */ + ResampleObjUpL.Resample(vecdAudioL, vecdAudioSndCrdL); + ResampleObjUpR.Resample(vecdAudioR, vecdAudioSndCrdR); + + /* copy data from one stereo buffer in two separate buffers */ + iInCnt = 0; + for (i = 0; i < iSndCrdBlockSizeSam; i++) + { + vecsAudioSndCrd[iInCnt++] = Double2Short(vecdAudioSndCrdL[i]); + vecsAudioSndCrd[iInCnt++] = Double2Short(vecdAudioSndCrdR[i]); + } + + /* play the new block */ + if (Sound.Write(vecsAudioSndCrd)) + PostWinMessage(MS_SOUND_OUT, MUL_COL_LED_RED); + else + PostWinMessage(MS_SOUND_OUT, MUL_COL_LED_GREEN); + + + /* update response time measurement --------------------------------- */ + /* add time difference */ + const QTime CurTime = QTime::currentTime(); + + /* we want to calculate the standard deviation (we assume that the mean + is correct at the block period time) */ + const double dCurAddVal = + ( (double) TimeLastBlock.msecsTo ( CurTime ) - MIN_BLOCK_DURATION_MS ); + +/* +// TEST +static FILE* pFileTest = fopen("sti.dat", "w"); +fprintf(pFileTest, "%e\n", dCurAddVal); +fflush(pFileTest); +*/ + + RespTimeMoAvBuf.Add ( dCurAddVal * dCurAddVal ); /* add squared value */ + + /* store old time value */ + TimeLastBlock = CurTime; + } + + /* reset current signal level and LEDs */ + SignalLevelMeterL.Reset(); + SignalLevelMeterR.Reset(); + PostWinMessage(MS_RESET_ALL, 0); +} + +bool CClient::Stop() +{ + /* set flag so that thread can leave the main loop */ + bRun = false; + + /* give thread some time to terminate, return status */ + return wait(5000); +} diff --git a/src/client.h b/src/client.h new file mode 100755 index 00000000..9ea73a88 --- /dev/null +++ b/src/client.h @@ -0,0 +1,135 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(CLIENT_HOIHGE76GEKJH98_3_43445KJIUHF1912__INCLUDED_) +#define CLIENT_HOIHGE76GEKJH98_3_43445KJIUHF1912__INCLUDED_ + +#include +#include +#include +#include +#include "global.h" +#include "socket.h" +#include "resample.h" +#include "channel.h" +#include "util.h" +#ifdef _WIN32 +# include "../windows/sound.h" +#else +# include "../linux/sound.h" +# include +#endif + + +/* Definitions ****************************************************************/ +/* audio in fader range */ +#define AUD_FADER_IN_MAX 100 + +/* audio reverberation range */ +#define AUD_REVERB_MAX 100 + + +/* Classes ********************************************************************/ +class CClient : public QThread +{ +public: + CClient() : bRun ( false ), Socket ( &Channel ), + iAudioInFader ( AUD_FADER_IN_MAX / 2 ), + iReverbLevel ( AUD_REVERB_MAX / 6 ), + bReverbOnLeftChan ( false ) {} + virtual ~CClient () {} + + void Init(); + bool Stop(); + bool IsRunning() {return bRun;} + bool SetServerAddr(QString strNAddr); + double MicLevelL() {return SignalLevelMeterL.MicLevel();} + double MicLevelR() {return SignalLevelMeterR.MicLevel();} + bool IsConnected() {return Channel.IsConnected();} + + /* we want to return the standard deviation. For that we need to calculate + the sqaure root */ + double GetTimingStdDev() {return sqrt(RespTimeMoAvBuf.GetAverage());} + + int GetAudioInFader() {return iAudioInFader;} + void SetAudioInFader(const int iNV) {iAudioInFader = iNV;} + + int GetReverbLevel() {return iReverbLevel;} + void SetReverbLevel(const int iNL) {iReverbLevel = iNL;} + + bool IsReverbOnLeftChan() {return bReverbOnLeftChan;} + void SetReverbOnLeftChan(const bool bIL) + {bReverbOnLeftChan = bIL; AudioReverb.Clear();} + + CSound* GetSndInterface() {return &Sound;} + CChannel* GetChannel() {return &Channel;} + + + // settings + string strIPAddress; + +protected: + virtual void run(); + + /* only one channel is needed for client application */ + CChannel Channel; + + CSocket Socket; + CSound Sound; + CSignalLevelMeter SignalLevelMeterL; + CSignalLevelMeter SignalLevelMeterR; + + bool bRun; + CVector vecdNetwData; + + int iAudioInFader; + bool bReverbOnLeftChan; + int iReverbLevel; + CAudioReverb AudioReverb; + + int iSndCrdBlockSizeSam; + int iBlockSizeSam; + + CVector vecsAudioSndCrd; + CVector vecdAudioSndCrdL; + CVector vecdAudioSndCrdR; + + CVector vecdAudioL; + CVector vecdAudioR; + + CVector vecsNetwork; + + /* resample objects */ + CAudioResample ResampleObjDownL; /* left channel */ + CAudioResample ResampleObjDownR; /* right channel */ + CAudioResample ResampleObjUpL; /* left channel */ + CAudioResample ResampleObjUpR; /* right channel */ + + /* debugging, evaluating */ + CMovingAv RespTimeMoAvBuf; + QTime TimeLastBlock; +}; + + +#endif /* !defined(CLIENT_HOIHGE76GEKJH98_3_43445KJIUHF1912__INCLUDED_) */ diff --git a/src/global.h b/src/global.h new file mode 100755 index 00000000..3690c373 --- /dev/null +++ b/src/global.h @@ -0,0 +1,154 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(GLOBAL_H__3B123453_4344_BB2B_23E7A0D31912__INCLUDED_) +#define GLOBAL_H__3B123453_4344_BB2B_23E7A0D31912__INCLUDED_ + +#include +#include +#include +#include +#include +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + + +/* Definitions ****************************************************************/ +/* define this macro to get debug output */ +#define _DEBUG_ +#undef _DEBUG_ + +/* version and application name */ +#ifndef VERSION +# define VERSION "0.9.1" +#endif +#define APP_NAME "llcon" + + +/* defined port number for client and server */ +#define LLCON_PORT_NUMBER 22122 + +/* sample rate */ +#define SAMPLE_RATE 24000 + +/* sound card sample rate. Should be always 48 kHz to avoid sound card driver + internal sample rate conversion which might be buggy */ +#define SND_CRD_SAMPLE_RATE 48000 + +/* minimum block duration - all other buffer durations must be a multiple + of this duration */ +#define MIN_BLOCK_DURATION_MS 2 /* ms */ + +#define MIN_BLOCK_SIZE_SAMPLES ( MIN_BLOCK_DURATION_MS * SAMPLE_RATE / 1000 ) +#define MIN_SND_CRD_BLOCK_SIZE_SAMPLES ( MIN_BLOCK_DURATION_MS * SND_CRD_SAMPLE_RATE / 1000 ) + +/* block length in milliseconds (it seems that with the standard windows time + a minimum block duration of 10 ms can be used) */ +#ifdef _WIN32 +# define BLOCK_DURATION_MS 6 /* ms */ +#else +/* first tests showed that with 24000 kHz a block time shorter than 5 ms leads to + much higher DSL network latencies. A length of 6 ms seems to be optimal */ +# define BLOCK_DURATION_MS 6 /* ms */ +#endif + + +#define BLOCK_SIZE_SAMPLES (BLOCK_DURATION_MS * SAMPLE_RATE / 1000) +#define SND_CRD_BLOCK_SIZE_SAMPLES (BLOCK_DURATION_MS * SND_CRD_SAMPLE_RATE / 1000) + +/* maximum network buffer size (which can be chosen by slider) */ +#define MAX_NET_BUF_SIZE_NUM_BL 10 /* number of blocks */ + +/* default network buffer size */ +#define DEF_NET_BUF_SIZE_NUM_BL 2 /* number of blocks */ + +// number of ticks of audio in/out buffer sliders +#ifdef _WIN32 +# define AUD_SLIDER_LENGTH 15 +#else +# define AUD_SLIDER_LENGTH 6 +#endif + +/* sample rate offset estimation algorithm */ +/* time interval for sample rate offset estimation */ +#define TIME_INT_SAM_OFFS_EST 60 /* s */ + +/* time interval of taps for sample rate offset estimation (time stamps) */ +#define INTVL_TAPS_SAM_OFF_SET 1 /* s */ + +#define NUM_BL_TIME_STAMPS ( ( INTVL_TAPS_SAM_OFF_SET * 1000 ) / MIN_BLOCK_DURATION_MS ) +#define VEC_LEN_SAM_OFFS_EST ( TIME_INT_SAM_OFFS_EST / INTVL_TAPS_SAM_OFF_SET ) + +/* length of the moving average buffer for response time measurement */ +#define TIME_MOV_AV_RESPONSE 30 /* seconds */ +#define LEN_MOV_AV_RESPONSE (TIME_MOV_AV_RESPONSE * 1000 / BLOCK_DURATION_MS) + + +#define _MAXSHORT 32767 +#define _MAXBYTE 255 /* binary: 11111111 */ +#define _MINSHORT (-32768) + + +/* Definitions for window message system ------------------------------------ */ +typedef unsigned int _MESSAGE_IDENT; +#define MS_RESET_ALL 0 /* MS: Message */ +#define MS_SOUND_IN 1 +#define MS_SOUND_OUT 2 +#define MS_JIT_BUF_PUT 3 +#define MS_JIT_BUF_GET 4 + +#define MUL_COL_LED_RED 0 +#define MUL_COL_LED_YELLOW 1 +#define MUL_COL_LED_GREEN 2 + + +/* Classes ********************************************************************/ +class CGenErr +{ +public: + CGenErr(QString strNE) : strError(strNE) {} + QString strError; +}; + +class CLlconEvent : public QCustomEvent +{ +public: + CLlconEvent(int iNewMeTy, int iNewSt, int iNewChN = 0) : + QCustomEvent(QEvent::User + 11), iMessType(iNewMeTy), iStatus(iNewSt), + iChanNum(iNewChN) {} + + int iMessType; + int iStatus; + int iChanNum; +}; + + +/* Prototypes for global functions ********************************************/ +/* Posting a window message */ +void PostWinMessage(const _MESSAGE_IDENT MessID, const int iMessageParam = 0, + const int iChanNum = 0); + + +#endif /* !defined(GLOBAL_H__3B123453_4344_BB2B_23E7A0D31912__INCLUDED_) */ diff --git a/src/llconclientdlg.cpp b/src/llconclientdlg.cpp new file mode 100755 index 00000000..ac8a844c --- /dev/null +++ b/src/llconclientdlg.cpp @@ -0,0 +1,328 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "llconclientdlg.h" + + +/* Implementation *************************************************************/ +CLlconClientDlg::CLlconClientDlg ( CClient* pNCliP, QWidget* parent, + const char* name, bool modal, WFlags f) : pClient ( pNCliP ), + CLlconClientDlgBase ( parent, name, modal, f ) +{ + /* add help text to controls */ + QString strInpLevH = tr("Input level meter: Shows the level of the " + "input audio signal of the sound card. The level is in dB. Overload " + "should be avoided."); + QWhatsThis::add(TextLabelInputLevel, strInpLevH); + QWhatsThis::add(ProgressBarInputLevelL, strInpLevH); + QWhatsThis::add(ProgressBarInputLevelR, strInpLevH); + + QWhatsThis::add(PushButtonConnect, tr("Connect / Disconnect Button:" + " Push this button to connect the server. A valid IP address has " + "to be specified before. If the client is connected, pressing this " + "button will disconnect the connection.")); + + QWhatsThis::add(TextLabelNameVersion, tr("Version: Shows the " + "current version of the software.")); + + QWhatsThis::add(TextLabelStatus, tr("Status Bar: In the status bar " + "different messages are displayed. E.g., if an error ocurred or the " + "status of the connection is shown.")); + + QString strServAddrH = tr("Server Address: In this edit control, " + "the IP address of the server can be set. If an invalid address was " + "chosen, an error message is shown in the status bar."); + QWhatsThis::add(TextLabelServerAddr, strServAddrH); + QWhatsThis::add(LineEditServerAddr, strServAddrH); + + /* set text for version and application name */ + TextLabelNameVersion-> + setText(QString(APP_NAME) + tr(" client ") + QString(VERSION) + + " (" + QString().setNum(BLOCK_DURATION_MS) + " ms)"); + + /* init server address line edit */ + LineEditServerAddr->setText ( pClient->strIPAddress.c_str () ); + + /* init status label */ + OnTimerStatus (); + + /* init sample rate offset label */ +// TextSamRateOffsValue->setText ( "0 Hz" ); +// FIXME disable sample rate estimation result label since estimation does not work +TextSamRateOffsValue->setText ( "---" ); + + /* init connection button text */ + PushButtonConnect->setText ( CON_BUT_CONNECTTEXT ); + + /* Init timing jitter text label */ + TextLabelStdDevTimer->setText ( "" ); + + /* init input level meter bars */ + ProgressBarInputLevelL->setTotalSteps ( NUM_STEPS_INP_LEV_METER ); + ProgressBarInputLevelL->setProgress ( 0 ); + ProgressBarInputLevelR->setTotalSteps ( NUM_STEPS_INP_LEV_METER ); + ProgressBarInputLevelR->setProgress ( 0 ); + + + /* init slider controls --- */ + /* sound buffer in */ + SliderSndBufIn->setRange(2, AUD_SLIDER_LENGTH); + const int iCurNumInBuf = pClient->GetSndInterface()->GetInNumBuf(); + SliderSndBufIn->setValue(iCurNumInBuf); + TextSndBufIn->setText("In: " + QString().setNum(iCurNumInBuf)); + + /* sound buffer out */ + SliderSndBufOut->setRange(2, AUD_SLIDER_LENGTH); + const int iCurNumOutBuf = pClient->GetSndInterface()->GetOutNumBuf(); + SliderSndBufOut->setValue(iCurNumOutBuf); + TextSndBufOut->setText("Out: " + QString().setNum(iCurNumOutBuf)); + + /* network buffer */ + SliderNetBuf->setRange(1, MAX_NET_BUF_SIZE_NUM_BL); + const int iCurNumNetBuf = pClient->GetChannel()->GetSockBufSize(); + SliderNetBuf->setValue(iCurNumNetBuf); + TextNetBuf->setText("Size: " + QString().setNum(iCurNumNetBuf)); + + /* audio in fader */ + SliderAudInFader->setRange(0, AUD_FADER_IN_MAX); + const int iCurAudInFader = pClient->GetAudioInFader(); + SliderAudInFader->setValue(iCurAudInFader); + SliderAudInFader->setTickInterval(AUD_FADER_IN_MAX / 9); + + /* audio reverberation */ + SliderAudReverb->setRange(0, AUD_REVERB_MAX); + const int iCurAudReverb = pClient->GetReverbLevel(); + SliderAudReverb->setValue ( AUD_REVERB_MAX - iCurAudReverb ); + SliderAudReverb->setTickInterval(AUD_REVERB_MAX / 9); + + + /* set radio buttons --- */ + /* reverb channel */ + if (pClient->IsReverbOnLeftChan()) + RadioButtonRevSelL->setChecked(true); + else + RadioButtonRevSelR->setChecked(true); + + + /* Main menu bar -------------------------------------------------------- */ + pMenu = new QMenuBar(this); + CHECK_PTR(pMenu); + pMenu->insertItem(tr("&?"), new CLlconHelpMenu(this)); + pMenu->setSeparator(QMenuBar::InWindowsStyle); + + /* Now tell the layout about the menu */ + CLlconClientDlgBaseLayout->setMenuBar(pMenu); + + + /* connections ---------------------------------------------------------- */ + /* push-buttons */ + QObject::connect(PushButtonConnect, SIGNAL(clicked()), + this, SLOT(OnConnectDisconBut())); + + /* timers */ + QObject::connect(&TimerSigMet, SIGNAL(timeout()), + this, SLOT(OnTimerSigMet())); + QObject::connect(&TimerStatus, SIGNAL(timeout()), + this, SLOT(OnTimerStatus())); + + /* sliders */ + QObject::connect(SliderSndBufIn, SIGNAL(valueChanged(int)), + this, SLOT(OnSliderSndBufInChange(int))); + QObject::connect(SliderSndBufOut, SIGNAL(valueChanged(int)), + this, SLOT(OnSliderSndBufOutChange(int))); + QObject::connect(SliderNetBuf, SIGNAL(valueChanged(int)), + this, SLOT(OnSliderNetBuf(int))); + + QObject::connect(SliderAudInFader, SIGNAL(valueChanged(int)), + this, SLOT(OnSliderAudInFader(int))); + QObject::connect(SliderAudReverb, SIGNAL(valueChanged(int)), + this, SLOT(OnSliderAudReverb(int))); + + /* radio buttons */ + QObject::connect(RadioButtonRevSelL, SIGNAL(clicked()), + this, SLOT(OnRevSelL())); + QObject::connect(RadioButtonRevSelR, SIGNAL(clicked()), + this, SLOT(OnRevSelR())); + + + /* timers --------------------------------------------------------------- */ + /* start timer for status bar */ + TimerStatus.start(STATUSBAR_UPDATE_TIME); +} + +CLlconClientDlg::~CLlconClientDlg() +{ + /* if connected, terminate connection */ + if (pClient->IsRunning()) + { + pClient->Stop(); + } +} + +void CLlconClientDlg::closeEvent ( QCloseEvent * Event ) +{ + // store IP address + pClient->strIPAddress = LineEditServerAddr->text().latin1(); + + // default implementation of this event handler routine + Event->accept(); +} + +void CLlconClientDlg::OnConnectDisconBut() +{ + /* start/stop client, set button text */ + if (pClient->IsRunning()) + { + pClient->Stop(); + PushButtonConnect->setText(CON_BUT_CONNECTTEXT); + + /* stop timer for level meter bars and reset them */ + TimerSigMet.stop(); + ProgressBarInputLevelL->setProgress(0); + ProgressBarInputLevelR->setProgress(0); + + /* immediately update status bar */ + OnTimerStatus(); + } + else + { + /* set address and check if address is valid */ + if (pClient->SetServerAddr(LineEditServerAddr->text())) + { + pClient->start(); + PushButtonConnect->setText(CON_BUT_DISCONNECTTEXT); + + /* start timer for level meter bar */ + TimerSigMet.start(LEVELMETER_UPDATE_TIME); + } + else + { + /* Restart timer to ensure that the text is visible at + least the time for one complete interval */ + TimerStatus.changeInterval(STATUSBAR_UPDATE_TIME); + + /* show the error in the status bar */ + TextLabelStatus->setText(tr("invalid address")); + } + } +} + +void CLlconClientDlg::OnSliderSndBufInChange(int value) +{ + pClient->GetSndInterface()->SetInNumBuf(value); + TextSndBufIn->setText("In: " + QString().setNum(value)); +} + +void CLlconClientDlg::OnSliderSndBufOutChange(int value) +{ + pClient->GetSndInterface()->SetOutNumBuf(value); + TextSndBufOut->setText("Out: " + QString().setNum(value)); +} + +void CLlconClientDlg::OnSliderNetBuf(int value) +{ + pClient->GetChannel()->SetSockBufSize ( MIN_BLOCK_SIZE_SAMPLES, value ); + TextNetBuf->setText("Size: " + QString().setNum(value)); +} + +void CLlconClientDlg::OnTimerSigMet() +{ + /* get current input levels */ + double dCurSigLevelL = pClient->MicLevelL(); + double dCurSigLevelR = pClient->MicLevelR(); + + /* linear transformation of the input level range to the progress-bar + range */ + dCurSigLevelL -= LOW_BOUND_SIG_METER; + dCurSigLevelL *= NUM_STEPS_INP_LEV_METER / + (UPPER_BOUND_SIG_METER - LOW_BOUND_SIG_METER); + dCurSigLevelR -= LOW_BOUND_SIG_METER; + dCurSigLevelR *= NUM_STEPS_INP_LEV_METER / + (UPPER_BOUND_SIG_METER - LOW_BOUND_SIG_METER); + + /* show current level */ + ProgressBarInputLevelL->setProgress((int) ceil(dCurSigLevelL)); + ProgressBarInputLevelR->setProgress((int) ceil(dCurSigLevelR)); +} + +void CLlconClientDlg::OnTimerStatus() +{ + /* show connection status in status bar */ + if (pClient->IsConnected() && pClient->IsRunning()) + TextLabelStatus->setText(tr("connected")); + else + TextLabelStatus->setText(tr("disconnected")); + + /* update sample rate offset label */ +// FIXME disable sample rate estimation result label since estimation does not work +/* + QString strSamRaOffs; + +// FIXME: sample rate estimation result must be corrected since we use +// smaller buffers in client now. Actual estimation should be fixed, not here + + strSamRaOffs.setNum(pClient->GetChannel()->GetResampleOffset() * + MIN_BLOCK_DURATION_MS / BLOCK_DURATION_MS, 'f', 2); + TextSamRateOffsValue->setText(strSamRaOffs + " Hz"); +*/ + + /* response time */ + TextLabelStdDevTimer->setText(QString(). + setNum(pClient->GetTimingStdDev(), 'f', 2) + " ms"); +} + +void CLlconClientDlg::customEvent(QCustomEvent* Event) +{ + if (Event->type() == QEvent::User + 11) + { + const int iMessType = ((CLlconEvent*) Event)->iMessType; + const int iStatus = ((CLlconEvent*) Event)->iStatus; + + switch(iMessType) + { + case MS_SOUND_IN: + CLEDSoundIn->SetLight(iStatus); + break; + + case MS_SOUND_OUT: + CLEDSoundOut->SetLight(iStatus); + break; + + case MS_JIT_BUF_PUT: + CLEDNetwPut->SetLight(iStatus); + break; + + case MS_JIT_BUF_GET: + CLEDNetwGet->SetLight(iStatus); + break; + + case MS_RESET_ALL: + CLEDSoundIn->Reset(); + CLEDSoundOut->Reset(); + CLEDNetwPut->Reset(); + CLEDNetwGet->Reset(); + break; + } + } +} diff --git a/src/llconclientdlg.h b/src/llconclientdlg.h new file mode 100755 index 00000000..c136540e --- /dev/null +++ b/src/llconclientdlg.h @@ -0,0 +1,96 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "global.h" +#include "client.h" +#include "multicolorled.h" +#ifdef _WIN32 +# include "../windows/moc/llconclientdlgbase.h" +#else +# include "moc/llconclientdlgbase.h" +#endif + + +/* Definitions ****************************************************************/ +/* text strings for connection button for connect and disconnect */ +#define CON_BUT_CONNECTTEXT "C&onnect" +#define CON_BUT_DISCONNECTTEXT "D&isconnect" + +/* steps for input level meter */ +#define NUM_STEPS_INP_LEV_METER 100 + +/* update time for GUI controls */ +#define LEVELMETER_UPDATE_TIME 100 /* ms */ +#define STATUSBAR_UPDATE_TIME 1000 /* ms */ + +/* range for signal level meter */ +#define LOW_BOUND_SIG_METER ( -50.0 ) /* dB */ +#define UPPER_BOUND_SIG_METER ( 0.0 ) /* dB */ + + +/* Classes ********************************************************************/ +class CLlconClientDlg : public CLlconClientDlgBase +{ + Q_OBJECT + +public: + CLlconClientDlg ( CClient* pNCliP, QWidget* parent = 0, + const char* name = 0, bool modal = FALSE, WFlags f = 0 ); + + virtual ~CLlconClientDlg (); + +protected: + CClient* pClient; + bool bConnected; + QTimer TimerSigMet; + QTimer TimerStatus; + virtual void customEvent ( QCustomEvent* Event ); + virtual void closeEvent ( QCloseEvent * Event ); + + QMenuBar* pMenu; + +public slots: + void OnConnectDisconBut (); + void OnTimerSigMet (); + void OnTimerStatus (); + void OnSliderSndBufInChange ( int value ); + void OnSliderSndBufOutChange ( int value ); + void OnSliderNetBuf ( int value ); + void OnSliderAudInFader ( int value ) { pClient->SetAudioInFader(value); } + void OnSliderAudReverb ( int value ) + { pClient->SetReverbLevel ( AUD_REVERB_MAX - value ); } + void OnRevSelL () { pClient->SetReverbOnLeftChan(true); } + void OnRevSelR () { pClient->SetReverbOnLeftChan(false); } +}; diff --git a/src/llconclientdlgbase.ui b/src/llconclientdlgbase.ui new file mode 100755 index 00000000..964f3b46 --- /dev/null +++ b/src/llconclientdlgbase.ui @@ -0,0 +1,1125 @@ + +CLlconClientDlgBase + + QDialog + + name + CLlconClientDlgBase + + + geometry + + 0 + 0 + 814 + 287 + + + + caption + llcon + + + icon + image0 + + + sizeGripEnabled + true + + + + margin + 11 + + + spacing + 6 + + + QFrame + + name + FrameMain + + + frameShape + StyledPanel + + + frameShadow + Raised + + + + margin + 11 + + + spacing + 6 + + + QLabel + + name + PixmapLabelCorrados + + + sizePolicy + + 3 + 3 + + + + pixmap + image1 + + + scaledContents + true + + + alignment + AlignCenter + + + hAlign + + + + QLayoutWidget + + name + Layout19 + + + layoutSpacing + + + + margin + 0 + + + spacing + 2 + + + QLayoutWidget + + name + Layout17 + + + layoutSpacing + + + + margin + 0 + + + spacing + 0 + + + QLabel + + name + TextLabelInputLevel + + + sizePolicy + + 1 + 3 + + + + text + Input Level L: + + + + QLabel + + name + TextLabelInputLevel_2 + + + sizePolicy + + 1 + 3 + + + + text + Input Level R: + + + + + + QLayoutWidget + + name + Layout18 + + + layoutSpacing + + + + margin + 0 + + + spacing + 0 + + + QProgressBar + + name + ProgressBarInputLevelL + + + sizePolicy + + 7 + 3 + + + + frameShape + Box + + + frameShadow + Sunken + + + lineWidth + 1 + + + margin + 0 + + + midLineWidth + 0 + + + centerIndicator + true + + + indicatorFollowsStyle + false + + + + QProgressBar + + name + ProgressBarInputLevelR + + + sizePolicy + + 7 + 3 + + + + frameShape + Box + + + frameShadow + Sunken + + + lineWidth + 1 + + + margin + 0 + + + midLineWidth + 0 + + + centerIndicator + true + + + indicatorFollowsStyle + false + + + + + + + + QLayoutWidget + + name + Layout3 + + + + margin + 0 + + + spacing + 6 + + + QLabel + + name + TextLabelServerAddr + + + text + Server Address: + + + + QLineEdit + + name + LineEditServerAddr + + + + + + QLayoutWidget + + name + Layout17 + + + + margin + 0 + + + spacing + 6 + + + QLabel + + name + TextLabelNameVersion + + + text + TextLabelNameVersion + + + + + name + Horizontal Spacing2 + + + orientation + Horizontal + + + sizeType + Expanding + + + sizeHint + + 20 + 20 + + + + + QPushButton + + name + PushButtonConnect + + + text + C&onnect + + + + + + QLabel + + name + TextLabelStatus + + + sizePolicy + + 7 + 1 + + + + frameShape + Panel + + + frameShadow + Sunken + + + text + TextLabelStatus + + + + + + QButtonGroup + + name + GroupBoxAudioInput + + + title + Audio In + + + + margin + 11 + + + spacing + 6 + + + QLayoutWidget + + name + Layout14 + + + + margin + 0 + + + spacing + 6 + + + QLayoutWidget + + name + Layout12 + + + + margin + 0 + + + spacing + 6 + + + QLabel + + name + TextAudInFader + + + text + Audio Fader + + + alignment + AlignCenter + + + hAlign + + + + QSlider + + name + SliderAudInFader + + + pageStep + 1 + + + orientation + Vertical + + + tickmarks + Both + + + + + + QLayoutWidget + + name + Layout13 + + + + margin + 0 + + + spacing + 6 + + + QLabel + + name + TextLabelAudReverb + + + text + Reverb + + + alignment + AlignCenter + + + hAlign + + + + QSlider + + name + SliderAudReverb + + + pageStep + 1 + + + orientation + Vertical + + + tickmarks + Both + + + + + + + + QLabel + + name + TextLabelReverbSelection + + + text + Reverb Selection + + + alignment + AlignCenter + + + hAlign + + + + QLayoutWidget + + name + Layout11 + + + + margin + 0 + + + spacing + 6 + + + QRadioButton + + name + RadioButtonRevSelL + + + text + L + + + + QRadioButton + + name + RadioButtonRevSelR + + + text + R + + + + + + + + QGroupBox + + name + GroupBoxJitterBuffer + + + title + Jitter Buffer + + + + margin + 11 + + + spacing + 6 + + + QLabel + + name + TextNetBuf + + + text + Size + + + alignment + AlignCenter + + + hAlign + + + + QSlider + + name + SliderNetBuf + + + pageStep + 1 + + + orientation + Vertical + + + tickmarks + Both + + + + QLabel + + name + TextLabel2 + + + text + Put / Get: + + + alignment + AlignCenter + + + hAlign + + + + QLayoutWidget + + name + Layout9 + + + + margin + 0 + + + spacing + 6 + + + CMultiColorLED + + name + CLEDNetwPut + + + minimumSize + + 13 + 13 + + + + maximumSize + + 13 + 13 + + + + + CMultiColorLED + + name + CLEDNetwGet + + + minimumSize + + 13 + 13 + + + + maximumSize + + 13 + 13 + + + + + + + + + QGroupBox + + name + GroupBoxSoundCardBuffers + + + title + Sndcard Buffers + + + + margin + 11 + + + spacing + 6 + + + QLayoutWidget + + name + Layout12 + + + + margin + 0 + + + spacing + 6 + + + QLayoutWidget + + name + Layout10 + + + + margin + 0 + + + spacing + 6 + + + QLabel + + name + TextSndBufIn + + + sizePolicy + + 3 + 1 + + + + minimumSize + + 30 + 0 + + + + text + In + + + alignment + AlignCenter + + + hAlign + + + + QSlider + + name + SliderSndBufIn + + + pageStep + 1 + + + orientation + Vertical + + + tickmarks + Both + + + + + + QLayoutWidget + + name + Layout11 + + + + margin + 0 + + + spacing + 6 + + + QLabel + + name + TextSndBufOut + + + sizePolicy + + 3 + 1 + + + + minimumSize + + 30 + 0 + + + + text + Out + + + alignment + AlignCenter + + + hAlign + + + + QSlider + + name + SliderSndBufOut + + + pageStep + 1 + + + orientation + Vertical + + + tickmarks + Both + + + + + + + + QLabel + + name + TextLabel1 + + + text + In / Out: + + + alignment + AlignCenter + + + hAlign + + + + QLayoutWidget + + name + Layout14 + + + + margin + 0 + + + spacing + 6 + + + CMultiColorLED + + name + CLEDSoundIn + + + minimumSize + + 13 + 13 + + + + maximumSize + + 13 + 13 + + + + + CMultiColorLED + + name + CLEDSoundOut + + + minimumSize + + 13 + 13 + + + + maximumSize + + 13 + 13 + + + + + + + + + QGroupBox + + name + GroupBoxMeasureResults + + + minimumSize + + 152 + 0 + + + + title + Measurement Results + + + + margin + 11 + + + spacing + 6 + + + QLayoutWidget + + name + Layout16 + + + + margin + 0 + + + spacing + 6 + + + QLabel + + name + TextLabelStdDevTimerLabel + + + text + StdDev: + + + + QLabel + + name + TextLabelStdDevTimer + + + text + val + + + + + + QLayoutWidget + + name + Layout18 + + + + margin + 0 + + + spacing + 6 + + + QLabel + + name + TextLabelSamOffset + + + text + Fs Offs: + + + + QLabel + + name + TextSamRateOffsValue + + + text + 24000.00 Hz + + + + + + + + + + + CMultiColorLED +
multicolorled.h
+ + 13 + 13 + + 0 + + 0 + 0 + + image2 +
+
+ + + image0 + 789cd3d7528808f055d0d2e72a2e492cc94c5648ce482c52d04a29cdcdad8c8eb5ade6523234530022130543251d2ea5248564056503300071f5205c0b2004719541dcb434986c22840b0260c56800454c9918b1c444e54454b1c4c4a424e5a4c4442431a0085008081231c4949511621021656565b042843a908032bade24a832547b21c6a1ba0f08d0fda18ccd6fd8c2009f58ad351700407358e1 + + + image1 + 789c4cfd4953325dd8aeebf6e7aff862d29bb183455d345683c21a10445159b11b24a5880a282aee3fbf9f97ebbacf5c61ef08cc1c3972d455fe5fffe77f9efaddfff93fffd7fffafc9a7cbd4cff67ba9aecffe7ffcc0e6f6fc7ffe7fffb7ffffffed7ff2ed4ebff93cfe5ff275faae7f3ff53fcdfff9ffff5bf935def7fa6ff93c955f3857cfe3f9874ea8652be70825140395f3afdcbd5c7096af9426e71fa45b96228e6cba75ffcb60d25fd22d9dd04e44fb79ddcac0c654132bb3a413db7f045efee0214b0647621c8e7fd8b663ec041ef3f000afad7bda1909b9fa03409f0bff472c0e95f92e3d850f45daacf01fe978b29a07fd975013d4bd2319422e89780ff25fdc5294e27f70583237932c99e20c9cd7ddbd777c3c2cff2fa0628a4997a80ffe56505283e9e0f827f51383dc16316989d603f0074976a1ed05d5e5e027c97e90cd05dee4b805e76f5de1009e62f17e06bec6f015da39b054ed7c8d4c78052e1a20954f4704743d141ffcb034a85abdf8048308f806ebb9c000a7a2903f82e7f8648da996e80af517f04941c166da0aca7559e9be6e6bec6fb6f80ffe53d312cf45e925503383d5cb29f028ad3692620e2b40ff8edff00be8baff1efddd695706f012587d72de044b703167a2fa500df769200baed6402284e33354069bd3e3214fcb41f6540b7fd1e007ada65dc2552d0c70ba0db4ee7805fc325a0d7903b021565d31b4351d923997500c5c7ef10503826b30087e3bb0f1414411540e1d87c0325fd2207a8f8dc1c0c91a4324540517837041485a38221cad3b3eb13ccfefd29c19cdf04b8505afd19e6fec5fb31c0bff83d079cb41f0c8b88a04f4011b42d00bae8ef19a0b4bef909708c6d7380eeb2ef027a9673a5c259a4d3c9f30c50389a5780c2512e034a41fb3bc0a5652520126e0670619082136e09d08b5a5c180a2e4f9f1b805272b70638037d004e41d30087a39b019c828a802bcf35e0ca626e703a4d921ee077fb0db8b85804f8b6992aa0c7efe501ddf6f70e70111c4f5bd26d33f532a0bbe45a01719702a0bbe49a80eef2f605e8e10a6ba0a6d221de5cd949ea4c35e1fc5f925172b81804380ac71786794ed7c80c0045d0773620d2fabdc1697d3259004eeb7940d7c8f4013dfed75d801fbf340374979725e032d9bf20adb74b80d3d81c7021bd0794c65ed680c2f1f613e070bc7d034e6305c0edb1b8682197e8b60b40b7cd7d02ba6db906a86eb888db46d29e4e0017d259406f7f3a05f4f69fb98b0be9f70847a4e44c3ec077b95a02bacbac0ef82e09e022f81950dd70efb71f493b5937035c38dea4a0365df36870132e73150f5771dbe1108f5fcb09462afafe252827cbd6d030774a7656ff0fdc1068028af5e7a52192e5f31e38fd223398036ed2c66da3699db905f42c6e382f225966eed586f9f70f39a58fcf0e902857de024ea7534001fbee01aed5d37f5182f9e6a20a58c66da9056572f31970038dbb38c164ca800ae99f99a1a0489e3473c029a499c2065039366e032eb53f0185b4f51de044b79c020ae9db01508cfdec01176cef06373726ed32a0703cb701bdb9fe30c01174fd08e8b69d2aa008fa58022ab55d282d48eb772343c9cdc0dd2de070b40025a9ce6f801fff790728c13c9f037ada8f35a0f7b28a87ab38e83ba7d37ca22495b938dde5bff4e3286c5d18227b5c560115c1977b402f6ab0302cf47099f135a0bb8c9b800bb606a0b7dfbf0754e1ec67017e0ddf5dc0c5f85c40f6f83b024a74e37340917cfe0c28925b0740cf72ac05f8b69f6d40f976f503b85ff96070e24f3e6f0077012780c2517805148ef51fa0705c3f05f8edb72b80aa936a1970f72dfd17054c59fd042a0b9b6343e486c21ba0f752d8027a2f6f7bc0f5dc6740f4d5134001ebed00658fc287c1697df2b603dce18d57192d94d7574005fdb393e5bf92df1def782f95785aa7d328f993eb538efa2fce9da47e0e8679d4b7df809eb6530970a9bdf934440b655f03dc0c6c01ae2c56801eeef532c0496afc0778f8a70828697ff404694a6e014a418fe92f74dbdd0c500a1a37027c5bbfec0229799dfe42c5d6616428b861a452fb047afcea18708e7a0294926b5b40e1e81c031c8ec13da0c72f6501bdfdf9cee0749ac97f014a41c32ca0dbd6f3806e5be31ad12cce014aa7fb4740b17ea8014a52bf7d40c5786b6970219da8d57702bd6c9752054aedaf3ee0daa306e8b65f03a0a280459c969d4e931aa0bbccaa01becbaf136ebee2089afe06c4d8d695a1eea0b7f41a8aff75574f70f36270f648da754019e8b2161049eac3e0823ef1cb2e925f5cd21529f9973f80c2b148afa1877b5e073813fefd024ab89b2f40a5c3ad62ac487e697d01ce2f1d4009e6b3057884b51a1031f60228c10cee0095b8df3786c81e872ce056ce15a0db3ede024aa7fb5180e3349700ee581d01dd568d9a1328dfaee359a25c9f550085a3f303b8c269002e2e7e031c8ede1650ac7fee0165d35cfa2f0ac777fa8b537ec95c3a0545b36752af007a51671780429a1d01a79066876340217d88f7e22c9679fa0414d2450b508c3dcc00e7ec5b400d565e6529af90bec5e3975dc0e6378012ccd93220c672be0d95180088375773929aef018f9e0f01359d2af15eea8ea0d29961e2f65847b5c7bff87253f2ec06d02fcedf00bdecf159804b98eb91c1593dd3fa019482fa77807b9e0540854123c211a3e7c7b88b0b83497d082860db23a0975ddb03bacb7c0be82e9d758023f9ab067808e90b70b75aadcf1285416f0fe8b6ea259dc0b9b20b288d3dbd0744317e0bb8fb560594f87f3e0125fefcc11085c1e30da0db364b80124c520f70d2be6f011ee9fd06dc399b036e293d1922ef0f57806e9bef02a7db661f3701d1b89a027ada8f2aa0db1e3280ba1a5f0d83b37a26fb00cc94917380c2514c41b1be8c8045ce2e35019585a536a0703c6f013f7e842346ae7e7601cefb8d8521aac68f5f433d9a708272548d99f16580c3713e36d04b2a037ab8e7b38048a70703835d4b405563760b782a6d01e8590e8ad332b309370d4019f9630ce82edf1540cf72bd0554c076d28bea35bc7d0444cff305383d4bb6df06f41aee9f0c1eca4a1a6f8032e1e0015038dede01d7c82f80c3c135148e6cbe0ee835340b800afaf36f402ffb6506a809f7ba06d429fa8b6bc48cc4471f70d7bc0928a40f2bc0832a5f014eb8db35a0b7ffcabfb8946abc03a790666be92f14d2979621c60c3a2f805ef67d0650c0664f8072766714e028acf12fd18d2c017a958f09a01c35e8032ad872af86b2b3faf333e0e1b01ae012867f7138fa2dc08d9a2de0e2d399f05f6daa86e22afec5f936d9cf01bdececce5073e21fe5035c487fc68b4a5cdf7e2a9b56725377de8f4b434cea5cbd19625cfbf70750d2ce160165e4c60da06b749e0115f47e0d15eacae537e08e661550f3fc6b0428928f7540a9f0b11f1003668ac27ff1a5a0674a054041bff80694d6071540a5436d09f82e6f0131a9f30378c0bd09785af00a50d26e3bc6a2a399c996009763cf8042fab0035cad2d0127ba21a044373e0438d195b946941f84c3215dbd1b8a7e2f5f7560aa6b7c000a47f20ab8b5b5003ca6741be0d2a1b2015c9dfc00ca0d4fdcc5d56b275e65b493930de0a7fd0af05dfa5dc095670bd05dae9e0165a0d6ca401b379e25dab8ef5f8007bb2292ddf34cbe5f02dc7638d401152885780d3547d059bcfd68168f548bd518b45f6f01dd65766e98e7aa27d08cf709548c7b48adc6b07eb301a87a1d3d01aa5ed7ef018eb187b56111a97009e835e4bf01bdfda723a087cbff0444a7594deb5ada3e5d032a3fbe7a803261790be8a2b78f01ce848f454055f4f81ef0a4df2da08aeffed91075f6f704709d9d07f4b4cb1ea0a7cd3e020ad824c21179aec6451da7f75940796e5f3714fdb2cfdf0185e3f806281c0f29b8e27b06bc66a21a10b9a103281c577e737445f33d40e96399055cf445c0228bf536808bbe045016eba6ffa21ae8fd006898d20344b5ff5a97a76b9c0f013d5cff0570383e021c8ee74b40e1783c000a47250b28138e2b864a8cf4c645a359dc2d19a2f79a1903bae8b5fee55f57d5a5f6600d28475d370dd137f5f8699d31d8c30c50c5d75a00cab7ad02a09056d680e2a37b67880988b34b4077b97b047497f37b40b1fe720528d67baf80cac24312e0b2f0980394a4ba7a73f5689e674b75c059fd13504abe7d00f470eedfb2042f39af0644d3e90cd06df35cd4ef65383744467e7e0474dbde1be0519667c0a32cef014e630f1f806efb7400dc84eb029e1929036a7fe42686c8b78302a05aac16178daee8ed0da0eaa4f30aa8a158f06ba06a1c2e00970e8f80ee52f80988f6fa0ba0877bdc02ae9153d0c3bdf60ce55899f100e85566bb805bc1eb003fdc7516d06d2baf806e5b8b775b7121ddfc0b885cf90ca8a0ef8d010fcbc56da37aed4c0df558daa46b4c72896ad3c963c630d36db3e3a1215ab0f98521867ebf1340396a5402f41ace1f01378cde039ca312f52b279181324f0dc0f3c83d40b1de3c073c93d8017497dc3420eaca12a0f8f899008af5dc065083f5ea00a8d46ecc0c315572510154e4b4cf01cfe1dc000a7ab5082882ba5d40413f5403e2650f0005fd2903b8079c0554d097c6809ee5b16c28ba5f79be0314d2ee3de0599e02a0905efd000ae9794490f36db6c3359cb40b79c02b444a8047d0e686a891db5dc0b33c2340b74dde029c91d747c04bacee004550ab09e8b6173d4005ca79bc97c8c89301e0c2711b1085e314d06daf17809e761d692cbab3d7f780a78eae03a2e9f40978bd431fd0b35c468c39672767aac59298f39c7cd7029cd5ef9e0cd170beda02ca7383174075f6f705a0a0771f00affa7d0b7046feec1a5c234faeea802ae0cf09e036dd1c50ac8f5f0037cf3f027c97dd01f0dcbcf26d4287f7fa00a885729e07dc9ffb06148eca33e091bc31e05c590988f1b104505ad7aa9b7c42ffb63602543a3c8c01b7b5cb807be28f80d72ab400377bde026262b90ab81fc5359c0ab37b409d8076dfe00e6ff6f21970b3f80f70c0c6802bf111e0fe5c3c4b14065e10922e02cf7d014ac9cd3340e934e1a23133e20443353f78021c8e6740897f3108888ef71070a1d4019ca316804a8756bcca18ecba3a024aa78b3bc0b3b311a7513a7cae00ddf67a08e8b6a557408f3f5e1b5c5c64c79f80e70ae22e51ef6f9c5ffe753555235f463a8d06bc5b17d3688d4fde9e00b5b51b7343cc01673a01ce73ebb561e12c962d002a2e9226a0817f4f82b2b07ed27904f42cf3e78078515c235ed41ed07b69a8d89a46ce9ecc9f0025dcdc0be00433019ca31a805b9f5f0191b3f91727dc8f23e037770ee8cddda4d770dbc1114453e1f80d28cf957f01e7b923a028ec3701cf587d015e84550c70145eec002fd1ac014a30fb03a0a0ef1680d2faf1c3e0ae7926f906549e961e01057df40b7884a40128e88531e071fe6e404c81bf010a7af509f092a219a0a0574786683b94ca80dffe0c70715102fcf623c1447171ec012ed73f01afb9ba07bc046f089cde7eb61031568e71dc07c0e1c8015e3bca2f62c4e8095038863340e1e8550c2e1d325e53338dc644a69c828be01f40d7180c00bdecbbaaa1e628bc8cdbbab9912d28a433baf7ed2da04ec0fdc8e0d1f3cc730a7a51d7df866819ecd680bbe6d780ca8f870da096c15721c0a5d4f70af0485e022805fda95c9f517edc9401052c39033c63b501dcd5487fa137f75e0b8872ec1ef01a126eebe6f979c910ab0777d780d7b2b4011506974f8052506e05b8cbb30e883d78cf80171e1c01357aeb715b32f20ef0c4e10fe086d12fa00c54be0cf0e367328007ba5370bd1f0fe77c3bf9fa0374db7607f00ab314f4b4bd7873916ff37b40b7cdbd035e3bfa03e8355c7f19a2567f2f01bacb36622c7265fe1df0f8fa1cd05db45bf0042a1d8e715bd7ea99bb3ae0850711a7914d7fcb80aef1f70ca87ec9dc183cc995b98b54388b6d6abacb9c7d49adbc21aaf9e300f0e4f43da036ff4f0228ad373a063704928f1aa07aeee50cd0452f2680de6d690d28d69b3d40f191fd09f0b36cf81747d0f80350cedea918673747f2b3005c155401dd7636009c927f02a2695d0394706f6f0d3163d5bd013ce876043c40d401dcafec012eb59f036213c904f0fa0f2eea70148b86c8a6c502e0d9ea05a0bbdc37036299c513e011c501a0bb7822754e36bd9c02baed680ff8e172806edb8ea78d6cea1e30db3b26a3074019e8ba0db87abd03546c65e786582bf9fe02b83f9700ae6fa780e70ae2ddc6e4746708b8379f07bce23f0778a51b1775e559ce1822ab176e00555adf970131c4185118e374d96bc08bf3e3f127d13bd1351631023f79bd06bc6d6f6f88c5604f7f0131e7f96af000c0e4e10150e9e0d915b6cc4cfad78022b9db0b700973393144562fd7016f976b028e0f7513d2dd2dcb12a086c06204288d3d7e03deb35a0e70067ab8073cefd131b8becdb46f01f7d53f00e5ecdb3ea09c3d9c007ab8bf2ca0804d7b80d258867f899cbd02bc0a3a07b8f6281adcd64ece3b80abe802e015667bc04bac4a8067896f02624efc025038d6dcd669fdd309e65f46568c8d7e000f64e601ddf6ac0878c68a7f89a53cfcc245ce5b0f7093f603f0b4713c6de4ecbb0aa0dbdefc016ed147fa883afbf113f024c617e0713aeee222e72c5e9427b8b39d3b400f37cc04f82ea32b40715a78017c971ce05afd0270adee2c46e7fdf211f01ad6532d56c8fdcbea2ee916060fda67ae328658bab2db01ee255d06c40ebbad21b6eed4ee01cfcfdd00de793005bc25b31310bb28fe048cc2ddbf004ec95f809701ff027ab7fb07c033cda58068e554009f33913744c73bfb01a8705ca5e08d39cf80865d14eb27d06d47d58078b806e0d99511e072fdcd10bde8ee03a0802dab80e2e37e0678e63dfd85e2e36203388b1d02625dce2fe0be4702788361cf100bca9eff00af1b6f005e3bfa06b881761f10637d1bc08d890f40f1717334941d1fb90ce0a5a27940af6199fe42b7bd2b0738cfdd9500dd76db02dc521a03aa3c67118e8a93f6fa19d0d3962b800a142d09f80fa2184f415de2e91a5056ff39002ac76a0f80da0ec3b886ebfdccc793a1eeaa7116afb2eedbbe170d89d37a5b31566068af503238ef4f7eae01ef5c7a06bc13a31810f362194055e37c61884569cd26e05d8bdf800a83ec2120ba801301b5fac50cf01eef0bc07b797e029cc6344252602b53e6ea1750ac277786826734ef2e003d4be7197073a30128f13ffd00ce735b4001abb4039c0a6fef0105ec868b464b7a6488bcff9c0194f7d74340e1983500e506ede42aa49b9d4a1185d1a2bfe75f6211d6065004f5cf015527e512a01a683d35c490da5502289be66b8097f0360157c0d9805886b302bc6266013860578002d6dd020ad83c5ea5dbfc19edf13e81ab930de0a329be00056c510f70c0ba1dc019790514050b4019f9620f6832257116a3401964000f854f00056c9d000ad84724ed98057c9b026ea0950107ec1550c01e52d020646760a8f9458d3e00576b11c931b4377d0174d16ac550774ace47c0267e964cbcb9a9dfdc7a6c705763b27a32c4213c9f6a9014fe2bfc4e17fd5324ffb72bed946f3399c4108b6bb51abb50649861780178135a06f03e1c65e422ed8fb70ee0e540cf80ee32f800fca22a80f715dc04b821b0780394f7fb6b43c16711dc3f027ada8f01a0a9f8cf5bc02da5045093655303dc64b9073c1a38075c3a6402a2745801ee7b7097d8deb1073cabe148fe57e4680b51310f280a5f2e01a5f5e335a062ab3a00bcc33f9ec5c5d6e02b0328607d2eea0473f50e2860fb5f438c4cac1e01bdcac90a50388e37801bf011ebb4611e000f748f0025fe7119f0daf3f84594418532e0e32d5e01afb92a001e989906381c3f6780f37e1ef0ceb60de0fd2f91702baef77562ca093c2e75053805dd024a419512e066cf24c001d3816427707131075c282d01056cd901941ba61141b598e62802de9b50053c1ef400a860abf6015df43bb27aece559560097749f0131973401f42a357356f86fcb9d4a98bb73c3340621eb80aaa43725ed520cbb4c861f861821199d1b62def47e09a841b2ca00ba8656ee14d81d97c9b601b7f9e3b6d1b8d204e609bce1a10c78365289bf144b2426b70740e1d06ef4137866f503f0b4f10c70cb710db870fc0a70c09eef0d51f4f9bd94688f69b3d309944e6f07800a14ad622ca4fbe70a297839503c4b8cb2bc66002fa1490025a9ed9f216634efe78057881c01bdcaf53de04e511e503874a061810d75194d6016d85097a977018f28c66da3b575db01f4f8e52bc073c03b40d974bd0af06db3174041056c16f0795bdcc5897f9503dcda3a00ca62dae95728310ee3ae4689d6d6b60f781b12e0f263cf35e26092148a0a6902b880bd02d46deac56dbd3432539e009ea0fa01d41070e38aad7fd9f50ba070e854af13f82886f4172ac79e3680b6665ca5a0b5818b3343b4b66ee365474997cb022a3ed76bc0f5cb0ad0e3f76e003dfee8cd50773d9769039ed58867a9c75c5244f224669b22054d7c516da7fc07898bbe24623d718e9add021edb4a0025a945bceca9d3fa6d0f704fbc0e4cf42c4aa7655a7dc317400ff799186251da76698825de93074085920700d83d99192982d2cd92a30ae082ed1150146aaab5c0dec849ff22206601af0d518e5d2e01dda57e03a840d1368242ba15b2bf073cf998001eb67d0b88c19d3f40919c9c033e4aae0b78e741ce10255d760178d429058fc3940037599e0077f0f897d828dd05dc747a075cd2dd039eafcc034a853a88a3c0cec7ccf81650d0739780de5c330505fdf10df0b6ac25a0a06b6eadc0dec8c1d714f0f6b05f408d9ac302f0c2e911e0d2f2ca10f3519d04f042bf0ca0a43deb028ae4f51c504887c580186618011eec5a012e3e278042aa9d8f275029757e6788ce6ae60cf0268122a080f51b8072543192436cc93cdc032ed89680a2b0730b2859ae5e004521b7f53ab6c94713f0b1018f8072e567446114b0f91da070747a8052e1f913e0135253500fe7ae6088f2341379bf1e638e9f8046593237809e451b0dfec1c42f6af46d88e273740fb846be043cce1f8fefcde793ef4896b14d6da47fa9e4dc3ecd0c5e0d9ef0cffccd0d71e0c3e8cce0d667b25a036e7fdc014aa7734561ba35d43d4fb68626c762402c371d18e25cb84d06d00e19addb2ab0f173b2fc04148e59135038ba1f800777be012587eb42406c847d06bc1d3b0bf8c4d81740c9f2376788d2b2bf04dce16d002a726a6bc05b885e0067e472401c3db002bc82f9087820f3c11005db610ee82edad857487782eeff0262f1c20be0d5940740357227fd1795c9cd0b439452af15406f5f1bb70aec15cdeabce0027b45b3d337c085c102d06db74dc0936d3d4019e83705e5ca8748743182361b022e9422d145236f5101148eb70ca042e9a70d783961015038921b439441d7ef80e2639a001e1c8e971d33783fdf803bcd5340e1585401bd86560e50d57817d7a8c724e82fe0ad1913c08def4e4094855dc03b86ae014f92d7011f92e0e2225a8e93af9f8048a75d4051d88e7f896660be6f58e4bdca3512eea2a05c79508bad4a97789e01b452f67101e81a9929a056df66191093b16ac354e9f00ece0015395af878028f7f8c013dedee35c0f59c136e952deebbad219a81ad12a05149d7ea55e61bfe2e010f0ea7e07e6513f040552fc0496a7a07e8dd6ad9eb09bcbb760378d6ebcb50f4cc48e50650d0734b4011e47abf4a97b8740728e8fb16e0b1be26a054588a488e2e71be04a8e8fbbd043cbe1eb1ee465ef29782774f6600dd76df043cf6390a708c7db7016fb99b024ac9935bc03bfdba807a7ce715831b79c9d91ef03ef121e090de009e581e018aa06a4432c5e706704b3a05af10e9018ab15e0a2a2db54ee91f44236f3a00bc76e30570a27b02dc4ece0644795a0214b0450d5039e6267e95f234f707280af76f8012dd4ddfe00276b2de03ca8493780dd1c87bfa06948216ef80c2e1d19e2ae569f1c310a5657e0678622832503d466aea80ae313a002a931f7a8062bd5d053cc11d45ce244aed628093653907a82c9c8f0117d2f1f8313beb2aba1a932993fda72156427efd021ed9543ba816839093bb6b40fdfdc31150d2de0e00a5a0fa2de0b54e8f01b1224295678d83599314748db36740f1d1fa0374d1b327406fbf940f708ddc7548995df16039e7194c741652213dcf603e01bcd5fe1170b1750be8b67bee1213ed718d280b1fe78087079b804f90d901bae82a1e9ff3b6b8a8eff29507d4525aa6a0625c07199f4089aef76d6099560b5016bb9d006e6bdf037a2fc52aa0727dba03dc37fd00dcd86c07b874687217a7d39b39e01d986340cfd21d01ca51d595218acff503e070bc026e5ad702a2f87c079491754479213d13619800cad9f92de031b608690c31ae478022a899053cb3fa082860d3b868149faef86a149fa4b158087ae33c176561d653f13546142fbf018f38af0362647306e82ec32ae0367f13703db70194a3fa91825c5a663b370111c913c0e3b81dc04d6bae11a747450a8a69637ddbe73f88b1ad3ca03777f70528a4d739438c28d6af0cd147fe3b37cc63a5acde0b675524ab922186181b4b40016b7c1b62c5cc575c2376b6952a807b7c7a160e7c485e17800a037d74a8901ed6d0df196266e4b804bc8c6f0a289b2e1e01055ddf7e2aa41f612a1701153917d50017d2e77d40454ee308a8f6689c033e65f5d910ebd8064b40254ce11a50e158fd03dc505c004a525f4f8052f23a02160dc5d719a0e430e7b6b1e77d6488e1c1b30aa008eadd02eebd2e012f278c088a86e2ba043803715137bf922ea008ba3c18bc5734bb3f037cf2e425a07094f780e770360191c53e00cfe1cc006f41ac00de3b7b0b28cfb5ca802a8bd28321cab1cb149cd5af018f7cb7031c304f82d629c79653c02b440a80b7dadf020a58ffcd1027b6bdaf016f0bbf05bc9d6104b8e88b57c93935e7800236be037cbe4307d0e17b3f4dc09f9dd9021a2e6df5002ff077e9900e0fb600efbde717b1076f0f785f52177069d90094a48653c01fb6ba01f42a6fe3f16352e739de5c94a7cb1ae0f2f4087851daabc1a39299de17e075065d40754327124c2cf6491ca7ffad5254265499fcdf91212a93974d43cc56f71463ff1dd5a1aee8d739a042fa3d0b287d7849737afec7c527a002f6771ce0926e1ed788edb65e4ec8c91cc9fe05507171770378a8f3117029350870241756800765cb863866e3ef0f5029351b035ef49c07bce0f0035092dade06c4a2e71f40c5f8660178cbccc81013cbeb3b60a602650be8699b09a042a97f0c8863366e0137ae6a80877e7f0185a318afc18563f233065c599401154ac939a08025178002e615771cc491d587e00a93b4d57709282557eb8007bb4a8072948ed9284c281cfb7f80c2f1b6005c6abf05c45cd20be0b1be2ce0199a39e0d6e723e02d0013430c31f22a6bd1359f0544e33b03787f4301f0ce832aa0bbac22e8d125ae9f01ee121703a21cfb047497f223e0498c5b40257f7307a8fc388f54e84229fbf307686dcf340378c9d9da1073d13ac2ed1f441974a7164af2af91a7eeec786b88dd4fcd9921ce1fbb2e032a0c749cc3095418147e0d31d3fcf60c28df1eca80caa0f34f40f9f64ea990734892df22a0822d1902baedb205e8a29f0e07e598b63215d21342963b4013329bb84b34f2b4bbb6901e0832bf057c5c501550ac5f1c0c2ec7b25e39cc092193e91fa0f8387601072c0738bf6c036234b00c78efdb11f00ccd0a50923a16808a0ae989a114a746be002a723e4a80a2b0da02a6ca953bc003447780827e8c588f0eef826bc4320beee222e7f1175073a394fec2ebc7ee0d65d7843a81ea041aa9b9b800547b14ae0095eb770740c596d7a17052c9e06b0df835bc07f8357c730de7ca32ff12c7484e01af0bae01cad9dac9554818737c7a019cb4eb80e74ef6800bb6794094a72bc08dcd19e0a6d32fa0c2604b38e2e8e732e021935bc0c3b6f1b2a3399aed030ee927a09036e259a2083e5e016eaf2f00b7b6d27f5172d8ff026a163f45be8d599e621f5063f32507281c9b780d5148175f01cfbfcc01efb0ab02ca51e7f780c251a918a2af5ece07f82ebd02e0e1d2b62189f5522340afe126d2184b239d0a0b313bfbac87e3149a493131c420a44e292aa427c8b4158ef4b3aac509a0e2d383a1e9212b831da0eefdf900f016d525a0bbdc4c03fcf8ab09e0faf6cc508c11d65bc0c76bad000f423e005e66710da8b1791d212dc6086b1ef02e8a0da0d2f27c0fa836d5f91f85297de46117f0129a02e0a3f5f2809b3d4bc0ade0568013ffe31da0f8b85f036e177e034a1fb91b43ec066bfd02aa91af5350c1f67106b8607b05941b2eef036217c50ba018db5f032e3f7e00951ffd01a03e61e6cfe0a5d599a73ce035135780a270bb0014858b7360a1a07f04c4c0dd0e5049a78f421438a865d27b029cc55e0165b17e1f5099ac7de285f46c97d1d1e0453699e20be03dcd1940416f540145f22152722d0ae925e089a102a048eeff02ee78130e077dd80754483f350d75d7ea5759c001ab01de43330c7036d5172e4fa064997d0214b0e412f060577a0d9585c5674001bb8fbc1f65e1f30e501abb4a7fa137f7fc05f8cb3d1b40772947c0627dd0f20e50a95d88e222c63eb9a85bc1997a3c8b0743937735cfd3e33e365d40055be56858787b47e70950d3e97d022857de6c0195635e76c2511d49466f8e7337126f414c8fc8e88e01fd42e76c16382223d3497fa1907ace333d116317178d02769e003e09bc067856e31b500ad21eef427a4446f21ee077bb2c025e489e01bcb0ed0af080c83de0e646dd103334b733c07b9bb2801bceb7800276ec07c4f2861ae0d51d97809bf8d780174e4748cb396f506e034a1f5f75403156bd03dc0efa023c0f340f7039e64dac1ca2919d1e01c5d8f112f052c0888f68392e6e0185c37d314ed5c82ee7804704ce02221c8f800ba53ca0707c3d032a94f4d1b21328c69a2dc0aff2c750737b3d93007ab73777809701670077ab2f02a2237105a894d2371f0bf3743e7b05b8f8fc065447cd9c09e3fb51139d2e7602af4458001e53ea0444382e0127a973c0a5e515a022e7f11b701a8b8c1c2dc7d53be099d57d40149f29b829d9029c4e9f013dbe3ec17502d51e9ff12c491cb0f40778674abc8624d6e2cf008f8efe00be6d09f066b825e052bb0c78d97c44e134d6a144f9e1d55093eb31e0e19f8d210e36d237b90ae937856bef017eda9c22393d20e5a20c78307404281c6fe7011e43d147210a8b7459d215e063cfaa8017d6af00c5e9e767808b1c7d21a6b088dd8289d7b1a54797ecfb8072f6cf39e0f5da13c09b9df20151265f015e187b043c6b5e009c2cdb861811d017fb4ee063e0168007ffbe013736df006564efc2e270934c69032849adb9464c3df70127a919a090de268658483ec8031a2eedfc00aec5d680cbc26e40f44d537099fc07f858ab4b4001fb4e7fe19663d3508965163f8062ecec157003ad0238603701b11f6806784e9c8bc67ec25fc01b6206801b8a91926b6e08949f01af757a071c631bc06b252f02a249cb2fa235de065c26af01d5628b73c0c77d4448e39b7eb753c0ddfb2de03dbcbd0027a9ab16e0fd1e5c34da1f6780f7269401d50dc73b4031968b6789a9a35c043dcae4fc04f08cd505a0acae43cc4fe0cec803a09e67a96670993c291601ddf621fe251abdab11e051eb2da087cb34019f5f180173019bf57aed0505ec739496b15e6adb0514419ad429e638d77a9737c41977fb0fc01354e92fdc6419026ad2be5f027afcaf26a0bbfcad049c86a3cfdc17390d67a233aa8a1c7e93ec6a0171a25fdd1067dd7407805ac1bf3bc0a7e0ed0195eb3a80bc989e75b3ee05c476ca2f403d8b73c7076754ad16801eff770f28e81ab42f724e4da6920374d1ab2ea017f532069470f5bd82628e91ded916f0fedb5bc02dc723e062fc1d50de3f7b09f0d32eea809748fc020a9806214fa0645949afa18ef72c208af1e91050047dbc032a941e67804bcb78950c66bc024aeb07ee12c33f5d4089ff6f02b826742aa4187faa034a309906e0f5c93f80dbfcd300072ce9010a5833fd855b8e35c0a3f87dc0018b48ae79daf8a10c7811451ff0d95003c0ddb778fbd1b4d6e474912376b25aca7302056c5305fc59918a21f6572e9680173e1601efd28ba41da5f6c723e0bec735e00191045093f67801f8c39a4340496a5d0794d575eafd3f98c40c5e03f082c3c8b7b124e0e515f00cde0850c0c67bc0736bf78002f6d105942b5fea80fbb751b0c584ddfa13d0bbddce00ddf66c0e78c5ee06d06d77df806e5bbb3178402459fc015ed01ea543cc0216e36963b9d82e07f8349c03e0cf11468ccd6315631ed0d35ee9b6794648ce3280b2fadb2fa014d455fac8c7de84c4d923cffab18b3b40d7d027954fe0c6f720c089ee3c67885196ed05a094dcbc02140e7d4feb3f7049b75f02dedfb0071441b7df8658cdd05c012a2e1a2340f5cb4f16f0b2829f00d762fa10cb09148eeb4740b13e88c78f45ad8d045038aaaf80f7efcf0105ece51e5072d82e01951fdb0898ab82e4720c28c1e8f4a81328e1be8c0065647d06f0044aa79988e4185429fc010ac7cf3be0b5a38780688eee0187a30cb8ae2c192af1099d73c09f341c01eee1dc006eafa7d770411f2f9b2d002f809bc57d40e198dd018a8f451350ef351faf32daeb6f034021dd2e00af52bb03bc3ffb13f0745cc44794fc2f8f80db852bc0c350e780423afd01fc25d6674045f0f32ba0229814140df8c208f0684f44728cb274da8002a6afba9dc08b5a7b8087081240e1584c0d93a8a336804bfe787351f2df5600efcebf06f4e6be0f80770d8c00b5e87fd25fa87bbf6e039af5da150c51f237cf0177f096805e43e100b8e7f9052803e5d7863886e55806545cb4068017fba4e0d9ea7816ef8ccd9e2d0005bd3a34c4b101d54841b1ad62a6c644febf230d4fb79de81a058ed5dcb60dd16fd0699e2750d25607ef041ea4564d58a0e46f6e0cb12ef8760e785bc519e02f6f1c01b7e877802a8bc634c08f5f76c068afef5f0165b17a1f5092baaf06b870d4c19b450e9b9bd4cb8062ece61b5016ab6f0d31ecf25e07dc47ce021e86ba02bcd6fa02f0d816e0872b7c004aeb8f5c34ce2258004a1f2f35c023bd01b111767705281cd7cf80c7e84b01715c610a8a207dcea3c8d97219cd7817395b6e92e902de7316ef25e61a9fee01ef036e032a725e1e021c8ee502f0c0ee11503812aee17068697591a3e432d76f8022281b0937d64cfc5400cf677700f7b4e21a8c7caf015727b78047f2fa806757068002761fb9a1ee2455c8000ad8a6062869bf160125edf56f80037639041cb016e00e4d0370b1f509286063ee12a576c45814d26dc04db87a1650c2bd7901dc6f48c14babfb80e72b230aa308fe6b035e9ebd04bc012432e1d411b4e8013e492fde6d0c637f1600076c0db8d25a001ea33f18e2f304fa526f313d26b0330494c69e5d7e14580ca61c5564d8e5a8db16632971a636007ce6df25a0926ebb01946fff968642ce23797d4075433103a8de7f9d00bacbf61cd05d56e945dd0abe043c237116108768ec0d3181797803bc24710ae8a2eb1474d17d21c0177dab18a2e1bc7f0614f4cc06f0299ad7809a5fa33b40454ef202b8c1da0cf06d75be659193f492cd1cd0cb76eb333d49cfdd842263285a5155e468bdec710fb8d22a021e4369010ad8cf45408c08dc01de5dbb065c694d002587e41a50997c39053c6bee64c904e6b008b8addd0314859977c09da26e40b4f93f01e5397d11b6c8597b49f31cf0b28237402db6e727438ca1bc4e01d71ed7806b0ffe250ae95fc065e11fe0e30b7280f7691501affd4a00bddbabf45f7c4248bcfd28a4fb4f80d7f644ce66bef2087856a301b8906e022e948a804a4b924314d2e5c81e13bfca9b1494097fca80b774f701cf26440445b9de1f003eb7270fb8f87c051485f32fc05138067c4e4d3cad87d3278bc86231c579f9037866b50928ad1f2b801a68fd09a014d49f02ba6d610878055124ed691c6c14392adaebab03e0bec70ba0a77d2902baedd90cd06deb91d667d19f5b007a96cd1fe0d9d9b8e83cbe243000f4b20f91c562602617b972e11eceafca538e6f4c74506d91b31827f7f9003f5c4be57a7ad2e2d900500ba5f90428c66ae786a85f3ecf00a5c2871ce0e6681b502da62534458e3c4c3a0e18dbf68e1b40f5cb471d50e9e044979e70a82d44c5f484c3b70858d42f2e0cd2130e331f80fb95778668d167ab80f737b400dd659504c45ec039a0b230e11a715a740ff09759fa80e2341f018beaa4d000549b2e7e008f7f5c06c408fc0650386e17801a57f7b78067789f00a5e443d71027b5ae3e0045d06502e835ccaa80a7068e0131917a0e78a07b0ca88429bf02de9a710d28377cde186a7108e01c50566f4c001f08320454cf755a8077f9e6012f6a8de4c0480dd770d05fd25f2805fd5c01de049f035423b73380d2bab6a9154bb17a30d33b037c98581df0a4f03be062bc19e054d8dd011ed6ff035c13be022a727410fa0914c9fa5869b1447572c8018ab166067038229dba3ac90eeb80f25cb30c281cfa8cf90954ae1fa680aa9366c447e2701cff0025ba7ee4a8a84ef22bc045f005e03dcdb7801f7f08e8455d705be786e50de0b676c5109f3729de010a58f5015092eaf38b5803bf031cb03ea0c79f8c0185639c01267a2ff1a2e22cb5e203a0bcfffe0a78c5dd27e0de7c03d06d17515cc441e8ad7340e96313175dc4be79ffcb7f1f8e50381e0d258534d12ab56299bd91da045f2cc7471032bd4b404348d93f4089ee7903a85a1b5e1ba2b268b5009fb43805bc3ee833c0b15ec9032e0b2b8658f878b804f42ab5d9a9c8e18cd9da1450b5964d0045a1769217d3c319272978715cd1102bcb3703c06796ef005749cd80d8f5fc007804fe09f0f2ec1ea070ccca804f8c7d05bc65660e78237dc4076b6ab68077a43601d7511701d1c31902caa61fcf80de8bc71c398b31b3fd0514526d653a81f658e9a4a36299725d1fe82b96d311f888a028d7f5cdb6627a16e3cb0070a377037801e605e070a4a0182b150c31dab37c0294f8976540c5c5a608b8b888088a8ec4ed33e0186b030a69730628a497b78072767d00a81c9b770d51d0ebe39c2750c06e6e01e5b9463c6df41b96034089ee30065ce1340005acf30b280acf76809ab45a847502bdca75bccae826342b8092f66e0cf86cca3fc01bd97e01bda8f732a00ab818b11ec5783d1e3f46f177fc22e65e33802eba8a0c1443481f794011b4de01ded798fec2bd8207c0493b4a184fe826df1940c714d71cd27f45b0ea863b8d6c72ace6a4f512104b9a75170ecd9cb802ae305dfb3733c43720f655c0a33d75c0d3b5bd00c7d86a00e8693f8f06b6642e00c5fae006d088d1ec31c02dfadd0fa0bb68cd7791b329338d7b431cd0e6061a67534eb457a3c8d99419578d9c4d996cf78012dda406786c3c0ff804bb11e03d23194339be5136003cd5da06547c96fa80f25c611e10c5e72be015776bc0eb0c6e01054c27e816d3d32baf22e81e0fcaf6fb80cbf51de091ab25e0f23422990d427bc0e5690f5061504b00d7b7af8032d06e6588f1209d75534c8fb33cfb00bca9e63c200e266901dee8f803784b157771a154fe047c0c5c7ad1539ecbde478289c6771f707e7929006e7d6600dd76f80878306306f8b62bc0a33d91b4a3901eef013736ef00a5e45e043d6656e7194009e658053cd3fc092860b74f80173f45be4de2ebd8af8002f6b402bc543452a19be7d943027870a70af8e8b43ee02534110e97c9c9eb11d0d396e3dd46d3ba3202f4b4d53ea0dbde1701af207a007cc041bc975897a3f3e94ea042691e054a34becb05c003006bc0b31a2f806edbba36c466a76cbc86687c2f5b80f71524802af16f35bf2aff7d3c4f95f807e0f350543556e338a944c7e314399934a31d8745ce1dcd3e2680deeddf53400c33ec0c51155cfd04b836f5846e95fda63f7dc02b543380979b6e025c04371a802be0a2a114d30b75c0e30eeb80985a4c41ef65f20c78ca66067801771f50e25fc6b3c416f7b73ae081aa0fc0e57a0bf05afc6140b43e1b808a8b630f50c0dce84d4fd1bcde007ab7dd09e0be69c91043370f29e89084ce165079ba7b061c85715b866eae009f88b104dcfa9c033ed4ec01f0414f9f861887714fbc9a4ec6de015e9812498ab59255c0e5fa1fe0b592f7800bd8374039fb1029d9cdf34c6b0f381c034025cc925f381ce35fc06fee0bf0112ae92f4a82f4176e8ddf012ae9660f8032d077e4ca18e75fad0185d42b66aa8cc3dcd6025c28257d4021bdcf002a94b2434031f692fe8bdedc7c0ef8348b73c03323912ca3e42705c53afa6e648fa80a6eeb80a78eb8865fe5fe00b871b50314b0f51450bf6177072860f9888f697cfaaa0af890a6c780585afd0e7839d00a508c5d8d018fa1640085a3f902281cbd0ea071cb4d44e12c5643455a8f7ec3f70e70d1f7012805e99cde13280555e2e1e6f12c6f80877f7280ab9309a08bb6d25f28a48bae21367f6d2e005d3492f67fdf2f55195430c49725cb2e830a1e44983c2a9bd6620222a32daac5f460567d9ca2c8b1ab197d81bdc8b1abc95fdd102346fb77c0a726f4001f7ef315e0fa65b5053ceb75668825459346402c29ca039e4dd8022ab53f6e0ca5f83af610502afcbe0214522f8ee330d3c96f0bf0313dfc8bdf6db106281cf335e09d5c4543f43db21dc087771e011528eb2fc0ebc6ff029c6fbd4493b34b075f07c0eb71ab80fbfb3b83977966fbf780deedfc05f064fd654054e2fc222652cf00dd76d507942b9d4d6bcc342f52507eb989f7e2b9824c7f03b88e4a7fe12ecf6340ec5b3b000a9817927398e9e0eb1b50259ef400e5978f16a05abdd804d4f7d8c4db8fe5403adca4981e77fab300148edb2ae06d6a6d40efe52b07281c5f0f8097cda7a06c5a8b44173301db35e0823e7243d4409d22e080e50005ec760c28829adcc56f6eb904bcd120fd850ad8f605a061cafad41055523502c6d4f32de015ff73c0bbb0b8460c4236009f06fc012860ee8bd5a881fee2ddc6c2a5ce0cd0ab9cc535a2fff27804bc21f7002860ef7f800f0419010ac7a40f281cc9ca10bd93df23a08b7a54a146efc43bca6af42cbcdca3c6ea52afa5ad7346f7b7cae43a9bbf3eb386286075a061b11e67fc27bb3ae0cd5fd7802797de037c51af864a8f5d5d96008574bc039492afe2b6b1c6c8bd020e664d662540e1f0ea520e664d5eb380d27ae52c2056118c01efaead030a5837011485ef1dc0dda61f43393e33fb0aa8507a7b03148e8fb84639c61c9f00b76112c03d8b1da070fc3e004a30ad78963852c63bcaea8c18951b806ab1eb1fc07bbcb7804f280362a9f907e0904e000f1f6f0065c2b73ca00ecda86b8829dfe12fa0bcef1ab99eae20ba0a8819de67c0d55a037031de04bca16e05783abf66f0aaceece817d09b1b9e013e38af0f78fde967805fe57b135004793f503ded6a1c0195eb9b3b40015b7d006ac0271bc0eb3f22a4d1b3d85c025eda14c92166785ff88503b65c01de77720bf8cd6d0195eb8f7d40015b2780125d756f881dbaefbf803b89513a30075c0114b0f111f0647d15f060571e50c06e8b80cad3dc1ba080b5bf0c0c4345d0a3183f3f029eb39802de17fd08b8e2ab027a732f39c0e1d8030a47a90ea8087e8a54183d8bdb7240d4b78f808718a3b88861a875247e772432d57bc067546d01c569bb0db8679105dc3b2902ea785fc46d636913c9d23d8bcccd1af04533801e7f5302bcddc5c9924a6b3301bcefd5854121f68bbd960095633a88b438e15b142ba58f099f7ca8f5037c171de255e458dee46a6d88c9e9ed08f091655b40177dca0644f7ad06a80bf8fd0e78b1e095217a16a509e0f6582ec095e7571150f6787b075437640a80eeb2aa1be2109e651bf088621650e7eceb26c0cf3259036e271f002f6acd007a2fbb0869f41bcebe00efe34b7fa138edbc022a1dbc809bd36f331747c0fb801780dbc9e780a7281e0197a75340cdf3e778b75103f51f008fb11500056c17498ab1ad1f40011b1e011f0694fe8b57ccac01af311a031a88387b0714d2e1b7a1eed1c0e125a0800d234945cfc22b113852379b1c00ef8f7a013cd8d5027c4a511d5026cc6f017f64a76998c42e9b16a0f6d8d323a0387db90e882ae91c70c99f01bcdaf61bd0bbcd6d0145e16a05a84aba2d018ec2b84b8c6db576011163d7809b81bf808be032a057b91b03aa813643c087dd8e004df9b6e33544cf22330554a05cc7d34695d4dd035e45f0097842a60e28828e35c05563017027310f6872fa7a04e8dd26434093d38b0f83eba84c7d1610db4c12405178ff0aa86e38ff053c6e790128d15de50025bacb31a080d57a86b9a37019692ceab9cd25a0083a1f008aa0661bd06d3b0540b5c77bdc36d6e3ce22d1f938ba49660ee8698baa6f27ffaa2475cdf5d9881378e0ffd650546d3a3953719130fae58e447a18f2c51ef0a6ef1aa04a4bdfe22c72f6f124f70d784eab02e8356c738638b7f8b10bf8d346af80f2ed6800f888bfbf00bffdf91e50c5a7ef261439b778d2be04940a7d22467ac4b03ef851e488e16c670db88e2a05f8b6c70470ddb004bc1ce81d50b74947519e40e5c74bdb1075d4b403787a6104784ce92e2006bbde0167d31ce036ff19e0d3d6ae00cfefaf001f1d9f5e43e5586161a0d27a02bca9a606f888d02fc0fb09e355b2a2ea08e8cdb59f0117c16f808be02ea00c944d2faa22b813c92116502d16800396fec2bd82cf0007ec7e0a78f571177047f30d70d17700fce58d1de07df37943cccdbf3700056cd80614b0cd4d40c4d81ef0c8e615e018bb07d41e3b7b0354267ffc018ab17e64e418fdeafd010e4764e4a8b4f4599113281ce505a07054ab80877fde016f007901544a9d75019585e528506227c6a6017864331e7f1a01fb003c803802dc8f7a029c2bab8017e9e501358ca63d401d897c07f027c92e0cfef0fbe4f008f8dd46fa884aeb690878b26d06a8b41c3500ef282b018ac246945271c6ffa604e8555ef600c5d87b1406d15b2bdf035e00b1037c68d50cf0911003c0cb805780d7f476017f39b06f58c406f61ca06b641f0027cb4bc09bf153509ff0e61750ed5176d0995c3afe02ba68bb0e786ead68882567e5b1215665b9e7f91fa8c972aeda741a2b22b223bd4a0edc9e1496018ec2c1d6101d3c1dee5ae4f4ec8c07a9d3c3b2b35d439cffd11f03de8a7001289bbeed03e2789c0ca0de9a17a6706a75f2fd6188ced9fb2da0872bbf010ad82c9e257a6b2ffc2276eaac003dfe45fa2fdeddf20e78e9dbc01035a1b764a6274ebfdc001ee98ddbb2c1f01c70e1780128efebfcc2e234ad092f015527671dc00df8b8464cfb5c3f007a73992ce0185b025ea813af212abedd13e03afb1750481b4340215dbe020a692dbda88acffb0ae0fd5165438c289e1d002f8efb065cd0c7e3474df8f809b83f5701f46e97034019a83e04bcec75047899f803a04cf896fe425b00567786890755764b40b961dc07941b3e2f003f0b109de62fc00b535250ac1f2680627df807f8d4a612a0805da7a065af8b14544a3dc6c345f59a8fac1e95e7c717e005766780f79ddc015e72f6047803ea02502975d1055447b52303c582e5c747c05beeda01b131e70b508b7ef60ab8602b008ab16a0570e7fd0a50b2740b654ae5791ec92106213fe2a2b1bc6158025c898f0145d0fc1b7095f402e8b667916066fe4af7ae6788fe5cb606281c5e7d3ca583f79907f4f87f4d40e1b84d41497b760fa8eb751ee188fe5cf51650057c95073cd9360394a31e3a808f94c902ea344f1da731b29929e701ef7b7538587731c8022aa43f7b8007659d60fe752395f8b34e1fff7dd2f2043595c933560fbe04c4f9eb5e383d636be8431e5001ebeda533160bdee40c714497b7caced22fd37e05b8067a1f00ee89570cb188e23503e81a1763c0f5cb2a2066126b8007433f006f60ef19a25fd9cf015e33d104bc01f521200e7b79025c9b728dd8f9780e78437ffa2f3e69a061a8f818fc4d1f506e38f400f7c453f0f7803b809fe519f0f15a9780636c1310f55c1df0845d16d0b37ce40125ba7a09505a2f4d01b5c6bf9a809ec59bad6754c02f1dc02b446e02dce8cdb600cfe05d00ae80fb800f34bc0514306f4f9fc5dac06ce31cf0d2c8f780e8be8d014f83bd016ea1fc025e77f1009415d236e05c5900d464e93f1aa226f48add1935a1fba6336ac2c501f07aba0ce063cfbe011fa0d30094e832dcd6efa517bf486288a003789e309e25a6e32a6f80c7b5ef01afc54f004f60f6018f08e400054c9f313f818aaded952106435b79c00bb95e0222c69e01cf484c00159f67e78073650350382ee6808abeab1454b0e522bfc4ba8bec3be009aa23a0c77fdc03aaf8fec6806efb3500f4f8b91da0527bdc35ccdddabafc027ce2f437a062ab18b78d5e636e00b88ace035e5c7b057842770778c1d025a080e523e12e626af100283e9e52d035ea8ed3587632793f071441ef7e95f4f8a2e467fcd45b33668c9fee9a80e7f8fcb41ce473ae08e29320137dedb7987e12e43afd850fd0c919a20b78b1063c1fd508881a680228d11d0686183fcdd601ddf6a21d1007d74c0105dd6da9390bdabd9936fd6a86bed55ae4ab1989378ff2d58cc973372076fad501a56457d17c352379ef012ed8022ad103be0e704b297b0f3813fe00de093a06f42cf739c0a7581500afa48ea78d4301061dc035e10da0fa651a4f1bfdb9c600f058ce04708bfe0350c0fe8a80f7f15501054c1f093d815787950cd19f2bb7018f16e701bd97ef88b1a881923da090362e008f165f01ee796e00e55b9f903aa706f2c2d839fdb9e2c2109db34d1550c0f431db225fcdc89efd00ee687e002ef9af01054cdf5a2cf2d58ca4b103dc798f8b4685738c188b81cce306f06d3f0125a9e401f0d45117507c14b6800699bc6c6d4e5fac3502d40ad617514ea05e413b72e5340ee1390f70c02e3b8002d6ca024a52eb2f40012bff012ac77e6e00056c97fecb447789878b85e4ad26a0dbb6cb808acf4a06d06bc8de01baedef00504a9ebd1b62edf96a0fa8b818960115f4b36bc00b858b8037065f02baedf606f0baf10869ac2139ab024a964912e0701c8f806f5b0214c99d02e0be18178db99321a0c9581fcd396795c9ead7100b1fcb39c0efd6a9b0e02f804c7e9cb40b55d795cf2a7117b969ece5e91a62a457dff22d2ee218b8c4ebf9f9004ab6b330c42ca08fd6e3e325c9f76b80aba456db107b67bfca808619b21540d758dd0638497d0f0c71b87cb101e8a23efe754195543b02ca2f8757407779ef07449bbf0c7823ec0e501acb738d387bb0027810e1dde06e53729907148ee310f084dd4d40ec37ed03ca62cd0fc07d8f1ee02d6625434cb63dde02ee8b15006f738d18e30b2037801edf2b54d32f8034b8a8d3fa7d02283eeea6062f08c9dcbc02aa3ca7df80debebe44728285c21177892aa970067838ac0c789f56fa2f0ad847015026fcba0794097de2f482f1c2ce27a05185cf1fc027c7dd015e5ef91b104b35ba802b8b1de0558c678017b75c02de57d0013c9bb031441d358e6c1a759437f5f25991aca75bf8ac48b690009ea17905dcbaa8018aa0520afe0864bc287f733ab3e801ded9360f708e6ad400f7d6f6807276e90df0d6f24b40e558e71e5059b81c02fefa71c610c3839db82827d97c020a47fb12507cb46f00c587f7de2fa24acad65e017fdf34de4b149ff53f4077f1d9e97c88257b7906f8b6d7809e76df0114ebfd78fc180dbc7e06d44e6e7f003eabc2c992458b970f807ee1499d457479b21e425a448593f808a6ff40e5d84bcb10154ef611f071a7ce8405be77f2646023ec12500d341d01ca738fa7faa594e3bb5ecd5b43748a2e5a808af1ec24c011f419bf882e4ff30ff0745c2fc075d44f1b502a3c5f036a8fade32e713ab2aaf9129f2649b492bac49748b2b5ad81ee4a27c0b77d9d006a4cd46e011f28340314b0c61e50a3468b064ee023ca1f0c51159cfd013e897306a840d1e7d44bb9b42ae8009e28bb05bcf99cbbf869b549b1c46745268f5f803290961495f8ac48b631061441f711d228f9975bc0cbd52b8097575e036e7ebd001e1ebc07bc8a310b288b69856a294755f0d6013c4eb70a885e411f70c0aa8002763f07d40a7ee62e5125dd019ecf3e008ab172a4e424ce42ba04bcf320126e8c8fd52680cfcc18035effb10514b0590ef046d8145415cc0f80d7d1170095859b480e311c36aa002e719b800faff8043c95760db877320354f2eba4d61328826ee22e5e4431298f010f20c6b350f2b70077bc5f0165b1c93da004d348ffc51b94e359e67197c884d15dd9b40097fc9780cf53ea017adaf615e04e51c610e72ae45a80eab96d043d2686cabf80eb972ea0bbd476808f197589fbafa057d09317c013437f808af1610550569fdf00ea24b66b80daeb0b27ba984b9ae8532d27d035fabf801ebf16d7886d59c52f40bfe81f01bd97c118d08059d7698c0aa7e548fed72952b9be51fa28b0165f1fc7fa0f9ca3327b431c3cf1d70794e8feae035caeeb5cfc52fa5980a72f43f4812e0b80aab56526c0af41634a254ef04fbe9780b269666d880db93f35c06b14978017c6de05b8d2d202dd12e7f34ff485cb12e7f3672ede0c3ef021d1793927d000a24e5d29713e7ff6f11af0f44227208efb38005e1af90a28bf2cb98b5372a30ba8e24b1e0165d39f06e0bd6f71975a9c6a3e07b45cbd5f003cfbd6063cc27a16108712b50025cb21d788d9376e1b333403406f6efb0728e89b5f43548db516e0ae463c5c3daae86bc0738d5f8067ce4a803b4545c067988d017feea50328c6f429d2132867379e0db14337c90644efb50778e6ac0bb8e47f00bc5cbd067841c8107056e7b6b146e012f001c2111f717ed0a0087898e13b200e581a033e776300b86e7803dce15d00aea3de01cf25d5007f11a50528a4c388d3a82bdfba8047f29a8047f14780e7b47a801b1329f8b66d4055f4246e3b73b3b8bc0d7038b4ebf904ae2bcf00d7622dc05bdd1e001f6df30a28694fd25fb81fb50794a46ea2fc98c7e72bae02dc2c5e3e00ded9760978a3743c7e7ca3bd7701782d4b147d8b88e42b4045df6a0be82e0f7dc0557407f0de8421a0777b702a2ce4e254d142404cd85501ef3c6800aeb3cb806e7b790178b6a905f8a8c11aa0519633bf86182f9c94aa809e365b07bcf9fc13f04ac812a087bb7675121dbcc9ba09b8a9d0001485e77f803718c645a3162b76001f46e84457884a4bdfaefd0f62d2ef06f06605bfa8424c84d41463ff2ae8f848860ad8ffd77724ca01b182a861886f403c5e01fa975919f0995d8b8018a7eb03aab32f678618a6d42c4f29fda0c3a409289d6a635f890f3a6474de78890f3a24db2de0caf30df02e8a474029e8786f6067db1cf09acd7c40f470ee00770153d05d9e8b80cf33e02eb1c2bd07b86f5a32c497d0f469b452fab986ee33a0587f2b030a696617106755dc00ae70fa8057973e005e0c965ec387fec7b34405bcaf03aa800bf12ae378befd2de0a51a7bc09df739e03ef21af004f70be089b2b846f43c1bb9809849bc02fcb403c01bfbfa80d7b0ce01571643c087fea7a08651fbd5103dcff11270c09e0262bb5c0b506ed0f869896f2f643f5270bd5f02bcb1ef002860ef0d40a5c3550750a1741e51388def1227803272b50db8b7160986ce6a15d0ab2ccc014f2d4e0007bd08384e0b805e258f1fcb1ab347c05369f12f51df2e96806ffb09b80977005415dc9d01ae4d234e63c5ffb80fa884b98e188b61cacb6b400977bc065c25b5013ded7805281c3a1bfb04aa810a9f809a1bddb80b157005d0d3b6cf0165d3560670059cfe8b17d7ba5ca7f7da9b013ef7fc0078fb4f055083a45d0554bf44a95d88a3c1179701315c5a06148ef37743547cd52f40e1e85d029e6a75be8dd1d1cc5501f002aa35a0c47f70acffebbdaa569f0c010f53eae1f8d046e6f7d1105f764a52f03ecf3b4354e2fa686a898f5324bf1f018ee49fb521064375d85c890f4b643ebf01f75f9a01aec5baf78007cb6786f8ee9bbb5ea5b45f9982cf40bc08f045d7cf80abc6774069eced13501da513b94a7cbf21bbef02de34f11b105dc073c055e33de0bb7c031a535a3c013eafaf087801e6c01015ce3a5e430c8656c780cfa9b903f4e6b23780f2ed948bc6e2962ca002f6e21c506e68740025298fa0f1b986cc4f0b70db2142ca3afa25a09066d35f78d87608b8469e020aa93ef970027faf2001549efe46d0e308d9d93be03a2a1f1095e719e0f54123c095e72da08015cb80dedcf71050146edf00f74db7800fba5e000ae922ee1207091eb78067ab8f01d149e45f5cbdae578077937e019ee3fb00546c0d4780973645d0a3063ab600efd2ab011e5fff065c15ac01156ce7e945151f8f2b4075f6b803a8c4d5e962ffc0755472f309e855dec46b98477cf4000fa836008f288e01af974a7fa1f2f4ad0e286064e4584e583e025ee0ff0b683074f500b88eea018e0fbfb97f7d427fd97a17e0f2b431023c2cd705dc377d01745b7d25f304be2d7789ee5b1150c97f88bbc444d9a40078c75015f099f63f86f89ec5ec02f007a5ae00c5e98f8be042ecadce2c00bd177d8ef0041e4e778229c49863a505f86cca832116fa65b28052d06b0d5095a44514ff4121a70a4789aecc4a95977743496be0b3c509a0dedaee1ed0bf9cff04b88337e75f1c412f65837b7c199ded524a3fb4f17819e0945c9b037afb3f4dc003ee7f802a2d7dc5ac947e24e3b10db8f688bb447f6ed4075c357e007a96d919a04a2bbb05dc9fab03de40b632c4d2c8f72ce07e431370c06e02a236bd0054590c3e01a5207d60bc947e35435f0c2ea55fcdc8e40025a9ee14f031c589214ed3d267124ae957330eef80437a1de090ee7b800f0469005e1a7907b8bd5e015c5a721767d35109509cbe05781d4aa23d0125be9a91594724477dfbf60378e54e02b8eb550594a4ca75c0ef760fb82bba0314b0ca1da0cefbe712d0b2b5f5a321899d8f2f01f1b21f00eff7a8021ef8ff06bcbaf4137049d7007c4e6f1f70b97e6e70f59a6daf012fa1b90d881edf27a0708cea80f7ad3d02ee7af500359d3c2b5a66f9fee30cd0aec5cc15e0af4147c28d5de0fb5a800356db00ca0d5f7b40a565b2021441bb32e08aaf05f80bdb118533158e934e1406318179bd071c415780bb2b6780f76767014550f60670ffa5082857963780e2631c29393e777bbd05f4e64671d1e8460e77806ab1c923a0c71fb9f8fc5701eb2edbeb00d7eaab3ee06d7b0d40f9a5b307f4b49b0aa08c7cc95d1cc9e5a121b6a97dc73562ceb3d8023c66f007a85afb7339c690ebb40af8c0982ce0dd0b47c01fd8fa35c4e1266597c985a8705e52f05ada1de0b9b52743f4b48e0b40c9e1b30128e85967b1426cd99da8b8f87f7d21a66988affdfef500cf570e029c2bf57db15225eaec64b7013c1b590654676f46010e7a9207dc70fe0054f17dac016f3ebf31c4b0ad8e392f55d2fe6d2b20c693db804f5bdb02aa6f7fd27ff156fb05a024358e5fc40998c918700ff83c201a1333407dc2bfc4508be5b715c0dbf64a808f5bff02dc03fe0b70145e3400970e034071aa23534b9574cef309f026b4aa21865c7fe2bd447dfb760454ad9df32f51df0e0157165f80cac26a115082f150458529cef11cd04ab7eb0bc3243e713005944ef30d40117488088a638addb4e61329d9cd39e08e551ef0f1276f80372b6c002587fb25e075e33b40154e2fe223bac4cb78fb8cd236010f1f3f009ef34cc1f5cb1ed0ab4c3e000ffe1500ef1af8043c2a59374c5d04e75e00bdfdf617a041b7fd7d80833ede029e9efc039c1c468093c3015072f899022a0bf549c31338e8034021fd8cfce21dcb99e777c0e75bc66d6322f5670f38a4bf804f075a009e48bd0714c95e4451e1ab5dd53ae073043e0185f4fe0a50b3e7a666984724bf004ab8ed5580839e2f00ca73ef6b4015ce7e0828a4ed6f4071aa4d57253e2a93ed473a8d854b87c895d1cf7e19018ab1fe1e7021fd09281c775d407d0f0ffefd075ec0edaa807ef67e07a84cd6595925be7693bd2c026e6cf601a5f59e3332b5fa7b21200622ca806a8faf6f4065d0775c236af5af03e03d8917806e3bfd06d4f87e1800aa6f3f7386e834ef9d4ea38a4e96cefb856a8c6d1d0005ec339e250eadf2c236beed9378e91b9ff2497e0f8638b8e67d6460e1d21ba0826dd636c45e8dc72ce0e1e33ce04afc2ec03590ce612df1b59b645101942cf595dd12dfa14976eb8058283c053c63f50778987204283e1a7340cfa2b37f4ea028d4c19ba52a6b8cceda80874b01277eed1a28f11d9acc7d1f507becf007e8cd65c780f29c67bcab69f55a33b0f5af027855d66d40d4b70dc04b9b068052d0ba0f78e1520750f1f97b0f28572e221c7186d9f6037019b40f8870dc001eaad802ee24fe00ca73a332e0752897801b8a634021dd2e00cf79464a8ee1e3f667406c85bc065ce1940185a3f005a8fcd0311b253e2a33691601b53fdcafacb2d3af17698cdeeb1250f6e85e020a692dde5cd495eb7b4031e63158be32932d4c01c558650d28492daf01c5d8620878006001e865dfcd0cb1fcb61f6f8e9af008b8b9918297017f000ad89c8bc6a8f50ef0469448dad159adef00dfe51bf05db680eeb2ca02ba4b2e8a9cd80bb888b41e55d2650950ae5c3f019e27ac022e1d2e004f0a3701b594f26a2a54ff75567d885725208e2ef904d4c4ef5e01ae816a805710d501a5c2620abacb283144b5d69c07b8946a6d014550270ba841f2fe072839549d4d0bb152b6e302a510338997df807b9e4bc0abf53f01bdec5c3c5cd473c53ce06d9df12c314c797e0fa8aef4f2fd6a217a27a50bc0adbe08583dbe8eed9c5d880245073e94d2afdd8ccf00affdba33784673a26f94956ae97ce513a012b77405e85596cf0085f47569607bc719e021a47e40ec4ce11751ad3d024ab8994f03639fbd80580ef40eb8e9f401b843930354570e5a80825ef806dc6f9803eabfe813f5a55af4b4b2fd728053a117e8a6df7f597700e5061d0555e2fb2f590f77a4df7ff94aff45e1685d002a939f6f0077012786c447b938f1d7a2a795ad9e03fe1ad230c0ef36b302bcba630638a4e92f1452ed6c2bf1419844074e95f8204ca694001a657949ffc567204682890532f97740216d6f02a2f67804dc5d99031ed7de017ab7ddf41aee04a4ffe249bf8161e6e9966aa4b1a81b86ef805ee5a40cb8e799017c0ced2ba017f5c15d9c91752eed0954b05dee0db171bcf30ea895a3af119cc0fbf7d37f71c21d02ae1abf00ddf6ab05a8e86b44d28e91cdf604f046d843408ce22f0077460a80a2b090055438fe5d021a92d776fd528dea64fd1d101be973802b8b23e099c401a0b6e5a808e8ddde395772628a9768d698494ca6803f04f70028c6ae9de8e8145507804758ab80d7b2b4005df4f2de102b3217af80dbc95780cbe41f43f41baa0bc063b0ce62854a4e09e6e737c06fffe8b4feaf1fa52ecfdf0fa0f870bbf03f503f6a1f911cc33fcd12e0031f5437fcf7352455272545723d3ed790fc550c315ff95a07d4e57129957e52e8f90d50485b4f8072f6fece103dad9b3fc093d33540d7d01eef125fffc9bcb50045903e29544abfdc338e67e19ba003c06dfe3da0582f2d01d52fad39a03737b832b8269c4c6e0075bd56b300df76fb0ef8c4c70ee0b5707dc07da011e0e3a49e01afa64cffc5bb289680b2e9f6c1102783f5eb802be06e406cef68028a0f27183ed433291401358cda5c348a8b78b858caa333ee4ae97779de22e80c64d6018f17de03de4fb8003c3bbb03d447de2480d7085c00ee24c6d3c6471074c84a89cff0243ad6aa947e8667f90bb88e7a02dc47ee015e6ddb04bcc5ec03d08b2a0d00bda83101cbbbde8f188b2f7e1ede008fb1fd04c4b8e51c50d00f5c2382de05bcbaf411f03693f45f7c82cc17e06f2e750095fc37f1a2626ad1e37475eadbcd19a070dc0e0147610dd0ab3c5f01ae802b800fc0a8031a209a350c31a238a9004ac9d325a0dbd6de01af5fff05f4e62ee3cd45e5398be222767364df0065d3df33c0c7e18e0115f43d3f5c5a359e017a7c6feea9474d985c26803f4ed104f41aee7286586f79bc0e70167b2b002a3f567340ef76e11c15d55aa6938262ec7704e8e11e868002f6f4047843bfdf4bcc024e3a7780679a778057ffac00d7d94eeb85986dea7e01ae6f9db4ff557cfa979fb84b2deaca1da002b6a328e4c34589bee25e4a3f3aa443bc4a7c7428718335fde8903edc5ce2fb40c9ef17a05f5c1f0195964f134021fdcb1a2afeb6e0d518508777cb2ffc5eba79c095d607e0f5413786988ebb3e07d41a1f5f04c409cb0dc0a7049c01de5a5e02f42c9525e04f0ae50cf1a19ee308505341c7e394f82e4fe66d0da874386c000f0ed7016ff9e72e71d2d13de003850e86497ca9f71af0eaf415e0bdf70f01519e9e032e4fdf00f76f9f000fcc70db181e1c025e8dfd0ab88d7b6588d1c05abcca984a1b36001f4bb3053c4c7904dc6daa010ac7db0da0acfe5a0054e1bcc45da23ad9b401af10f905bcd8e703f06deb8087d45680f7ef17000d438de2bdc4112af916e091bc1da0bcef853ae92774c63dc08b9f1240b5d8a60c281c1f778638467216b78d91bce11ba054389f003eb96503b82b9a005e625503344dfa1b0937ba5e6f45c0436abf8087077780b722f8f1994bead401ef3ca8014a63570e185347575940cf5277462ec457dd86e3803818ed0ef035328072e5f91650d0ef5a80d7e5bc18a2803dce0057385b40b1ee45cf135675ea6372275024df740cb1914dc758ff07be686e08e869fb7540719a8f6b54e24c84c9ff9fa933d94a6509a2e8c7d49c253d0e515110b145516755f4a2d820d27cfdf37a4eec7ad3bda02a2b9b88ccc868002e4a6539f6ab5fa4a2d7538033d954014ecdf909b016b3b8086b607a710df046b16e107534cfac707e4f6bda065e2e016a9857e5afb0b05a9bd50dc827dd3720163009e0b7bc0f00ce919901a45f7a3d80e658b16810c5b07f00d6844905a067f8789fd7f6e9ef0ce2d854780d105e26470047403c01a45e1763803effedc4e0d092dff7c894e12994ba007b552c035806edca002fd30780bee5ab0a90e49fbe0234c7f639b0493e7f861a563a03d87d3f7fa88463297a3d9255be458fc5b149d92b2bff2bc3730090b41c95006ae9fb1d40938ef9918647551b20dda0cc4f95bc0c4f396650e828a55bafe465785a6700eba80ec0d90ace014e19720ad0fcf82801647639b80068d3fb1a4d8f439112d457f2aa3b271f001b433f016e4717e070a83d4023a79498957f2574748cfc2e00f4daa3784678665cdd009c1e670f703b1280d3e3ac01be295a02d41f69fe16c9f5497c6d28adc61260778f5b804333ee0076bfed01a41a2f6321db1e9475e25bc289e2f50ee078a03640d261b7014816fa02222380fd5687c42c02d80bf5a7006e7aa504f069fe022079daf6d8127c3ee900ecb43801d8b2790bd0e7cffb006d583beed348079356370075722705d86e7907d0caf68624c367c2c98032ae9f9ce7ea17d831b66e711187a2a43c03a80baff7005914f70142c02e1f01fafc8917e1af067224c62d40c39079ddc6a128697816fe6a20299cb2275d29ee2c3ebc4c4b5e8469490ffddd37dae29c550d4a919bf2d120cc834a9af9077c4f3834a846c0761b20a5e5145d54ff499f7f02c4436f00d240167d54ffc9b65700298b4ed7202eca9ca38aca3dd97e0ed05fc66380a6e545bc25eec5de6e004e00da0be0b7f8aa7598df9cdd0334fa2b9ee1a17cc8ffa2fd876fcea8ba93bd5c01ec9d5e3188bbb5cd15c0b6be73803a28fd02e86b6d32a1844ef6310668e456f95f7ca7f50e905a9b160096da3161d270ae5d00ec7870067030fe244064696e036c2fac01b44c5f1e0116b0f92f240cfa33808441ed06a096aaf85165889fe3d72940626b3f0a1072bd07901ccbc6005f2e6d007aed531da091730a95bc40ce570be09dd23e80fbe3fd0ca0b1bd2900d4b0d629a0a21e1b009ce8690c50c30abcd663ab3a45152ae6a4fd3b80b62cc34383507cd910e053e3126023e403c01b813d40021681320aef9f1c381fca274022e728e69893bd24e525405df81e2b3b946796009ccf20ff851a567906d853f61de0d3fc1540fd71f76230892c23fd001194770ff0f5e42b40c2a05502a861bb3d400deb2500b5a37205905aeb6a4b3be47eeefc33409819160087ccbc00bc955c016cb6bd05e8b5bd024062fca20dd0b5b13ddd867989833dc0bbad3a4063ebdbea611c450bf3038037797d80c6a5720f7076d306400d2b5b7b94c29fbf700390d45ec5c785ce7e3d0768bd9c7d03bc90ef016afa677c4b68f5ee1940727d95ffc2a7c63e403db68f9686edd33134ff806f129f01fa964d7472c4ef8f4e017ac6ed11c02df5842945a9b81920b28cdc03ecdc123d6675928e8b00290be5e9ad8c22c75d9a3c184452d5cea58193de654ed234c2cdb3776710e3e2a8a3bcd651f7d5206e12bf0f00eae4e7ae412d2e30f7012243ea026097c422409af034be2582150649003f637d07b09be709405df87e0f50c3de4700bbacf6019e528700a71cda037cc7f76c10e75bbb688ec22333bb18039c63267add3b83acf50150d3ed3c4939a01b551daee4e58086bc36f2e4dd03b433289500566b9f00ed0cbee3f3535f4f5a268fd808cc2b00d74d780c6061709300240c922380155f07a05edf7d03aafab82780e44733462eceb7a52b80de62937c5e0ee82301a883067b803dedf35fe82d472b80c6b6fb0090163b0de07086e4e403e09ac27580067b510678b0e3b5c370f7480136751e01acf80e00eea047800deef92fd4f465fe5035fd253a68140996e21971467e1e019c962603f8be3205a861c53a408aefeb01e010c41d400dbbcc7fa149c7d84649a1ec22805f3bdf01ec2f350778f33d05d81c56034813fa427744c87f9abf453b83f79941441c16e3b5e1f7391902ace653804f9e0700ef3f4a00e9fd81054a1e68500448df36e700ef1df2bfa8a54ee63142239f784a612d3e9d06f0d87686008d6d670c9036dd5d01b43c8e538043fed7001b1172a0a13cb52a284521dad3cf005649275380330cdd03f40c3b608eb8af9cec007aa8e30a46e8ec973e40fdf16269598afcda4e423ce2685e1d00f4714ee2350a57d1e4b80ed086d5be5f2394b853c88e4289a7974380f6524ebc39c29ebcdd1884905e1501fafc9f13802385bd5e4ae155b18bd1b71ccb7cb33a2a4599ea6913a0d7b6626c7def91ede36be392bc12df3229e95b6ada9e8f63ab904de7063622249d5703d205ad025845db4f7a8c917a901a4468863d76a9db947d2c013e01bf01b4c775e81f5599b25919a0dd5637fe127116cedb4311a66cf608d046e0a30df0cd6a03e09bd50940cb74ff02d06ad8760c285f310368cdedc7000994af7b807accc1b454544a5bf95f1c78d10548890f2700ed3f66f19750d137d1eb1ccd8b00ebcafc175a952fb700473dcf007aad93ed8fe3689e0d79ad7d7b3ea3617134ffaa050887907b80fb6300f091f800206979f30ad0d43e2c0174b258e6bfd0422ec7740805fc1d031567f5dd00600799638035d006606fb93b8016d0fb03400335cf0092fc370b03d2aebe01f4967201a059b84b013655f4008ed5f800d8c85407f8ae2086c16e3859af07f0ad7911e0e8c93dc0b7d5558054d2dd314012b7700bd06bafae0ce266f5fa1e603b4c0a708c6601e014196d80132c650087ee3c027cbcb7948a6375e2d3fc388ed5497a05f0e1fd0860a7a326c0fea79f002df5711b20dfe27e006201bf0076aedd00ec97333540454f0076f41b19f8489c5c5e04b0067ad901a4091f3f011ab9ea3b404d778af271a8d7e4d1722c8ec4059b8fc7845564d141a13c9335c049897a00a7408867d41df65bdc00d4635f6f017c14bdaa00fcd04b809301f10cabc6597c7e78645ebc01ec2e7607907add0c003a345f585c84024e4f328004db474c98347244ec00be133f01a8a5d529400f7df78a2a65e16abe3588182b47718e4be1e9d6d3e8530b2bebb50ca2d6e2e4c1c087e6c4e92df2fa51cb63805e3b5f00d4d29ba641e400782907f043a77d8026dd570720e5f97607d0fee3e71520717113df7218ce4fdf007dcbe93a407ccb0fc017a92580fa63d20768182a17007561ed1c2071b17e3720c0f01b60bd5f03e85b86194072bd903fc3068002c0837d0390066a3d006411a8454b337b653d15015a1e9b3d409bdeda1dc007efa7007179d002d8e07e0590506a55000e52cc81d4c9f519c0f99468a91750f20cd002eae4ed9040f14671429849e304e0e22531b678ca3e00fc2dfcc5736cf606d0b7bc1d03f42dd3fc2f1aa8761920a1f4520448045768a997faf91ea03d7fda33c0f96909f0ce600bf059fd126037f12b808fc4438096477f015027cfd300ba8d2cf43f0ca222fd730c76d4a0f261841a54493205a8a5bed3a20655f2d203f830b20068e28ece001adbfb6858b8df3abfc384cbe94d0c94f57ee6046d79d92a96476c049e788687f2e303e01ee3b59e740eef98a0d51da735c18537240c5e59f53ec02a7a05d06bbdbba01656f2fd02d06a702d8a4998d30b57d706d6fb999d0626b9deff017833f101708859fe1717517902e884b3f24ce678bf8bcf0f6730fbd34db8133f790068ae3b00645272cc487af504d043ed0a38c12b6b5b01a8d71dc43ae1d0bcd90124b60e3df97fd5fc9feb6cba1c04f074e8dc026c0bbe02e8a1378f06a1c46b9e4161d7ce7e7e00d26289d76ddc89279de8b190633e154c4a5ed9d9c6f2b4e41d6ce6bdf6a41c37233f4706ce9c9fad27000db66a70577f67585cc6b60c2a76146eae0cc2147ed403f80afcc90053c500a061504c62352ff4a5ace6550a7d258ae4aa52c62b51c6fa2a45ba929ffc173e470d0dc248fdf404d0423e7e0ee039a6d447556a7265b31a4042fa7a05d0d8aeb7000d94e2d5ab07f9e97504d01c7b6800b4c96b5e1b5078e43c40987e8700bd76db06e8b5ca9753a5e2567af60ed0fc28150152e2a501400d5365963fe044820f0611bba2c446556a72dd283578352fc1f5f204f049eb1ca0c156cc48352fc1f5de07d8357204908e6a462747da809b2c40e476390038dbeb33c049010e0116c1f9336cbad901bced390148a0f41b00e986e3f85a27d0c9863171e3045c3f07a861a302403d56ed02ecd2fc06b04bf307402dbdc97fa139f65501788b5f04688e552e01ead3cf99c1d8ee2f8b980ea16ff7f70035bdfe08b0bebd03a8e99304a04efe390478651f01bc339803ac702e0ce2589d5e03b40877f92ff4dade04a0d7be1e01ac5e3b0089be633f14d5d89901d4d2a32d407dbaf35c8f7097c2e03e803ba8370638d74d1160e7fc14a0a1ec8d0092eb9b1380cc0c77538372e4f85f0788640ddf006be425400f7d9f01d4c9bd21406bfffe16a0d7d6e32fe140f5d900685c8ede00be69f64c0eb7e8745d09e02975d001d8aeed19c44d737508905cefb6011acac7685844a4a6f18c4829b36a00ec52d403485a2edb0047705f02b42fac5a1686024e748afe03cebc70079074687879942c0bd3da33405f5b7b02d8576109d0b47c8961f0a9207d480176068ba6875daa155f1b735d85687fbb3c9478776910d5d40e9e03b80b879f0651f8ecab0bd0e9b5f903d0d8bea701ac2bcf9b00b7746d10f54ea6b300fe85bc3aff8026ae22fd7e411c9a674f00bdb6330f10e15003808f4d1d800dcc5d80d65cebdc20ae7cd7b700c78a9e06f096e56300b09ebb0658cfb500bea29800a4916f0f019a41c7258370f552b0f51f503b8ea23fe252589515fe80d6cbd139c021aa3f009f80cb0087ca5e031c1a9a03a7b3fc00a861850091d47d7602706d9f5180b87d9b03eca5760d704bd7006be405c0ce60f95fa42c065b801650eb1e203956eb00b473942be02f08c7e935c0637bb307d8f1a00c50c3d64d4043bf7806a81dbb3380daf11c1d447eba31407ba9d39874719c3dca003e78af01765beb03ac4e26008fed14e02b9bfc2f4ea3102b6a72e09be61c7835bc02340b27d163d6b7d97206b03df905a076fc34001ab9af3e401aa89fff4512f7d1df12b9e40b575d80daf1530f6079ba58039c726809702a863e40e37293ffc21edd017c064a1479f007b490df7e006a7aa50cb0cd205a6a0fa2c2dd2c8015dfa40ab047f714a02ebcba04d84dfc04609d9d01b44cdf2c94b8694e1b00dfbc3f03d4a79ff196c867d03d0738f82b7a2c32fbece26b4345cb44f00bea712b7a18c04a6b530248f4a9b2e42f08c7a5fd07c0e7a82a4043d9f0b4c4d5eb6309f0d1bc0c70a59a19c0f1d98f017ce5ebd36b91d414c30b8094c5d523400f2d5c02d41f47f12db64aa6cd01406fd9fe00b495dc449f0ee36bef017aedac0070de9e12c089381283499cc4ddb038cd27f270af96a802d07d37b0393dfd690788b37acb20bcd414e957a54868d21a04b0065a7f007c467e00482317128037025380ade743803a687c6e1029ec9f1f014ef7d1096069b97907e8b5a753803af97e09d0d47e2d02ec1d164d4f0ffe8c90c95519203df73a003825550fa086c9a9a44a91d0a4ff0ab053eb23c07149a700df89e7c049785600cdc2832ec0a917a3a551c5acd505485a569e0278b0e76380ef1b1a0037ec0ce06c279f00abe837803dee9e0136770c0152388d5380a4e5e5c4c0d6f3acf904f0b6671320ee6f6b007bba7d036c1b3f0248034d8f01d2401faf00fb9fee01d20d9525c085be5e0ce2345f1e01ecb35907a8a5fd37809d16ab007761fe0cc7f05600127d8b15c00a275a1a9ee5ad4780b6c5fd18b938bc675d800d2233805aaaf427d5527e27de04a8a5efaf002fa007807dbffa00f5e9660550178ee32dae5e9a6cde00f6dd88e910b15e9f53809afedd06a8a5af7b80cffb4d80f7410d80cd0caf009d6fbb12b0ffca99aa4faff600597a5f4e0378f4c70d80233013803ae8e803a0917b380348484fdf007aed91bf36b21326b34a000f548f5ff86beb3d807df20600fb08788e71f39ed6018ed2eb01fcd00ec09bef21c06e8d0380baf0a10850d31fe32de1bf3ebb03a8c74e3600df236f01be3c7807d8fe61b1c5edfdc333404dbff806e82fc56b8013a3598efdee61344fbf63a06ca32fdc4d00da5a37a70049bae4072049b788a68755e1ee14a00dda45f4476c592ead6f4b71aefc7a057823b00768184e8f00b68e467fa48ee3bb8e86c5796eea655a0a49d78ebf8483cce61ae01dca8d815754baf22fca7131b4540795f39bf716400af8ec0e6043c411c055ccca0661d657f5e32ad54b335dea54a95e9a9d6e0156388f0035acd606d89bb26710e56f94b5a94ac1d39bef4680708f2a01342edb7380afe20700b554b505ab143c4d7faa0047d61f016c83bd02e8f39b01bcddc80a4580766c2a3e50cd8ba6ee0e025803751e01da9e2b84a89ad7487d6b0334b57f4a00b5f4e40ee080ba73806e79f66f06719dff5c04e85b3e9e00de905c029c188d5fb8e9631e1a26825b809aaed0bf2a5554b3effc199e0e0f00ad4a1fcda9a29adc9d01b4857b8bb7c47663370de0761c6d01568d6d8044cecb0d403a7bf00af0f936265d5c0dacb2009118fe11a0fe289c0234e94eee01de4cd4011aca9714a0fef8ccdfa2a194bfe51f9030e8e60fd516dfc77b8aa626bb2e40536a5b0758f17d036c009803ecd19dffc531231d80bdf64606718dbe3d0268189e87007d7ef318a0d77ede021cb65700a81df5364033b9e786116266f371995b736f59f23aab932ec09ed46380467f3e00688eddb9e964c2924f4d953aab89af38a9b39a744e01da8e8e3a00eda5ca7580f36cde02242e1e3d714b8e6d4ad2c700e12db706a883daaf000d54f312e03c9b1706bef24dee9f0258cdaf7f003a7addee00d6ea6f003d63e90913868842e50020eda130825f103e792fb7007fcb12a07199ad004ec1f405705de219405d588f8645985a7306d0e8df1e03a4d537317251e9bb1b23e79b804c75787f41d8825731c7c26c5bdf02bcbb98029c2f7800b0e5ea04a0e9701cf374e832808a6bfc05a1d57f8e016ad88f854129bc4cc6170019dcdfbd2acb91176eba0748a0a80864f55fc95cfd65fc6e1051f19547830875ebce0076d3ba33887496f70f01fc979f31c0ee844d83709f9bbe03ece1be007833f10870829437803a68b802687eec8a067178ef2d019a85ca385da5bc6b769c030708e5cfd0d836f35f780b3704f82afe00a0b1ddc45fac5e3365d2fb032e29145d185627c55657f3f2aea31e40ed5099956a5ecdf5e71d20e1982e015afb95fc2d92dafb687a5c50cd97006f37ce00f60e5b031c5790017c347f01a83fceee005a73cb4780b61b854f83285df3d80678bbf11c20bc8f5f0076bdaf03d4529be5a8f75a50eae72af55e0bd521c02eef538338782b6ff11fd0b4ec3d007c7add01b4c4be5e004ee6b100e82dc52783a8d53a9c01a42b8bcf003be9bd007c9a5f00340cef0700c9b182ffc281f7660cd06a507ecb2ac55b0be31ce8b5f36f8023fc7320b91ee2021bfdcd2d4093ffe2d8a0e4a379e12d40c41564006d596ebf00d6ea0f00af861f805d136a00f5c7e819e05bf33d402d2d7bae478859b6fa00e8e32ee26b23d6eb650290b4b406a2006ca13d0748253d4f015afb27f15a5fc6a6d94900afecd3178086f2367e611b7d9ab502b83f7c095a211c7bd9310893fc7608f015f80bc066dbe8a0b8132fc7d786b3f1c502e02a88f1f9717a1dc5c84511ead925405db88e7658e4643f1b802fb8bf015a51be38ac84393d5bc45fe20aebed0ba0bfacc700df5918a009c7166ce588051cea1994bb4d8f52835a54532b00b4d7de2f03585c28e4bf9a97aa6d3f19c4ad79ef268027ddc514a08ffb8ab7f87c9b9cc72f28a7fe00d0d8be1d00ec825705e81c95f5013ef10d0c2272ba750d904a52eed22ad55c0b47cd006ec7a802b063fd1740134655ccaa146f4dee1a00fb4a8e01de381f0134938b09405afd2b5ecbb972027024681b20c9af9a18550aafa6a52780cf95f95f647748df01b23b5c950cc2d03d7f018ca5810e034452b31ec03e572d80b3cf65003b3fbd01d4f4af32c05ee113803aa892bf456afe23ff8b83be63d28da206443740b4b405902a58af00bedfdf01d4c9d7f9337c297c00509fd64e014ef8903f5493ee39fe1276edaf6780bda02f005aea677b80fde9960049edc537c03d560768a9bf26004da99b18ca30528f9f001aa8e30e40533bed0324f99b4700b563f40e503bc61b8047ee03a091dbfaf389065bb402585c9cce01f6dabb02d8e1f005e0a444f95f9c646501f056610ad084b15aabe21c37ae060837be1b8053b87d01bc31da021c1d7700f0f6eb1e20a174e771c1d07d9701340c2a0a51a5886cb29c017c89d102e8e3b635803e6e10af75d077e1b31720768e2d80778e338086723407684b6b97c42a86ee875380b63d77f92fa44e465eea79d6ea578037130b80f4edad671095f2b64d8066506f6d1007dec71f803d118e016a69fb05e0d349bcd6a99f93c739c0c94c77013cfa8f0d80bea57c0270baf5e864a7d5cc16ef003b94ad02f8e3d273809445f501a0fe58c7d886c2294f015aeadde88f38364d52808315e2a171593f3905b890935776290a48af6299867dfdee09e0d403168ebf1359ed98f5019285bd2a40bdfee33e2d97a216450ad01c7bd70caa91d67bbd03a80b7d8eaa91c5fbb26a10a9e44e0a012c508e6e00cea5f600b00febcc20f61fa34b8056a51ddaf3dab58a06abe6b56b0feb00f5a9aa44542966fbabdc01d251bb9a01b56be3e362ff317d06e86b0739d0266f790af046600d5027274f0047b6c5337c34cf36d300e133f105b0af6419a0fe1815019285eb0dc0568512c0517a2b80776c07000df636fa23bcf6b6b7009776be0a10611517002bf1738043661a004ddcf408a091bbc981c6a51a1de4a3797216ed88cd44b50770b8fe27c0a6df26400be86d0f706a9b1cd8aa30074867f79700cd20c578ff01c9f5a7e8647bed65be39cb6bc69ed6006a98b770d48c2d7ccc004ec9dd0738fa2926ee246e670f00de4c9400f6fcbb0678efb00768181645800dff4700e765d9005cb9d87f61ef5098070893c902e06caf17001fabbf016ad8f71940e352cdff62abd3074082edea1c203de70bee7f8568a570bea21d614efffc01685ce6b700c77bd4019ab8dd0ec0de0c35805dce32800b5c58b04591bfc2c93080158e3d556aec2e5ed7008dcb5b0fa069e97b42cadda6b75b83d85dd4a70122a0ff166057c039c076edfc2fdab2bcc62f9ccc34398c86395569e1aa037096c429c0d6f315408a4fe91c7e41cd09529a31b661a36f6f019a0ee33a4062eba50c50d319fdb88a2f6e0056af2f0009b6f789416c263a5380b3047c00f48cb29718ee84db0ca05f3cd6011a979557365b85757c6d982a54b1ef0f684a1d7aed872122299f032cb54f019afca7f12d11153f2f02ec18fb0c9046663ac49dd687df528e3dffb00bd03c7df7d7962365fbd70f400df30ee51f90fcd86f0176e4e22f2567b2f1d42e7b9f9ca692a7f5622432762c609df0417bda53db385142a13f204d78313188f8fdb725406fe9160cf0116800f42ddb4b8054a323a8a8079cf9eab99edfefdf037c49fe05d0ba550e802ab57c93c237c096de538095f80f400f5576a06a5ea957d590aa79a5ded68d41ec1d5ab7002d53fbe35298377929017c49be0138d1c21b402b5bc1b47fc069adce0d221afde807a0855ccd003eace67fd184b127022573d334ff856e45cb31d87105febd00e8e36e660075617907d0d8ce06007d8b7d0428999b56ba00ed820b0706d6eac967fc05e7b81ce8f3bf2e01f6b45f03a4d63ea6000de5790ba0764caf015ac8f3f8fcd0fb954780d64b3b0338207700d060d796008d8bed5275d4fce603601f56eda5ea68f5b40cb059bf04d0e7db1392b2bbe9700ed0aabcbf02587ea4004d87c92340dbc0f4d920c2e52a03805d578e005efb1380154eb423cefbe30a403df6790ad0bafdbc01488e8df25fb820cc134007abcc939f0bee6c0250c3de9f007a46fd0c202bfea31761c4be65170d80be76b400d88efb09f0ddfc18a01595c443a3b6e05d0ab0d75e13e0d34903a0864dbc2a31fcdf7a9e961a92ebe9a6681037abf7cf007dfe6bfc226e349b7583389db4e7006bd3e8f54cbef849cb0236f45c7aeeb55fa2ce590de084531606a1d6d29b01c03b8331c06bdfe3c2b17ae5912b4778c77a04d042feb83028c72db15efb7bfe3db0bf541fa0cdd543d720320dd862d420a06e590e107e5b6f068dc856f01cc0c2e0a20cf0f1fecc204a2b9633808e2bdde300be9138b80268791c75010eff4901fa38e551ac52b9b8d08aaf8d63f528fe624d98d99049e5e2cc16d6bc72f179fe0b7b545d0034a56a05800d2273803dbb128338237f3d01bcb9fa024837a8e267b5c18157f54eaa79f9dfd70ac0079a3e400dfbba07f8f63e033813f80740827e16c3e0cbe9c2e63540b87aad0012e347fcc59f5f3c0278993e03a4c566f12df60acf9407fe0fb82c51ccb1b08d37770007280f01d273970700bb34e7407dba3e3798c4a128462ed4daf731c0c7d96780af8d8b00f5faec15e0453807a84f3f0d7e0fabba385435f93fa0b1ad9602f82d4f65803ab93c064889ef2600bdd61ba37f405db88d67387d74ba690708eff453804f9e3580cf403c2384f402a0d73ab89812c2892d028ddf93e75f969142f73980b74edb2ac0d9e71e001acad904a0e5b1bb03684a75ce014e78f90dd0d876bdc4389b3a88b51167d3c2e7054053eafd07e02b0a2f20aa318eda00ebb968696413b71b5fa354b7e56af01820eea2e7000de57c05d0c739e8fb17f8b8d2f6322d4522b07e7c7e235edb0638618ca5256aad19bf88ebb8f927c017cb2b80e4c7c502a08665a70075d0d58b41ec1caf62c2842d27b33c0db596361b0017d8ba31f0012fb57f5023ce73d9d46b1fc5f7b506f8923c07ceb4f80990f29cdc006411f8d9032483be360661ff587af4cbb1757a7d06e8e35e9700abe8478094b8b72cd47ace94bdb24ae1e6e4273588b4abaa0250a582727a50370897336f14f30aca8fc300569e1f8f00bda5786210d1e85f6701fc979f7b8026dd7c02503bbc41a37271ba9a03d4ebad0ca0e9f0d137889a5cc39700ee8fb307803360de02ecae7e07503b940afb0fd85930079241ebe820fb7c67dd06c0a9f40f02c481770b7090620a70ec7db434ca8aace2b571053ebd04f870560568cd7d6500fb3af1d07068cf8124ccb40b7006bb6b80ad0a7d806f1362c244e616a517afe635854709c0ede803ec165d01d8487d0270becf2940bbad517cdc388a62ae019a85e75f00b5c38ea07991e1a327805ecbd77258cd9fa1c19e9e012494065380369b931897897d8b2709403d56eb01a42caa658006fb85bf44f21b1eeaa6abaa5b35af6d3cea02d4f49f6b80a665eb15e0343d8700ed0c5401b57ac8ce207d02d86ded1da089bb780548673b9709e5900b5f2b808e808335c0b16f9e0eb17728d4e2a1b177b0f63824d66bfc0890187f9d00f45a2502fb03128eeb17805e6b5fa743cec8576d80de72750670e07811a0899b5600de8ec66b43ef770f0036225401cec3da063892cb0b284ec059b203e82f2a76fc07acc42f003a69cd2d2ec87b7efa05d0a9b1e859185b8542ef05e0ebeb114013e6d5b3107bf2e931c0feeb4380145fc712268ec485872380ce629d18fd38035dcc005a62670700dd7ba86af92f886bc1ee0fc047d163807b2cff8baf7cbdc44a96a7e9da722c82bfd25a0ed463d509c07749f92f7cd36cb9ce6df5f73bc0b7a219402377e4fec06f3cb32a60bbf1f60a9008fef4aa2c87b5f8ed0de01d8afba31cf3e35c4bfd57ed85261c18843f9d8337d250f3a90d2279cde9fd1d407fc9ae0d2285fde816a06f995f1844c6b6fd7b000b69471be7759a9f6f008e0968012461566380def2f909706adf214022c7e94f28dc9c2575804b9314009a852f6700079f57003e89bf0134d85763807a3db90338735c7c6d68f55af4479cb3f709c09ea15580cf62870087544d00fa7c7bfd52c9391d7601dad1af0238d94bf21e3d164adcee1e94762e4c7b00299ce70dc03afb0c60bfe0981f613e7e2f00d4c9af3f00bb357e00f496dd39c06b6e0cb0f6d81ac4b1faf513a0b72ceb001b118e007ae8f814e0a61701f6faad01241c1ddd927268eebd03344f2dd7a9f59cee0a003d747105d0302c5706717feba3468a267c7d0358232f01be59bd0038d6eb01e053740ad08451a9eb3f6015fd0390e47f9b029c68f214209d7de2894b71cee5394033e8fb08a0a6d7ef006afa4705a061b8f54c8e90aab4df02384c3ede520907ff3d4053ea7d07f0cd3bcf0883d915407b87461b200d94e66f7195aa924158ad7f5600a7c7b905e8b5f32f807afdec0c607fa9784b2d8c2a9eeb14b2ceb6006d478fdf011aa8767c6de42179dd00ece916c31056eb8b11c076ba3380efb32db64aa94f38ed988561737c6b012472561d805f1b3339fc823b1580347227a64358041a5e73e8dbcb980e71de4fb45548c3ae9d16bdf67fb7747ac6ee16e02e4c007a463bff859afe716c10ce0b6f1f00a9a4b987a15c097d1baf8d0d5a49439961d7ee5d0588a4778941e4747b2e198491faa81ac003556c0134b58ba700891c1572aae625a67d214389e9acf50d50af6f6f0c42f18dae00fada660670c4e118a0af6d9d03f450fbd464f87ccf1b06436703b62ac8b03827d11f68c20c2071b14f011a5be797a21a7461cf4363b7550538ddd810a0ade4e9bb814dd0e9721ac00b683404d806bb00e8b59b47805e7b3a01f8ae7100503b3e77002713eb1a8c5dcb46050cfe81b8c13b0068ebe47097bcc4f4ae01b01d7702704bf3874ac05e7f00ecad7f0b909e3b8e2e8ca298fd538054d2b201f061f50e60115c0248d09f66004da9e90fc076ed042011fcf30990a41bb7004e94ff0590e54af5a2ff01c5d02433ff251cb9b24e122052c9bd017ced7303f0fdfe2dc0eab502d06097b700e7c5bf0038706b0050d37dfbf60bdcd2b5171047e232c0bdfe790e70d33f00eaf5791d600db400a8d79dc52a6367601b1b25b7b3f3b6416875ef1d3294f84f05a0b75cb500eaa0d72f8026dde5b54118cb6bd1b050e28eb2c93096d797002bf113806313ca0099b26e4b007dcb57b4a31a47f34780faf46100b0abd7046017ef1886f0db9acf01f6c57f0048383e0c019a1fdef4667896cfca00e7658949e7142a892f85290e9edc7c00bc1f8b391656fc664ca986cd50db68479cd59fea006d6ada310c11fd64a35b163b83e422dae1f2d0c9d32bc079d0ae02d8c2cae8c7f1ed6202b0fbdc14e08d514c3a2ec95f01ead3fa35c0a1e52d8043ff9a06711b39cb818314e3b59652e9e40ba0bf8c5a00a93587fc67dcab6f470049edd7fc17ce54e2fee06ac06e5a197b87d500a067f83092b1997877d32b07a53fe5992d35f9f3cae74e539cd7354f5701fc0b15c3ae52933cdddf1a7877915666000dd4f518e070db2240c784d1a781edebe97d065007a9c05695c2de999321e785bdb331406f39bd31f0593daddd02a4f8fa5b80b62cc71d8044df750d6087b211c041acf95becd9750fd0aaec2c0dbc43c9be7701dca7f321c05a6c00900c72de406a70a73f670007b1c6c78da2dced29405f7b710190504a528044b04343a9d29dcc5e001a6cc7345394bb7094bf45537b7407d04de23e9aee0d49328867c486a4ff0590f86c0d00ce703807f8dee30820f1b92a00d4852adf59cdcb76dfe4cfd0402d2e0d22315a2d7e110680ab2dc087e619c037010d808d4c09c087f70e408bb09002343f5ef367a883e66e7a644acbe63700fbd3bd03242dd3cb00513ffb1560bf8b1c686a8f73a0963a6ff110277087740fd9197c7601f6923f01382fdc0a601bdb35c03b8317806672d5bd1e3edf49371a16e77de75419b255d88d000e314b007aede318206170146f89f3fee23640d8e88f013688bc00a4e74a558086d2ce60c338dea7475700f9a1dc7a6c49ffdabe0148e158ad510bbc70750ad0303c590685decf9a0b80fed2891eabc796a50c50c39c2421aff4bd3d0348599c593a9448d511cf70aeacb412df12aeb30fc700ebfde8b190a78ef61912b8f5132d8d58d1ce3dc0d970626c7da795ce642f1ce2499d5e001ca07c6010f9d7171f004dbaaf6f80ba70dc34082fc669fcc2fbc26c97012c2ee217e1143fd5b7fc2b63ae89db2b19c47d766507906a6c5c022c4f1383509e8d14e060da7846a42c9bc62f6263e49ce514c3cede0a003d63d600e8c07b98030df6f20c20a9fdb1360843f7d906e08bf6578055e310a0e5f19c0124d85af15a829d3a002769ba013830f81ba0dd96fd834668c28f078066f23a9e11894946e30016e39b25c0a96c33805d8aca005b7adb0007b3d4016a586d0490a1aade0038b1f31740521be032d5856a3f4068e411c0f9c766002d755799c98b503b88e45fcd69f5ba03b647a8b56515a0cf1f5e0134615408ae4a55eab45d0768e29ee50f553b6a6e183ece4ee037c2c7f971047052b325c0e982fa0087ee5c1b84067a7d01a8836c921fa1811e1e019ada932dc099b0e22d5efbc9f70ce010b31d400f7daa0034fa4e3944ddeaacf902b09d3f09e093b8afd246511c2bf9be0f101ae808e05be25b8053a8940152495727004bfe22c0bed6f95b7c299c3f54cba31b0315c7d9db3ac041685d80c3d33f01fada47cf648eb3df7b805a5a1c00b4b2173580f6419b0071aeb4c7ee2802940bd547803490ddf74768b15dfe0bada8efa6419c3c5faf0092da8f5e0d716d9c35e31771293cde011c395d05381ddd1340e2f324264ce6d13f3837206239c625d65cb303d0127b3d01688ead3380c7b609d055eb4847e251d96b2e75fe8f5f5052d39d526614eed9a9d3058de296386d54010e7672c3b8145ea700d9a41dab31e228ea5bc0112e674b4ffe727822d87b701417cbd9d01d548e5dce227e41fad72f80264c514a8bc2de8933b68dc94cfa513408efb0f102e0b17d0138e9cc1ce07dd0ab41a490fd69017c5b7d05d0c4f5308cf35be21ac069589a0009d8ee0660b3ed10a0b1f56e3caf28ed9cd454944e7f4e0112b01f5d805aeabcc554944e1b0b80edb8df0007c1f30cd78d6c950cc2aebd7906e8b56f03803dba4f014e37760b906aece47fd1f6bc760dd032753d8b31a7c6e70ac01154cf0089cf64089050722d0a6a4e67470b801ad63b0f50f221209e1127be520560bfbe53806de3258026fff73940eb76f304d056e1be00709ede1940cac291a063d46be300e05bd115c016e76b80da518e5f44f8cf5d0de08420df00473d6f01feb877801e7a350568552edf003ebd1600be19a9036cb6f5220ce370e1b303f05d52fe0bebfd1ac0e1615f00298bf21ee050957380a6b693888e230c29b505fe17945cf3c0933fae8d0b166c14c32ecc6700475f3f001c9d1f2d8da21087d11fe19e5d7c033818ae0bd08ada77005ac876691e133adc9d007c2bfa0290deef7d00a4a3da3194213ebf660007063f006c1baf036c0c3d36a8478ce60ca099fc1a932eea4632c7229a743604d89e7c0fd050161f01fab8b945705c0a272eb59017c36e9e037cf97803709cc52b40df721433d9c9a2b2553f80cf7387f12de165d24c0cc274736e191466dbd4798bc71c56cb6f00bd76bb360837f1d53dc0a6dfb64104d41dee007a4bc11d540ebbc35919209b63a802f4ed7c03f0392a35a8c555fc3bc0472f1d3427e41befdf1838bd78aa82c9550a7b67df4b83a8d26db7572a6c67dfa9411c56c7338014b00d5554c74ef64d80f70e73834808725c06e8f3d7ed009696ad27803646eb26408791a3478046eeeb01e08c29870027562c00a4c43ff36768a06af12d31c75e0e028451760450c37e9a00ebec5b80f33b94011a4a6f3627c4018fa283e204ec38f1bce6b4af28f29ad3f62fa4e674ea3b0b6a4e67fb33803effa7037052d54f83c8d539cf006ac77319e093e72540c3d09b019cdfa109d0e88fae01d2a62b5eeb2d8b0daad48b4e2aef0075722103f89c3d0748282d0f013e789700d2eaa5038046df66062a4a173edb005fa65401feda05c0eae40a604fc87780f4edded312256e9b525e2ffafb1ba09173bcd884345f1f2b80f4dcd5c6206cb04e6f31e148ecbcc5933cecf7146017abf3004e97ec4dde84341bbee49ac409387bf9047843f201b09ff43dc097c24b80c4567a0cd0b8343a00ad0697569ca05ed7270007900d007ae8ac0a70ee418f7eb845176e6b0007d237011628ef004fdc43804f8df1dad0a6b34b809e61d7b70996dee23d40d3c1769809ced79d786d1c9a8b7d80f3a0ed00daa15cc6601f4676b1686924f398453b22d1c2fb39c01121d10ed75a4c3fe2a1e129db4e01360f7601da3bac0f01da05b763b0e3c8f37908d06b9b6b80fae3b806d0b7382e6942d473d1d3a11cd9e61f4600a935c7ef53b63b1dad005a5147f18bd85b9e78f4cbe178e06df1242e63b3e129c05accf3b41c97b1ae8436e148ecfa849372e4dd98ff00f45049ba5a5ee9fbaa0ef04d40c720127f2bab468d3ade8902eafe80154ecf20465f76ba1a45b913e52cff036ad8c5b9419c91952ea8f6afb2b516d07408908a9e6601c2149e03aba431400f1d2e007628cbffe29c7f8706cec895c98fbe4621ebecfb07a08f533ea55a5ec87af000507f7cdc0324e90e9f009e74f1f914a19e02a481740d56a3e674b65e026c619d0034b6fb21c021556d80a6f6e4cd20aa54bd467f84be558a8c1a15a5d3d509c071e2570087531e00a4811a43803a5921227fc0a15d7700a706af1a447aadb73e406f59fc00344fbf8e01d6ea2f007ded6a2d80ae54a6f83f20e1d8f743495af5b505382dde35c0e7fd1140ab21cb818f6f7383b2edeb9f570089adef2e409fdfdf040817ab31c086dd1b80bee57b0b7014f833403bc7b957360142aaa25aa33c74a17c0cb0367d07384c6d0cf03132071239cb0a4083fd9abfd659013d0be37c9b7c9e05f0b1a9570678a0f25f385dd035c0ae6f338094f8636ae0441ce9ed1ea0affd3900e82daabaf307f471cd4f80de72eb6949262c298b3f60936b1da0fef83e07d81db9691099b01ee22f8d92cdd85ed9a5c32875fd0dd0fc388ebf842fede818a0b93e8fbf84606bc743e39870300238fe7606d0b6a75d3070718ae47660e08ba16cfa05f043a7006f154e02d8ae5df5c40d8b7336f42fb8371db600eab1498048aada78023860fb1de070976780da71560de0142a337fedafae74aa52af7d74657a0db036fd066821373cb6e5acac8c07aae858fb57b6dbb6e03783c8e699ee034446f23b8094f845d780766c0cc2e2acc894dabf62d84e447a18c0cf38af00acf85e0cc2c0bcee00ac3cf35fe8b5d31140abe16d0ed06bb33b8006ea2380556396fe009ca4e90960d558063897c9b301e13f0380032ec7007beeb401de7cb7009a0eaa23f107d4d2f109409b8966c1005bf004e05e5f02d4f456fe17270568039c36b103d066e26304b0737eb423e26fad3c8be14194cca6001f345f00fafcd11d405acc5b962217a9f24fae5196395111c83fe000b26f809a3e78022874b81fbf0837e0fe1aa081da1601ea42392fd4a8d35c505ea71a759ab3f3fc1992dac931c0c7a657803a68166f89ac1a5f3b80fd821f007aad5512859b0baf0b807628e30140e2a2190d8b581e052bd4f2c2cdf30940c3f07e0cf0cdc839c03eac0f00ed0c547eef1fb070fcf252cf6f562b00bb36dd007c784f01be027f06d8f6d9347009e1c215c0cfb8da017c68be03d8363e07e8a14a76fb07ec59ee554950efed01c077c05f00cdb1d60060cfbf73807606c36b03a7842874a30bc32d69f90dd0223c181a38034441d5d4fe81b05a1f0024729ad1ebe1ed52a9019c90ac0590fca8c6028a9bb3c71a40cfe83504b0e32e4f00ced6e8bfc4bd69e1cc52aa1cf68fd4aa2054633a8c878621e2f12080eb67dfb883ca115d3bff0268f4cfe2b53569c2c25905e063a4c556eeb8b435388cc36a01e0bd7680548ec2e9899a5e3a08e12897a2daaf6c3870adc5b981275dd65e19c4a9f1f019e078e409c017656d83a8fd342b01f450dd89d728dc9c4d2e008e284b009ac94ac352a3a47266614049e5e4a26f10f92ea62d80c6e5e707e03c792f004d07f9d3d5f262c7f50a409dac54a5bf2092321f6701c2a3aa0d503bd21dc0ce605b80da71d9073850290732bbbc5f01b4e6a637069192eae614e0cc3e4580e3a2bf005a849fc706e1185b7a06e8f3779f013493b3adffc205667f0590849129bc9617f71d7400ce86530d60013b9d1ac4ddc9b00fd0b85cf7009aa72f09c076a99b007e68c1e382c219df03d441e33b8074c36c0e90309075f417383225995c07b02a18540156055700fb9eb70116e37d8075c31340affd4e00d6fb8f012c50f603803e5fd7a4bfa06a5bdf67b4348259d21cf8b0fa08d06b179e9618437719403d960e006afa7bfe0b876525000d543ba683055b72736950779f36bf03b84f7b2b80d6be021e6aa570074a3675030ba542e703e0a3d71140c7a6c37783482c703903d8a1ac0df015f8354006c4243a281cfcbddd28c5e12c51029d5a09c75839e7d74a98295f2f007a46f704205510a28fda824d4b6d524294bd4ce3f231296f0cc225c0d68d52b91a06d547805676c3828d7bc2b1a565398ec4cd9a41c48c787b5ec24c393e02a80beb97009b07a361516368a691fb5d9371b03a3608d7594b6d4af7a6b62a50a93753f6a81a957a93977380e4a982d0fe8086b25f026893f73a340833f6220736eddd01f4d0cf1420a5f5f16d105500945bbf4641dc64770a70368b5b8013e79d03b4d4e56afe07bc8072a0a5de3c01e85b9e3a0047a66406618454e8702d2f662bf7ec3fa0870ebe007a86ee2cfe01dfbe6da34f6d952c2846a246b9dbc2db3dc0e7ca264073fdee19a08f6b7500ae9a111d14d56d4f47001f01a700673acaffa2d5b0c900ce747401b0a9b305707993f700dee41524a4cb51d53e99d7024429c135c0e7a819c0be2cdf00addbfb21c0999e3b011ca57716c01952b3975280b08ff501ceeff00c70279f03243e6b67004d98c10ca0a11cc4c7919ba101f069ad0cb0f27c00384eeb0be01bcd05c06903e25b2a51c87a07d07af136b08caeac160136a86e005a2f7daf865fc5e76fa904f0b7283cec0f7823700bd043b72580d4dab80fd021a07a00d06b97d185a1097fae0092859be8c2506b577b80dd4e5e01eac2da1460dbc5006023f58d416409e8ed01ce75f3017042b22783b8e5d98e006afa6e08f0d5e22d400680e21d4013777302d02e78111d1432793b01d8d3fe03e01e8bb10d43d543bc254e27e516409b9ab3254083fd10d301173ccfe4306426d53780cf730d80d46bcbcba3ecd5901d793aa01a876d837aec4fe32f8d48cee87694e350e46de03fa0b57fec05548e281b9f1bfed503b626bc308894109b538388e2549eab7fc09af0955f58d0cf6a004d9897ae41e45154299f5a5e98f73e05d8512701d83bac0a7061cdbe41d82da72f009b8f67001f790e00eac2e41ee023cf034033397904f82e293e3f4e6b0a31abe5f56f0f0f004e635d30083365e30ee000d425c0c6e114a08665b700cdb1b7578076285d7ee13daea2496b15ac9297358096babc5d6a94aa4d77a700670988feb0fb4bb62e19c4696df402b0a3f010e0b35811a076dce64032f9f809a07d725f32b9925b25bf019a416737017c266c7a4a61847cae01a4a34a09c08949f2bf688ec9c7e81f28d965d5e38206daa5007d4b7107b069ef14e0225d6f014a1239cd00a15f1486f4077cdb3400d86cbb0668a09467f31f704276e5c7ad5548cff75e04f8a4750ff043bf005216691b20a9fddd33f01545a1d80c60e5b9e802ec9892026cfb1c03fc960e4072bdf70dd07418ae019a0e8f9652bf1ac8e56f7e00f6bab902388af30ca03e6d1401b6391e017c3d195d18aea2b7313f22d6bc3b02f8a8f10ad0223c8b3976288359f219df12ce2ddfb700c7ae9c18c48efebd0c50c37ee21971f35e1f036cecb284e13cb78bc93f71acc65e5abd428ae1cb13801a76ef49178ea0c9835776396cd255bf367c480a67a500ae07dcb0e8e3a455d2f2a0aa6cf2d209102e890f0049ba42fc259c399a69002b8ba35b8016e1c129403d76dc34b06b64a1550c10975c6b80daf1f90170fec214a0b7c80bba965762ad570112f4362151893553e9bc3fa013ce6a01d0c7bdc75f1c019196ee0076e27c0148f4d9fe51e56055f904684a7dbc016cf96e00b4b22f6a00073a5e1bd8bd325bde02d40ed57eaa519ab5f05201a861cb11c097290f006d3754d5fe0f388ff33680e558e914608f99687a44d6b70e02783a8c1f003edfbe029c02e114a0953d18009c8a32fac326c6e4ccfd8106ba69011c00f20ed0e8ff1c1a8487c8e90940dfd2df039c5eab0e70b4cf07c04e03eee4f0ff48543dec1f887cc14b80a3e37e00920e9d2d40a3ef43e23fa0a59eb90bc9075bad00ecb3390158475d02d4d2691ba045b8ed0064b9aa45077163d503a883b63580cd949700bff6162011dcf27a41473d8e00ea31fb4b55f1eef0dd7c953bade30ac0ba610390a4eb6d01da18dd5c03742da804e4b52afe969323808ed5931f80bea5770cf05bd6003bc8c474f05ebb50ab0124b5bd99a846c04332a900f42d9d4f807acc1721d5f0d94cde636ca3ac48af09d084b9fb04d877630f7089e9e8f5d8d1b7aa007d5cf716e00a86317261dbaaee00fa96cd21404d5f5d1884807dfb06c842d2fc3218471ef858a6b1fb5c799e1256b11f1b14bd75eacc01ced29c18c446f1d10f8d20c5a4eea663d97c3e0668c2540e01dac02ff35fe840b35f1944e9bcf22bc0ae2b16c1e53847ed3700bdf66b0150d3c7f19638e11c6b9efe6ee02c8255c8a946add6f4f3d52082140f6a001fac3e00beac2f193868226dcc03f82f2ac95ea3466ad6d900a46fd3b5412860fbb151ac34e9bf019c17ae0f5007d55280768e8b9141384fae3280c4e7f931407d7a720ff0b1a90ed08af2d4a6366996dd00b4a2ee1e019a632a62ff076cc5cfdf2229f57a6810ea75950224a5d6c700f5c7e910a0b1fd9c03d441a79f002de4a73b80daf15605b83cc179005b034fa31d91183e896188035e320748b0bd94004e9cd704487e0cfcb5d47f993e048887ce001a06db3e29459a1dbd00f42df3fb00de8e5a89d7b06cee2e0c221d9dad4e141a2d28ab792d2f343a9b019c4731ff8b26dd3803e8e3ec85445dd1f4710a90fcf8b80d50b279f0c020aef4b655800f8987006bc21ec016b4024012d7eec8141a4dcfdf0dec5e5968ae0284d3e203c0e7a829c07bba1c68b369836a2d1470b2ba0c1001971380f56d19e0cc3e57007d4b7d07d0b7ec12806672a509d09a2b3e019c0e770ad0fc785a0628697964d1a73e4666c73d805a5a8fa687b1ab3700a8834eab00a77ef612e3d4a8143bb5bc36697904b0976bdb20ec96ab1ec01e334f006be40f80d4493fa6545ab2192abe360b6f977e0037fd6405504b0fd30091f1319aeef34bd68c86850fdaed3dc07b875b80d6edde5d18ea359dac009a52f539401f773406b800bd07bb1c191f475627e8dbd1894128cf532ff5d09559a107704e842b80b3aed4005a2f76f7f80572bf4d1e1e01f6418b87c68137247f396eced6eef54a1c13865a84ff4aa2da25b107d09950e91b6b75e21a3b5f005b473f0d42239f15013ace9e3c019c79616010e6d2dd27c0379a6d80cd9439d06b3715834836b75e01ec1ef5057074fe14608b62fe1787b95e19c409b83803d8a27801d05b544baf465dd142f209d04cbe1b00bc5e9e001a97e3d300b66cce3b06911460b40238e0e11ee0c88301402baaf6017080e1114022a7d606e8e875770790945af18c926f78e36b23e78e122aff014dcb4efe0b75f2b60b70d0a6f674d4042dbc5401567c0b809a3e7c043815f639402367b35c3d147061fb6c10c7d9e41d20b96e67f43a0974ec175ca722ca9b070a27cecd1a205d39a8037c6a6c00340c56167919d141192075322801343f264b838a3d21fbf18bd0c84a0357a3ae68e178007048c408a0d76e0f005a0df737009b5db600bf36c625dc3c9f4e005a0d9357808dd43970a84a74611c785514e20f78dbf309b0cb6a06f0eee20ea00e5292ea1aa548335560ff077cdf30f38aca230e0f0076d37a04a861f6aaa863c75542c31af54db3f75b80dab18e1914b74d1f1f009fa27f004e12390768cd6de32d76044d969d006e7abb02d0ceb1f304908439de01f49649ccb1f02f2c94003e897f019c6920a67624116d7f023493bf4e000e407d06e864713006d8fb27da11ee1ef31b803dcc2e006ac76dcb00178977801e3a88a11c977ce16f211d596793f42280bdc3da9e7468f57b8f7e393cbacf0e03780675e2a1655bad3f0244cd837207a0a61f7a0195c330d3f3b48c6bd2b4e161285368f41ca0ddb8cf2ff572c404a8a4e11f90b2a815019afc8ae0aefd2b00ab79bafb308809937e037c5d5b30885c7b366551673553fe8f1a755633db2da9b39a7c5c19848ab6d34023920264c3ad412405f0f5425e23b5f201b00feb37402b4a45a96a79d1d4da10201de57de12fb0b9f47c6210eea6cacbf207acc42f017aedd71e20e1d85d01d4ebdf8f007ddc51cd206cd2eb16c0a93a1280d3059d009c89620a7010eb0a607bd00d40c60cc63672001cc5b7847fd0f11e60637903a0812abe001c19bb03a893ed56d0089d9dbdf402f89058f633707b1d2d00d6ea3580e4bab5695e34f5ed0120a134cd7f21f1e9bbf906f11e4a2cf00734fa8e49a4ce6ada3c0ee073833779145ecd8e6601dcf4b76780f71f4b801b7600b0492d05487edc7c00240c9e7f003a58355a017c1db7691a4476a0e91dc016f8778056767707507fd877a381c3d0e607a061b82f00f4718ec26ab033f071853aaba90fab0d52eb15e31755e773ec9703c4fee31ea0b77c1e01f42d4a2eff07ecca7305d0fea33004d8226061c046a07f04f0a6a60e707ce509c02afa00a075bb7d0ce0b882fec02036023ebd36889e6c7e01b45ed233802f968f006ad87303a0e950d903b4b2e731f9eb5ec8632f753c99aed600bb7a7d022c0b9b00e7853b016877d1bb0268bdf463bd4414672d40b836f5760057418c861dda69e0367a2cf2c2f55a0035ec6002d0e75f45c36c0048dab19023714df71a606fb92ec0091f56007ddc229e11aaa01f3d16a9d3bea600adb9760a5027776f00eac2a6f60eff0ae24a37c422c400b0f6d84662a342ef0e60a7926f80eb02f602784372ed96465e85e430fe12898d5e7601bcff78b03a89089934895fb8a241ba2b1a340c26f1977009d879359429d1560538083e9e1187663bb5362abffbadbf963ea9830e8bae0a99362606b1dd50b9dbda219b09bb7b1c726bbe1c031c90db0248357acf7f480ea2fb11c0f6f50ca039f61320926878451d621150c58b1ab55ab3cd06e003efcc2082363d50877948e671006b42fe1211989b1d40724cd9b36b871c9a959bb296572f5d1400d6401f009f5ebf003a9b2603802b901d06f0e83bf3c261e8ecf47305f00e45d3e190443ef6b7cccb998e9600376c0550c36c8739c4a7f7ad08b012bf3588f5b229039ca6670df091b808d0605f97001a86f9074023d76a07f0d76ef885c5d6a33b393fcdef00ce115104b8d75f0176048d9686c7ee660fb0f6a8017cd1de04e8a1d347807aac9dffc566a868581cbcfb0f002bad3ec0af3d0438e14301a05e77ce0c6aa4269529407d7a7d19206e029e0de2f0deaf00348356af001fde13806df44380ac2cbb05c0d6f319c0a55a5a01ac1a8b5e73a5ba33f83f4e02447f1401fafcef0f80134e0d000ea77c01b85ed212a05dced8eb96fbec4e15a029d53804e8e3ceb7002be017803eae3d37b0b44cb60580155ff4075e59e7005b154e00b2d45462c2a45efbfd0b80dde77e0c6c724df6cf00f5d8ed03c0d7f95580dab14d009aa7ed9885b6d1a7d5580d51beb31d4b3d0240beaf01bed4b90338e8ea11e02a00df00279b8b5f84a9f3bb09d0409de4bf50d3975b80fbe301a0871ebe19c4a1e8d2cba31c278bf32383a23fffcb634b117b9b6e7e41c9f7735e2f14c65b788e95aba1926e023837c395251d4684eff84b1811daf197883ada5b287179e050b7c37284ff84d82a537d309e110a786355501e8665a20bd05b7a52c094bbcd1c2094c6d540fa1abf08bd6f671f6ad7668bfc176a9893bd503336b31b1f3563b3d610e0af6d0274287a3d33884c47342ccc0cfb0f806d4af700df9bf60d226655b9076b7979573b2da6c4acf64e019a303f67005f51fc007c6375671021aaaab0fd07d48ef7cb00a1b3f985f7ebcb4f036f2692af1e4002653504f8247e087018520ba087fa5e2ccd2fc96f016ad87d12c03239d1944ac9d33b7a0348157c1c011cfef30390f85c4d0156f3658054d2e0152013d2b617c022e7bd03509f4ef35fa81d8f6e3a35633bf7005b48ea00b5f4e613e088f612c04e038f00c98f757c7e6c3716a9416c26160940627c9101f4b5d3094066975dd72052fcbdc633705beb027c585d00b43c4eee0036eb0788bc0afd7b803ddd2e009e742580e5690ab0367d0748c0b65a00bdf6387a8cfbec2d40cf189d019c38af0eb046f6f2e0483c4f01d21e2af9f00b0e236b53cc8fd0b7fb1ec05714c7009fa24f01ce44f165904674ed1aa0afbd8a5eb7f53cfdf80448e4ac9b00a935c769a544fb1cbe029c26f02940dc6a785586362d14bf0192748b3b80bd19ce01f600a800d463ed98b871d778fb08b073dc15c0f314a0340a8963135232f8f76e005a2fb716b0e528f5747f6050f4d73eb90b23fb7efa5935885c59be584ee3c09b748f014ae7703a3608a7f8438b1cceb763cfe4d073697901d02faad651e55158592a0009834d7c4bd871ed19fa0f68cd152e003aad1deba1bfdad672cc2b2a23a6a8d435088d5cbb01483576a70035ddde1d9488cde66701a2624e02904a5aff001c81390668f2fb42e617487c66db99815563ba1a01d4d2ca0aa0bf28ad778d02b0e9c119c035971600adec7dbc252c356f5b80b56915207171141d3409c36e74509cb3796d6842abc6bcceeaea0060bbc3154033e8a809b003a61fcaa1d941f0d4484da6470027334d0da258a9ea78d7a84d9ab50700b5d45e6a94222d28976b2d2f45faf1047076e4fc17ba2bf8e219f65f1f6d0c42254dfb00db827f009aebce11917139ede8852cfcb50bcb6783c8086a63795e8af4630f900c9a9e00ec1bf80db01b706660efb0ac5b0b10d91b76005bbeb700cda0e512a0c1de6c009a52fb538057f619c037cd2380be7616ed88eb6b55efa8e5f54d1b1580faf4b804b04744bcb626935a767168500f2fb511c0f7ea9f00bdc5796af20aa8fb2b80fad45e1559dc56172a5b80cba99f01f47159d9204cd0af29c021aa1d8006ea3a075ad90f16285c5fef2f009a850eedcaf28a6c0380be659cff45df32ee01d485e57780d37d3c0174f21c5a3ae485576335c40566eb08e08dc0a5014e699f0069531582abe56555db31faa180bfce00fac54d343d52e9377518c97e159fb4d8b206b017e31d404dbf759f92cda2ed29550e27ceed18e0cc4f5eb7e5c86354b7908e9367a17309b0df781720155d8f87866b64d5ebb61cbd1ec21197b38bf88be57a765404b8ec9d55523932a4ae2f0036ecfae32a5133762a654125d6ecb8601007cd936f8033737c0124b55b2383b03bdc3d00d4f4ca1b40bdfeb3318813df6b07a0bfa81ce11fd09665102d8d1c11fb68d8380a077c00f4d0e112e008aa0a4082fe7d05d06b7df53c2469c4fd04a0bdd4d9cc200cccab114043f95301f8f3b700e7752a025c09be11c01f77a0854c2dced4fe96c33c03c41ea08f5b3c04f0ed5be24e0ec5971e3c01343f8efb00893ed5e0ae517a33991e032cc6df03f82ce6f48d79e9cde407e008aa6f80ffb2054815d86c3b2450e9ae0b50afefe600cdf555b4a32c3fd8c2323e8ebbd76f807ef1b101d895f8192021fdeac98f61b75f07a883ec20330ca5957e1d05b011c19bde2127be6511e0abd606c0a6ce2b8004dbfe16e0f8970ca00e9a6e0112280f3194b5b22bf6454bc36ceb80dc2157addb3ec0ce601700bbde9f001c18dc327026f0647903d0a4ab2c0092a78b0ac047e2fc2f6ac7a408503b6c1019465a896c7c05d091a7bc01583a9c0364513c8cf911a9e41e3a0009b64dfe0bad5bd57cfc031e866b805e7b65098303777a0ad0601ff6007ae867fe176f266e009a96e3fc2f36ca6e0092417797005700895f380e275191f25a5e45d5f71ec3d0b785de29c09bcd2780a6d46bacb928207df50d7086b21e402db557f830acc5693f5a3a96c9354963ddc6a9e0e409a0fef8be02a893bb7e46d4354ffb57019c7fac18a0189f5f0c10466a4ffeb8254e9a168e94bfb94c0d42189c5c07b0b1ebdc1f17aee6c9793c34d5524f9fbdf6cb716f3a3f05e82d3dcb0fceb7fb51003fc359ab87e5b86b3c778f55c20d67e51eab447ca533e70f2bb5b275b626eeaf2ef5babd181984d5ba3135884aacaa88f207ec7bbe03f84cb805e8e3766706e150b66f03ecae3e02d8a2383788f36d6d0d70c29836c045647b0651acb45503d85d0ce00b195f94fd036ad86b3c34f6414f2d80a4432ffe627d9b3a07e228ee6fd3c513a02ed5a8fd29a5480bfb39c04e584580cf951d80b692ef2700ad2867791f458eddd4b71abfc0f3c37138a3b0b016deca00abf90ec0e9924b00dfbde67f7188591b606fa826c0ca730bf0916705b083ff3d402682af47033b2f148eae019aa78b16c07b98238004ca6a0290e27b6f03d441f71700ad86eb22403b83a34100ef0c92e8751f9a93f11ea0afbd5901ec72f60590501aad019aa795db0025ffe5c720dcb33f7600cdf5249e11c6e1ca25c0fe63758025cc12a0853cd800b4319a5d19d4a3446c1ac0cf987e02d4d25a4c18ebecd409b747841fa7e700c78b3d01d485f32b8084e3fe12e0a37911a02e1c5f00d48585c4c00ab8f07c08d0c8fd9c03f48cc613403b366f147f816f24aee26b7d7e295cbd03dceb4d80f741d7007b201602d8cfb1bd3088d36beb18e0f36d0ba07119463ba2f466ad0f704b57009bb1f7004d98e6046037ad1f803ebf180b28aa656d06003de3339669c469f5e36b27d6a6fd5e00ebec96276e14b749c60d83c82172b704e86b2f3d50e1f39d4ebe0d22a99973328d22fb7ef6e5cf8fcbd8ac750a90ddf2c1429a58afcc1d449eaba547bf1c3e46554ffe7278db560b00077f591656a252dea9be76ccf9f6740f903a39af1a84a9c2b9f6a8919abc6d00d284bb19401ad9e797318ed3eb4f8014cebc07d0d7ce2a007d8b03b7c6e4b4af7d03a41a9d7f9d0aa8e96664e06ae1c96e15c0e7ca8336407b2987868e71696ecc008e46af017c673105a84f67dd00259bc20d508d2aeb5ecb0b8d8e870047f9f6035801bfb93fd0736f25800dbbc7000dd4d106a059e85baf31be4ed73cd41b81c22940cb74eaafe57cbb1a037cb7b6033877d82080e7faf79b81838c526be47114d4ce3e9f02780f73eef981c9f5ed0960578d778036350787011cea36ed1884c271e989310ac7795928ce59b85903b45ede2e008e91e803d48ed62d4033e8355a1a27cf98c9a8a4de03c0d57fda001b00aa00d7597d0ce073763106ca5a2c991c00ac4dab00ad4ad790c98b847e36013ef1dd017ce25b1af8d458708cf73fe00bdd33801af6b907e884939c027c3d990375e1b210c093ae9218f85634196d017617bb0248068d86008721ed008e588e71c9e236f212e033721ba0916b94017ee81ea01e9b94003ebd6e0112066f0f009777b550c2714995e1fe80eef88e7e00f6db8a751b55aa1ee3b5e1a7f4790bb0e3f419c087d51b035f61a5fd1f80af391e00de284a798ec3613959dc00b4f6ede43bfe3d8a6afbb5e9048883771f20a164e7b831be4e9b22c035fd72a03bad5b7f5c39f21938d1f5383295240e951d475ec9a4ebe910e55d5397271897eb8ee551ade75f102ecdcf1eb9b8d04dcea31d993d66aa99411832ab7e4b250e34756bb14aecd73fbd6e2b51cac7090ec6d5a22fb89ddb659257bbd91ac4edecead0208acaec8f003eac960c7cd04c8eba007b63af0052384fa7001f246a01ac0a062f007550a96a103ef0bb1240ba725606f882fb07a05dcefb0c600d943f543afb6a04b06e181b4418d2e218e09c190d805eebcc82d4154defe701ec7fda729fa280b7f7008d5cbd06d07d8355f4247231268d3d409deceba7499c5e0b6b7732274fe721a18c6872bd04680675da009775df02d4416f8d00364127f19738576eaa006bb10ac0e1c7c7001d34f7f92f1ccc720ef0e75f18387438719984bcd0e8b40a7014d601c0feb83b80e458e92b404cba3780158e270c36e9e5234023a7c2ab350a8da6bec2fa056589ad9718caa80af93901a84f17cf01bc2abf626c43892f0e011aa8581e18871bd706a1808b57005b58370089cfe3038064616700d0666219bf88baa2be299a70e7593b0758236f011a8671fe174d98a763806db05d8094c5f133c00ea9f933242eeeaf016ae9dd09c025dafa06a191c7e700bbbcbf00f45a5f604e4267177c389b843b72f6196f89a3e8660ad0d75636002db1f935c0199657001d236fe32de1fcd4ef00d4d2ca054033d9996b2791ac327122c14924544ed63b80b73d1700fb8f3d184c2298e51be020f81460f7ec0e400bb95703d88eeb918b020649b30cb081d9ed087d9bbdad03947c4b6c59c871b6fe0ed07ab157c5a45c3ed0bafd2a040813741f2063683b9e11f1849d11c0b7c4e700cd75bb464ea8d1def18a8a0a75e9450e240c4e3db6e40efbb4602bdb98919e7865875756d2fa0438fe6508f0dee105a0cf2fc6e787c2295a8e45fa93747767e02e4cdefe3461fd8004a01acafa0136e9d3438093f0740176297a3408afaced2280ffa20ca9f5032e960b6b80efb4da0027116d005c15a102d0c8e95eac4e05d44c6534ea143c4d5ef25f38835d1fa03edd454be3145d7900d819fd0920317ed90068180e3a0007e466013cb6eb57805ddffc8cdc6afd02503b16d700673c5802d48ede18a069799aff450debbe01343feaf5006e58e907a0d7ae788bfdf99b1e5b8ef78b3140725db1b37fc0aed50b80bef6b80550071df503b81dd70700b5637a01d0333e7f001239b39101068015407dbaa901ec93770dd033def25fd819fd3c8045ced1b741c436bdd5013696f700f630ab00a45fe43e57a7126b61f004d0400df3b7588e457f5464fc2b4c63c28433d87d06b0a75b1ba01e5368579d4aac49e701a0cdd5389a1eb93ab341002fa0e407a02e1cdc025c507b01f0e512cff04e69114d8f0bbb6506500729f5739dd2ace9dd0ea0b9ae345fff801f9a360db89c4e01eae4ed134092eee415e07a5a6b80764a4f314fb119ec00b67cb7017ae86b4c98b8693e28001cca3403e8197472dc2337a30b2394e9f21ea0b76c62e2124b1cb3306c9faf6d80fab4b604d822f00cf05e3b0096ef3780ade76700dff21c0134fa179f01ecdcf2101326a28d95ffa34ea9da42bf0bf056f2d8801c665d80f741570271de2f3c78e4a8d8275ffc3fe0cd4417e0b2bb5580ce2f57950076adee5d03fc164fcb38cd176eef0c22d351fb0960fbbac7a5ec9a5c496b6f60f360f2dd0fe0d79e6c0dc2f8f7e0e91045fe9287b9414ca9f33a40d3e1bc11c07e4a678f009d808ffdb5a1f7d3978941c4466ab7552f92b24cf68f7a31eea293e73b800c000a4fff039a5247670661423aee02d463a72d804c04dd438053b9bc188449feab05d05f9a7d80c4855ccdff80a665f9db606c5f5ac5dfd629229b758a003d54f91cff8014df300168113e5c00bc1aa60049edda97416c9cd7e700effade000e1d5e00f4969757807d156601ac2b9fda00c769f9a1b177c88e1380fdc7f25ff8da7801d074680e034492150f367a7f7308d03c5dbd02a41be49d5ea7446ce10ee0fc1f173d803fbf0ed0504edb00c9e4bde7186a3efa34d47cd61c001cc2dc0268dd9e160012c1ca585fa7aa6c3a2c02ec87125febbb8244b7b37fc035cace005a62ef3d806fcda393bd55485574e80f38a4ea19205958bb0ae0678c3ce9f22c236b807d043a002da056bc36ee1be6f18c9acbdc8f9b0122c4fd0ca07179ce81a6656301901cebe4cff0c1fb1de024c423800edeb31886483b527e05d844700c7014d600e088f6394043a968c13ab56bb36503e0f2151628a42c5b6f007a68a507d02c5445a53fa06f798849172ede5905a0963e2e00eac2c51940bdde7805a8c7ce7886956739de129b095d0ad729775bf8e9026cfff80168e21e6f01b6d3ed02f8d67c796f10151e5e63e442d0cf2f01de5d9c016cbbe8016cbbf802d842720eb0b42c03a4b426d1d2a12f41cf6211c686a4f203d06b0b0580e4d8f60ee0046d8f00afa818cac895a5343dbf20acc5db4b8074d45102d05b7a8f00a7b33c06e86b7b21b67079af01ece9a61d7d91dbfbe41ca0876e33836284a75f03ecdeb001380ffc37406f695ba044e5e2b418cf880c10cd22401f77e2f552f6256876b20048d2954f01fab8fb684784a93dbc00bcfdea01340cddfc2f1206674f06ded1a72baf86b27d470b67f12d0d27bd3bb74c2e1f5ab0b52ce8631f9435e36b6327fd13df1226f94c7bba6225dcb39b6e47250eabad438073437503c41db0a6435ecaf87d61100b68770b50d395f6bd4ea1e24c152ffe800dee038348eefa710f709c7801a0a6eb66b55e8ad4add9fe18e0add309c0baf201609f892540dae3b403907d5d3954eb2592bd286f71bd94bbefb700ea42655aac5364381dc45b9cc22d959fd21f909169fb62806f601da0876ee219b10ffa2a0334d8f73380be76760ed04162ad6999572e96bb7a9dcac5894a4dfe01ed0c2aaf009722dd06703b2a2f00b5e32a0548c23c1f007cff7261109babed10a0b57f740bd04095360035acb300a861578500deb23caf018eacff06d8327169103734fb57800d556d805eebb3c72fb065e2c8f3140bc9470be087be031c163e01e82dc3278073859f0488d43663838802ff380178211f01ac6f870075904210ff01ebdb4500fc2e7e006ae9e902e0345f5580b61b8b26405b85db18ecb07f1c444beb4e25b7a805080b5a1da0d7da8652c2e35f053eff018bbee51340dfa23a9af5120691e503c077e23194f699483eaa004d875919e020c51540bbf1e33b83107ddb25c0b18003809e31ab01342eeb478024cce714a0d3da772c8f90969d14e02b9b3640caf37a0170e84ef458661bcaf98d411c453f4700e9a84d2ce4b05d3cf5017605dc029c1fe60aa0affd5a03f4dab405d03e6811f32322c9e7165b2475577d8f7a5e1e7a7607d074f8be04d8bba309f0f6ab0dd06bcf4e03449248f71855325b5b806f233b003db4d205e88666d20850b6f9c77d1a86996cedd1c7e3ffcb6bff77ffa15579e13517b72b85de39c045654a06b1b2efe32fe1a853f4022a6761fff02c2c8f6c4fde5af4a1f747162895c869ffb204d88922318824918a35ff0543dbc67b1a865f59192e89450332c5570d86be267dea0588146e4b838824b7d59ac2cdd9fe04e04cadef013c94a5d420269d7cadff80066afa661036b6d314601bca10e050ea47806f35f88bf59c123dfd013d74ad254639e4acd50368795cb7000ea7ac03f4da8f67805efbfe1dc0af1d7c027cf0be00f8f23133b036cd744b5ca760722a1fc53fe0bc2c4b80f49ce209ebd4474e3edcebe42ef525060593b33405f8aee010604db80438955c0360534502f0229c0164b4bf78046879a822db1f501796a2c7a2bcc9417cbe5574a210c47a5e6379f50570c3ea005b8cf25f785b1cedb0a9b370f4604039b104606d3a0738e86a0fd043d76380be767105d08419c6b7543dd85f4f06119ebe490156d1d70675d75df98e8f732eb5e4ec0a20ddb0ee03d4f4f40460e7962ec049de0a00c964e5a8aa53b839db9701120649d32074f6f80c604fc86f80a6c33eded28888d4630307d4158a97006f261e017ae8b40ed0ce203d02a8e98a9fabe7759abf9700fde56603d05f7ece00126c4f2f00df14c5b7a49e96c7b706e122517b0738082d03fc47d4996ca50e0551f46332cf424868868a828acdb3c16e96d088a228828a7efd036a9fe3742f486e6e5775eb5643c0e58e414cba4a621042fcf39f01fafaab006af1400f95dde1e9cd209ef1f625a0f0c196011279d7006f86860152ecd4203effdbafa51d6fea428554edcc0d621b7f5b19c443df344fb58dd7fb06d1b0f1d4201af6b13488916b360d62cdb5ef0d30669c18c43c1d9d198482f6848caa55506977a60228137346dffe96370b03d2d19d1960cc280dc81bc8c4957b657af36a80dde1cd80f4494c296509486f2606f12d9ffa8b7c37fa8cad9202a4378541b4f45a2dcd09cdf8d543e58d7dfd6dc0b73c19e099a167b8ac7bcb001f92538398b80d464e3e24494fbfb041042926ff8f24e2d51b99f3d272ed9339ebfd9e7e21678edf63839841176d018e1a3f2f00b9791e550d08cdd8352001d71c20430462feafe476d4af6cfc55d85ea861baa139e91ac42f228a730b424447c2ba0d40c98b88e53518c7b42c51265c1dbbdc6b19c4304492842d404f7e328891eb7d087014ed4e0d42ffe8c65ccfaca19c3e1970f04e0c0863bc3588344e98f533e930c5292db54500cbc45fb9ecd3b1418cfe49c32066d0652e20ef8e578398a71546df4a4d94bfd9020c110f06e8307fbfc098f1cf00c7944701fae3ad6780df05836dfd633a3740680d0d62a0de0e0cf0d87d3520bdf8950077274b267f352707d1a7daa1cb94496180656262109dfc9c1ae056f007f0a87a33885e47555883f8da72c502faabe37d2440af4f6b0638f85f1a90fc66604024e8b1014e8b8700ddaee4b901f90c2e0ce22ffd81417c4bfa6210f2f6e3c62024509d456867d2d58d016eaf5d03e23c8f0d0871df37c00fe5cc20def2a8c9aff08e9d9e01c92b7e0ce8a01d033ae8cc0067d25f83981ff77b00d92e265583f8c5b9fa4386dd978e416cc15f9706dc03fdfd8524095f02686c354d7ef6d324b29d6c01aea2cf066c8e0383503693b64108be64df00dd52eb45b71ac3d22006eaa2614006bb330316b256a52a6f5c9e006c99b8370819c5dd5a6675e368cf80ac91fb0621e63f78a8cc0c65b8676f41fce28b39e67441bb0f06d11fdfb90101ca344caa4239fc3288bb82298b509ea1e5d33f016eab07ec1f352e30938385001bdb155d58c3d73a59ea5b9ab1f397277a06a68aa4570ac8ff83e5a16c05c92e93bf365029c1254091f577a18d679936fa070470a62db8c60cca7279442092322924876cd299d6fe7b621053ea3436a5fcef9ae305a0bcb43faf00659b6f9e0014de11f93e1b7fb5d1236b42e3afd0f9d7b50172ffc020e6292acb5fd5f27f2d013494f40ba0bb828805dc82e88f83c420fa23ca326f808a1f854a9bbbbee9dbbb019e19af06d1419dbf5fe06f391740ee9f4e0da23f96b941bce5f517a00bbb7013df020c11af0631d8472f025c401cbd1bc46bcf1303bc7f4a03766d3ac89118830783582f510f670db24ac479eedd0b301d961f0678992c0d08b5ef18c466f0796e80dfc5d42044f454df2289fc7e6810d234d2386d0117fe0f06f1d087a9411c57d8723635c943de560e0438ae941583f8b8deb701b6f1c480388baa0179e1be0430c99f4f00b23b7ce9b5323344f4e4166055e81890f1b162106f8934c55b103d56e8a14a0a10350f1aae389ee6758398c97785003bcc7ec520deb27c0448bcee3e19c412bb6d19c443238aa291db79619a1b709a1f1a6077f836e04c3832403f2d0c620b4ef60096b7af060cd4ad0116d6078390404bf558898f51fb0220c7c751c7801c115d03b2357e02e43fb69f19c45ff0bac9ed2350bf02203c4b2e1f739dc4934815b605e455c80de2f3dfb41a387b247386617d128fc97f5711501a8553037c37de0d902f53038c6efa0bf9fad25acd2036e9d99b015eae1d03a643cf2074ba9b8141c8ca03ba503564ca939941cc8f9b0f03cefb7fbf20a2fdef19d1418bae002a0b5e9db95c34d3ecd8809cc3cf0045815f2f0d6293fecc0de22f2b36949aaa55dc2406c48aa600ed0e075f0678449c011af8c25db26e6b2cd3e2441f474855f2c6aaacc982d6bd31886deb5d4339a8911e58c3a04c7ac7081ce731dae5e332ddaefcc4f2d894978f7179bf06c8eff36064c0c8ed03641f3b2b0d62937eb903c813014fa6fa9f5767df207a7dff0320c7837465409a9e99414cfe88476efc554fc72fa76e03c0c19941dc0345b2fd2d08ede2f64d00f18acda06e5561113b8c4ba117d5ba01d2f4c00099fd2a80888e7c8e5b1022faa030e0da47cf509ac0ecda80ec5123839830fda9004de786b72ecd2089a2215bc0e19d5eff3bcddf187023313088d74e5a06f1da835200630677d11b101bece72540a97e4e760c78edb5016beedb00dde1d6800b88be41cce4c873b5017c7e6f66c04508934e55668a28f1b805a1afffec1b7089f1644074cb974174d06a6a4042c3ae0132fb9f41b4b47d6210427ca88649dd783f32e0683e354034ee01c83094d634c7141adabc3208ed73b06f80d7efa1018949da06b19f8ef416c58a96f706c8db91417c7ee7d920de72a1a9cda12819e92d8a15dd393620ee6465400ec47d033c213b06b1afe70d0362ab770d6216f6353fa4a14485dc2d2079c5a941ec30cbb14188b5939e011710a541d871cff5f9a52e644a8378edeec480b70c0cb05cbd1ac4b7542a06bc45d341798c462b83e8a0c6b7413cb476601033e8e2d220bee5e2ca20de82437b5dd1a4e9c58341e8520f89411cabbf9a025c0a5faac7642218df1b60b9ea1a20a29930f2954c234bc016c4332ede0c628949bec835a118ec1ac4b764cc756928e9cdc800a5f7c5804058f60f6928c9b46ac0f5c2dc00478c7d031c41ef0dd0831e0da8184c17d630b926f3a60117434f069808ee0cf0b73c36c019ec9f008a225ef2752935c55dcd209ade6701393bf2c1d220fa346733a8a93856bf6940eae7438368e9e791010a5a4b40de727a864e38c5bd01a62c267f4dded877b9417ced191b8a1d426a5d01d54d40de3adcf69f3e8e8bc3b2d4a473b6938a413c74a2b19531e3b866100d4b98a7996436a7937a26337624acdb8258d92f7c6da68c3a7863d733aca3c5046191e9e8d5a60bb342c91943a36f58416be700695b874b0342777e0df0651901144cfbf96e40c8eeb741682891ca650ba23f70a169b8b64fd60728b74bf5da20d4afe69101c9b34e0c58c82f06d1c938c7357c21f3db34885ee7c67b035058c3eed0b082563f3588d7765f049896977583f87cfc501a7ff12f5f06d11f5153780b62e4eebb0272d4391008e7c9f2836f71886a2489dc02c2082e0d625a3eef18846a7d7464104b8c134ec3a12ae7a5018949fc506e9ba248f916c46a40ddd8fa696dbf763600c84d7cb93088b70c2a06b1f6df2f0c6297ea9406717ceb8d0de2b56f1f020a50fe3488b17dad1b442763b6dd807846bb0048635b8d0d88374d0d4202fd7e1b447fa0f6ac41955c041a4addd0ec69e4e4c4397b342040b934880df6fdd820ced9ab13015c01ddc9b2e54cc6065c50bd19c4fc78d3c46d226f67fa8574a9c9c2209eb17c3120bbd8b9412c8fda874134bd323008d1b8b767806146fd5184a75b5ae8b556ae4e0c62cbf9d25cd79edc4d0db0c3ec0394f86af2601033f96e61107b50991be0a9b26310637b7b6b4062348dbedc3db8d56858759adc18a03aad0c88d23340e00c1380aabb0c0b039c7d760d08006919a08f5d1b840e73a919245dcaeb45aad3c585417c5c11a2b1615daad534a0c7fe00fe30f4a98c3b69f669409691dc800c986d03d225f3f92aff9bcc68a96267d3a30383786d655780e970c39a9386521c235f6a4abea70564cfd0259f6ffd83f0b086ed30df1583109eed9a0087c4d633400ac9fcda806830b64f85aa943d7d5c21e38e3e9f4094a4af5f2867d7218bf0afeab0daa119b462e2d694e7ea8a899b49be8c592f99aacce0edd2b07671f3f78b58a675065b61bfc5cec22046ee872596e9e439e3f333c5ad957c5c26c36e54fe6a3477ec59be3008e1d9ad01e4c3da3c06c8b8737c6f403cf2c4800b2afd428ac0bf86809c7cfd0be45c3d6443d359359e5a06d860eb06b8899f19c4b4fcb70fc0cda224826a7d98257e6eba2780e2fc3c3788af7d3a33409cf40048d374f56840dda6b9413c237d16607324dac705e8cb37b594c0cfe2b1671073fde40a504788bf570df0ee981be0b33931c01e3432883916858b360029d6dc3720510b03f5272bc7067ccb8701c93b2f0c3848dc1ac4b770dbd4fcf333383588bd303933a03cc11b405e04b58501e2f50f44c38a2f03dc4d2b06d8618606d43c78122043eaaebe56c507925f037c15fe19f0f9e706d1c9df2706312dcb0780ac0a1e4a5f84f40d30002c0da2e979d320f6b17d96a92f42daff0ce28057560d1038b9413c94cbe9bf7af3c5d200478c3d8378c6e8c000f15a374012de1a20098f00aae7597f37e0b6e9db0045e0d7203694dadf5f981fccd35a457524f60de2f35f0e0cb07dbe18c4b49cb13bd8f1b15237883e3d3d37e02ee9cc00cf3f1ae6e247ab9e0181289f06b13bdcec1be0ccd137c025716c40de0dfd85a346323f3188a3c6f2d4203e6e6f0a90d9b6d63288bf148c4b4dba76f60738bc6780baf2e3b27fd454d78bc36a53ae91c9e25000e1b94c000ad83e9918e048ae86c92270bd12e0193f2c53078fbeb2c1da11e3b86e10ede815021855aef417153feaf60dc214fe1067c26626513066e4322513ebb1083369f4b39941fce5994598c9b279cc6b33d92e0ee3a12dc79b1eef1b70563f3688b7347b00e9a7510e680b3011d400f2aac8ae0c480ffc2ea05b8dc40089fc601012b91a4bace593f8d19b00073cd28db5ec33c131a1259f89342ab1ae01b517ca76dd2006fb30139089a06540228ed42006eaab621033f95d40ae91edb141584808206b59cc9f0f04d8f9c974b429731fcf88ca925b4004e61d402e898b6f03bc2a0e0c0855f934e066e4ca0059b963400e80b9009a015e7b2d8742127feb9af5e9efca00a7e7be00ae2b9144b4e18af4e56fd52034e9d9930167d38901de1d9706b1f6f11dfdab591fb5afb72036b6ced880dbea678310c091447403d0e8f7d4c93a454f260628358f065c2e5d18c45c47636b392544ba27c025576d06d0ad06f2a5657523393788d74e970678971606f1b5516b710ba21d5c50b5a46ea4e76a7aa1ca4e4f069cf79f0d10c04b8378cbd53f011c7447ea426d28a303032242a60634bd6f80baf169403a98130166d0be3a7980167cf7664034e98d41bcf6be63106f99f50d70b3181bc4c4bd9a19c4c80dfd50cea613f5878ee6ed730374879a011a4ad5000de5ce20f6e453cd0f6928933b03fed23720ace2d820aef4321a66678ea8ceb005b127d7e606b4f45e009b63540b5f03e91ff53d83506ade9606e874c7067c6dc5208e91f333802e42eeaf0c508b1b0621e7c617029c6f977a8b6e35483cd1f22546d4adde802cbe859c4cad1afb69124564b780a0de5d839863249d69492129b529a9044632bd3688965e370c422d1ece0408bc78fc36881e6b31b6b22a9463bd45ce600d6483959a7d6483549664901ac46bc76a47ab869585655ad36de47e61409611fd6210ee4069571f47b47189f375ab26f7867776984c37675c70b73294bc82c88396af28266c3999cabc9db0e6320a4916af9702c8fd556839ad8d83e1b61dbdd8500add2624cf6f009521267d526175e36b4780fde327f4c2e22fadd5bb0117dc89002377db358805b4ac19a090cc00384f96440cfdd5acef3f0ba0b2e06156d80162593588bf54f7009a969cf78bbff88657013414bcc23720bab0e81a84c28a3765e19c908bb601cac49e4174f2cfd0205e5bed1bc40c8ab4cd1b4088c84c7fa9ab7e54c3005378cd205efb59354087e918e022d13340bceaf36585c3ebb7b03281582ba44c9488e8c21681d7d42036fa28b4b1015c624c3494aaa8544f0df043691ae0887165104b9d688ec26924f7d5634ad87fd0358896ae7a06d8b53f00b23b147f80c3fb950121999f06a1f63cea5b38d02425cb431595cae5d280e9706e10afcd5b06c49b7e18102d786d8042323388f941daa2c21e11c98941ec0ecd770342aa347232fce34fb706ec74e763800cffc9a501e3921b70ddf268c0405d19448f3d09c8ce7f3737c0cbf5d120de52fbfb05eac6a501a2313340aaff33c0d55c530a7523edaf000acd18ed1be0d4fa6e10ed889af55b801d466b4ecea4ed638338bf4cbb06e470ff3008eda21f0691a246369c64911bc4947a691b847a3efa6710fbc74fcf00271bf6f5b5aa105f7bbf6b4088c897019e2a7f205afa5418848e4b28642175a3243d70217347d2ee18c4301c750d426559212c6ccc189d1b501b8cb19575a320e165f1a72afc18e0eea1d772b04ab8bd2f541c2bed1d0930d71f5851b526b95c7b13803ca9b1e36e40e88547770248b10a43a98ca025a12aebe5c3693ed7c7295914ca66515389c7e3dc20da91b3d1cb11b4b8e36b33f96eb4be01dccf25a73172e55f36cf0e00c7836471039055a1dd308855491863a9cc4fc9c3ab41985cb90928d7523d74878f6303a26c3a06a4a4fa3240997800287ab27e0650f8cf57dd207ef1ba2f20eb466110627e553188d5f0f00290545fee0870b0f233348370042d1dbd70fb2e80dc7fd4d7e648b1851a6679fb6080bcdd352061ccc8207a3dfdfb4548b1742010af4dfe0d0da2e98b1e40a7f9bdba01eda8197039dd31e034ff6b10edf8ca0da21d85bab08172355818447f9cec18c4163c578f29c1e3e8db208661fe6640f0e89701c7959141ecdad395807c474f004ab4504f0c109e7303b49cbe011e00d70684aa2c0cb846ff0148224f9e0d90734f06f12df38101793667005d0d2c9f0cb03b540ce2a15cf897169ec34b03d48d6303bc6d35502531453bea0fc535bed70d507bbe0cb8b2f96780f74fd720c6c54329f15afb7b46c8868b1640f162a43d2b7dbd30bb3620b9ebd8209e31bb32080da5ad8fc3913ced6b6c555efefcc920d61cd9804b5d40a4fdb10147f38601e7ec33833835ee2d0c484ec07470d6c8a79e01cfd835881e6b1506315037bc45659953eea24b1b00067503949a1b01c4daf80ea0d08c8b3d8390a6f35b80f2f44615c42de086e6c620a4c7cbbe01826f6e403b7a06d141c36f0374984340a69cb247064451bc0b90308620e735a8c59a2bf42db29faed4ebf50a1a9b3aa8aecba5b60139229a06044a33d8f2624c77de0d883b5107e92071c652afb52a78ba8d0534b60d837868af6b4071ce5480915ba83f4a6e670bbd65801443832d6d1178e6a1999c6ba95651da4440ce8c32c399a33861f26772a07aa74fedc53866b033391c46d9eec6c049abfeed1a84fd832c890369066537314046ed19c4fc788b711928c1634142e581133c7ead0c62e2fed35f144571d91090cde017a092319d4301dd47f12d3eabefa606d1c91f5503a4e93580b37af17527a08766061ccdcf046a140de9027c12cf0c30649606e40fba35887119e9e3ea6817bffa384bf58a0197f5fb0688d799416c4a84cc0cecc7b6971b705b7d61c0c1fbde8003de8d0106e677038ea207009da2df6f0db85d6919c44e775e3788c1e6926be0dbfb6b6661b5c0bb2355d36d5f9f1a109b901ae05a5d1ab0e3324f9d5079d43540347e1a60815f18c4405dcc0da2a53f8900768728ecbd0571fdd4d68451cac3c9970136e96b8378ed74d72016e1646e10dfb2ba04e86c9a5c1870f0be3688ede25123a780eda98641d2746766100fbd2c0cc855113bffc03520ee3e0ce21778db0efe44e38941ec96fd230067d30257af81e21a93f9d4203aa8f169c0e9f5ca20f6e44bd68b4463499295811cfcd39d0940828f14e5039d3c0baa440c2cd65a8f00d9b53fd8a574142d1f1906f9e2274f7383f8cb786840b286ae41ecb8dd2b8310382d064a57f1e549cd2064a5e6baa216134ad70c74159f4c9a069c914f0c6206657a86827af1ea1cd839ffa10f20c350dad040e10b5776d430c5ce1e6702686cfab84c858abb2c0f654d48bb1702bcf69b7664a51ceb638e0d5de0e2f00b20abf5a20f503bde63ed0f9d1480e08da1bcc24b72750e5549b1f8d811c05a8c8fd106c402fa1e0394c53b9b0b6062fcf807508a3fbcc287cea0fbaf691037015c600e15265f7cdd0ba02a741f0172d5583e08e000517d3220188e1efb13160706dc566706313f7ef70c3801af0cb8c12b0df070bf10a8213dea00c997c58501b2e1ce00d9901b8414ebbf1b20c5760c62a9e3de30b4e977a4a16cd6c2103139328849b7d45b5a44e971141d5a682dae0ce8c257037a6cc78063e49941348ca8e7a12a17279cc4873a5796dc01af01c7eaa7b901a2409fef6be3b1013dd63088993cab1b8462f47a2c803f4c144d5d031593232c7ce8f8fdc59101ee0d6706c8ecdc80c3fba741eceb9d1783585199da81684c3bea53bb78bf18c4427e6f197046ae1a7058bd3640fff834c088f06810835d51170ed0c6dfd585129ed32f0382e106069cc4af0de22dc32b03f249ef19c460f7b480747add4901ca1bd8ec1bc430e0be3fb4001ede1b44c3ea23038203e9a05a85a9dddf17e0a197a9413ce3323188a1c48cbd013c83fed061b5683c0ac867b36380986f1a2091ff7e8149fed420949ad33640d7d7fd85017e6c3503ee5e0f0564c5d75b74e025de74680ff7cbae41c84a4a5f0de560575c5d1984f4e8e9f3b58dcfab06a4a2ac1bc4433fe606b813be1bc4a168f6f70b4c266d8390c8170706e81fcc0fc70276cf0c625516bb061cdedf0cc8f4fc6110075e2e8686aa0151b6a7807a251e3ad633944fe9ba63107ff93620e3744fa02943c4b1410c765a33c0abe24540c61d76989a4ac31ff60da2611fef06f12d6fea31ed85dfeaa0423a6e6610538adbfba1b23596b30f8368584f736ca00808bd85620cc92f52acc6da2f72863263012584970eb3aaf4a04b03f229fd025496e841007dac98b1b1654a7f8293dec8f53cb13b8c144f98fcbb33c06dfed780e258fb0015e5cea26123fbe21f8c0d4211c06d6d64933c57032379dc25ffde0c08eea91bc4e81f3c1b909eef43007bd0a20190d1fef60fc42f96738318ece3bf5fe080990ae0dc72ad8fd345fb626cc0c93337c09bb26a801fca970029ec1f960618cb0f0438bc5732802c02bf1d036cd2a706f11692ba8facc22dfe01b8c22abf1b06310c9f6383687aaa7634c23521fd4a00726add691b709a9f19449f7e9e1ac42eb5a7fe903f1d71c023fbd391fd65645deaf7c800f7fdb901d72d430374ba8101f1a6870251a5bbfcd4c71572e25c1aa06c9e18a0c2cd0dd00c9e0c42244df40ce941c59b01c3706cc0c8dd1bc4b7d48706d1853f0706f12d953b019d70760d4825b7035032e4c9b78032fb3c1b443b961303da313040008f003a25bd370d70354f0db8d5981a10bc7165100f9d5c1a707faba92d9525b9318887ee8e0c7868df00e7960b032c356c6c35ee1ad3f39101a6ce2703e21a1f0da21d955783d8a5464706c8a817c00e77c0934301d6cb7d5b00ef528adb8c140290eebc19449f7e0f0dd0b60e0ce26bfb4c29651a48fb4f063150b337036ebce706b8f1950694f1a2d7a590a4f50303fc0bd51f521566a706f86c760d5048f60d42ee937e6da48bf672521a84fde36a57208b5d7bcad476b682c6c280b7685ce4a477a1766863a3d4e4e8cf6bef594039661a0621090fd51f6c6cc930352037d4d82044d2cd8500c784b9465f0ac9eac82056f64dc7201481c6c420e46dae2e2c2859d7dd356028db06fc45bf700e91a9417ccbfe40003bcce3a94128ac776aa9520f74912ff2402c6e902f35d5afd42f32eceb29e9f8d74029a8796d86735c92b380325db7744f0418973bfd422199cd0780e28031ec8ea4e594a46119ab287779fc60109a7467cf201eba2805786d3a04a8a025e7cab15c014bd2be8fab19d9e68fae0530babd9e1844c3289b39967651b09f8e1d045fd133ac4cd40c283bf3234017fecb0d10f37a28ba43717a6d80aab012c0edf5b7077056f353032e64e60218aa7a23836847fddb00dd41c3d09411f2564056a79501b252408a40676610fbe9e2d10073faa141a81b15f598b3037d186021591870786f1ba0d4bc19c4c656bc1bc43cfdfd3108fd1473fa1a708d3e3907c8f5ad7d6ac02ffe197078bf37c069606e10678fcf5f8318b9c5a101367a355d01cac5c20069aaa92d1b4a2537207af2c780489d7303ae17760cb89bff678028280ca261f76702a8d6130db67ce1a62f062824a50141796aba8203a7af0688e8430302c71383d8fa5036c776dfdf6126cb5b2ebd1c1aa0d4bc1ac49673776f4058e79e01d99127069447aa0a70e0ad31615c2672676c8089e0d5008564664078e99d01f91d5e0c70c13b064877b82c0db8e066f2db76f1716880c9e44800b176c894b27563d032e0ee6466106f69a5023c23d15b141f55ab1b1071b8678073cbad41b4747469c0cd7b2280dd32eb1a90be803dd955bab327837828e9e8360055616480bad111e05b56ea646757bf34203736ab52ba43aa6d4b9729c9fec420f4b1198bd0a60ad29d8e25f7cba95e8b1b5f71770350128ddf96417cdc616240be0bcd2079ff8cf50c3b1cb23bc8e1b03c3e328855f9dd3188bdf0e45b80a53ed6b78cd894f69e016384e748af25c030edf27199c2f54920bc060c65f160400e912940f68f63017ded8271c994bee0bb6f102b7bc86ac8e485b4fb60105dd83811903b21f236d30e33e46bf34a8d543f08e07cad3c6d4177bb3936d7bb07d6d198a76b201789889fdb023c213b016465495e4b0154b8a30c20078858d96ba0434084fc6f806e9bf40b9918c33aba01e874ef270065698e78b12d083da8abd7ca75f6e1412076baa2e818448fdd36010e523c3688b74485872dc053e5d620264c88930d40ac455ce31ae0d6581c5c1b7085756210fd717a20809237e818a0c3a8c79cb7a76b80ee7068100f8dbaf75b4068f99541cc8fd74783e8f5931b01655ed0b834f1108994656ba0420aed91013683ae017af25000f7caa5fee222d4438398dadf890111981a17ed308f138398daaf57063871ce00d2723a6f06b859fc1ad085fb06f8d45c1a7043d33108791b817d5b80d67701c0fe914e343fa4e57ced19a06c9e1a203cf57103d5e4d22f460a845d1870ce1e1960bbb833886f993d1b9024616e807bf6d880dba677009b413a7c3008157ffc68101bfde4c720def2d536c066f00770a2580650d44039ba30c053b66510afddfbfb4bbce5726cc01c7b3408d930e55bacc30c6f0d50481e0da261c3be41c8dbcab301ae3c6c3936998cfb067855f40c382618e016fdf40120fb4b7ab92fa05a8b15405689e9d0fa15c058fe7d67404a885b8318fda39e01f9e93e0d883666edd75410377b3320d1536e106f69a406b1e5cc3a06048eebe3942d2932b46f4174f2fda501393259c835256af97e30207d52c700bbc3ad41c8fd696e101fd73a114036ec3f198466f0c98ad22d4fb23f14c066d0650f72c2873bf5baec1f2f030364833a486e5ac5c420e6476f2e403bbaec0e4ee3b4879c93fd23ed1d188410df530729c3e1e13f8390ead73f028af760b03379dbf6fa06f8b2b06d39d153834997c929fe83d59029cce47005209b78f1c6f2c864edd98b61d8712a86f103406947f6051401115956d740733d120a6d410cf6c79e417c6dff0ea0d8d9d79141acdbe3a600f68fc1870115835f00baa2e86506982a6e00788814dd8a0146fb6f803d33fa062802a706311d3a5d01b4cfb0f6ac816c06bf1706488f5f0322421e0d62cb898c295b10f3a3aab7305065f70d5062a47e6f1a90f2706180ac5427ebacde991b70569f1a20e6770db09e3f1890cf51c3203937bd37a03fea06b4e3d70039f762809cd3c7c9ac3f7a37201bdf854174d0ee9141e81f517963039830d55d83d060e7a5412c8f3d0df6b8869b272db5ebfdf4cd804b8cd2806f991960dd1819700f3435882de7e2554025969e00aeac3033885d7b5e18a0083c18704b3c3088dde183c1ae29776965d700c3ccb7010666e6ba9301cd0f0d10e20383e8f5e5be40786525d77a8683f117066429ba31c0c0fc6b109b41fbc48090881e40722e5b1870efd136202afeef17646c6b0870267c517fe864d1ac1b901beaca20be76a0b7e8f2e05e4d572e9369d380dc83f706d1b07090d982980e49cf005f858e0033f9e61c20d7ead68f41bc657660106f793a3288b73ca9a54a11dafd03f1f9d9ab01fe0e3383d8b53f3b062170f6d80c9c9c0065730362e45a3303bc3bd43065490ceff435506acebb4b801c415f6a06b11a765383580dd7bf06649cd630107f5bae34fa43c0a3c676a48a6cfa38ceeac98716b22ab0ffea5bb8c4488e42a5dd51d04459b23ba89e78da653564e4dc495a4c4b49e4e2906dcb5685c8e3bc06dad87ee26cba93cb80f8796a803d395a5a755ce3e3cc20c62532e76f00cad5cf950162be0a908fd16261101692b049af81fc83aa3f0271022e9a4b806aa43e7d09f08bf33380dc3cd37d016ede477a86fc2dbb3b06310bd1c6ab4ec2b3fa0420c40bac1b550bf1a842b4051800ae0192d911b3ba0544d77e1bf097330306ea0fc422fce7bf601f8b1bef359044fe3d3788bf5ca993cb5a0c65249c5a8301a108876a9864f6eec080e35b69405e859a01de5013030e456d8084f8b26e106bae52352034e39f01ae091583b0c34c34c746f8c10ed50e391c16af0648a09541ecb8efe706b1194c6265571d0230ac18e0ee3133e0190f06214e067fcfe062b92b90511bec0de09367d5008fcc1d03f420a6b6c5ebe5c420e6c7bdda21efc11603e5ea83bb63038cf64d03f2c32c0c7004bd31403432f9951e270d97e62d888dfee5d72056f653c7209669441e6c01d71caf06a16b73d2aaca245f7c0f0ca2e92d56432d27e2b07e2ba0d45815034210ef0d901e47069cf7f70c62282397eb16c450ee5d09b0b10dd485320f4e960664093836c007be2d500be11917cb6b20a9fef16ac06dd3be01772787061430383240aa6b5ce40dd5f830c0b9561f879d2e79d933205bc1b7015902ce0d42f075d50e2297d2d63f0352d91e0860dde8e9e34aca897da50284ccdcdd1ae017dc3488910b7fba0d50854b4d29b9351eab93957ae044334875467a6d034cf26cd28af728d4633a129791937a0d64fa3d9c026499a8d405280b90b0e5644304df2ec222d3c9621e478d6aaefbb929d3612dc349f3154bbdf6973631da5153bc47f2efc700f7ec43809c16c363660de45f184967d6c009fc4e056a21922217d21a28c27f7665807f61dd00e15905a83f8e4702140bebdf03141231bd32c01c7664407f8c0514e8e867e0a3b8772cc00566fb1980442ea71383f8fcd5d820461f1d660d3033845bc116a03bb4058808f9d2337c12bf35e0bcbf32e024fe6a105f3be80a20c5a67a868cc3c5d4809bf72f033ccc120372309f096444e99d007475144977b7201a56d931887dec459d4ce5f334e20ad64067e4d9ab41ccc22f03ceb7116bdeac29d96d5a991b20e6770ce25bbe7e0c62cd859ff416503ced4480e36c874e7629c1caa701c7fb99014dff30c0b9e54280af6df2b54e878b65a2e6fc30df1303d206d063727d2bdb3d031c0fe606f197bba541a84ecf6abaccb6d9c020c665c0c8d59480abf66010621e05be66a36cad6a80cbd98501e1832f028a7dbb3588f3cbc197410c65c6ca96eb5b79f407a261fd67030cbb1706480fd6ad32d92465dd203af9a834e0a2fdce804d7a6a405cd2dc2036b6b186014b6fd9d1402992abb663407cc38b01229a4dc9c7d945dd0033f6d280d88499013189fa16c5cd8f1e0ce2e0bd2c003a158417c11a9472b06b1be088716f80ff981e2a6d7cb0674068b9a6c350912999016ae0c0203e6ed901a8fc4d8b799ac94abb7b2e40d0660d09a4f8fd545d98c97f7d978f535980f25bcf2038b0286858a6f4c02fff04e8b14fa447bec3c43d0c9525539edee4752040463facb4992e96938f9a00178791067f0d748f9c3e02e4b656ff3660eb6b0ad08e937383f043894a8a5b4098ab1a5647deb26b674e4bf37a60101d74f00f20d17875681052ec490da3ec6e12e5d5b620d63e56964cc7d9a2b703d0613542669a2e625f5c8c0dc244109938b7209e71756410eda87604d0c7de3e0ce22f2375722be20a92ecc42054c9af2700e2b5384b0d62d28dd4a70547f3af09c046ea2f033eeec9006ffdbfbfe0adff6610db56246ad98258513f6d83f896fb6b0195143a318876cc5f01ca62f57563c059fd9f01b1803b061cbc0b031c0f7e0c625c9e5602e8dacd5d01b48b42bdae73f66c698022a087ea8677726ac0b1fac520d6cbe195018e6d03030ac0fefd25f6a04223c7422e9fd563e80ee939cfb00ffcf4c12056c3e450205c57d20edf622fb5cec2209e51f9fb05e6f4530374a9338378cb4f47805559fd35088dbefacf20de72752e504343f904c8b1be95186064fa11e03a0e4d3a5bab397811e8f3b59fee0c0db86a1d1bc484b95707c9ae7db730881ebbfb3420e92e5b8eb2d0a4a70f06986d1f0d287a383188878ef5f9f293de3f1260a7dbdf03342a9ce6f50ba5b46bad0c625cc2196c0b5026ce0d6255ee7d1a70029e03146cdd9f1a90eb666410835d740ce21cf5a4b734d9a5c6cc7599b1cbb7aa016a60d780c0adb101e6b0060037e064da360871d2d5b494e16efc6c8055e1c200cbf7ad00e2f5a60428ca26b933c0075eab61ac0b6e366925e1492e0b83784b8f91533e83b4b76f103aff19132653fddb63b6becc4e8b4702e841e7f47a2683eaf2d5003ffa7b800ecd9dd42004ce0b1b7daeea500bbe365741edebd8d773e7dc791d1a846bd3cfd2808c4bef808cf438470d00816c49645adc028a1f2d0470288b22196b20fd34d2d16d41fca5ba2f50c32b4b7f69ca25402dd5099809b3065c8335f70ce22fa73f00647679a45f90e0b1f87830883de8bc02f071f6ca009bf4a5019ac1b1010be8c08030f94f83b0b09e340568c7eba1003d36557f946c9f9131650d74b18c9e9c5b782e4b032e961f0da2a53f7b0664b013904d7a756140aebd17031eba32880d25a239b620942b74cb35c04430d140212b8bfb7b8398b8b7fa5a85aa5442bee4167cb32f037cd0ee0ce2e3465301d236a338e77f1562560648c20f03fcb5fffe120f7d581ac4fcc045620df0fceb7c0328459a9e9706bce5cb8083f78941a8d69a85be143eb8312045d79901a5d17a0049c2d1ab410cc381816ebc9f0136ecbe19c45b227fe106b0297d5f1884029f0828d7cde80f9034e2ca203e6e5218505268d720be85c359ee4a35cd6b01a6c3993a592530a2b4f3166022181a602efd340839777f6640e6c98e00895a1ef52daac1dded19c45e98d40c3867bf1b70cebe3608f972ffcf2076bae52d40ee2fad8a0147d11303eec4d90b75244e66c70668393f06b89b1e19c452afebe394fbe7c27fc198513f3320c7ff2940757906ecfc35c5ab4798c916c4c85db3f3af0fcd644b4a0db85b6353aa8d64b5ee1ac460f7b44cc7f25faf1ac40cc2492f57ec7d32664a65e41e4ceb2ce4cc77167d011d9a7968a66a6a63b69c8c8c4bc5315b9ffdb63aa1b0e6b99232770606b1af4701b6665d91f5c9e20340caa1049fabba1cb893d7c220dad1ee03b05c25ff56021ab92e4033f9be6e10eda896802675679b9f00997ebb2d017e811bce1a84c049b27f06f197480cdf7449f6e2f6c70097e6678110c045eabfd488c3790460ba29ce5f0c629eae7601126b5fcf06f1d0a871d87439f5e2ead10071d216a0c7a268d916c4e8476df42d88c17e1b18c49a9bde002409dbef06a45ffb31203bd0ad010d7b31c0e9f900209fabd9a701a14c2b039c9e2b06c467c7ee50ff8b7efa31c0b38bd74aac1551896403e88fe981413ca3c3c7b958faaffe827c297bb90117febf06d1ebf91b402954ce0b03ccc76706311d4e1906d971cb9fbe414c4b4c9d75996dcbcf3f102d9df7001249519f700bb8d19c1a50d38f656ad9d0fa30c02dba6bc0c96207a073949b2e331487c4bfeae991236203109e4f7aa8b29dccf70da21d8dc420565494dc5e03e996cf1f0618cb5702c8ecab57031c96c700257a1abf1bc4d70e860015ea59aa93651d6d5d1b90eee3c78083e64c8025f6a3c196736d97cfcf14ae3f6f1884eed0a91890a885cde0efa4f50d2084b9c05e5857119532191ac48e7bc3e76b1b4fb8bdaf2bbb7a71c82e9569dd7698b8b9dc0a06ec96b95cf07eee0d4864ccb7e4badf27f0a29e8fd9f9b97e5acf30c534bf00d4a791c2bee9bae6653a06a8022a778d0d954849a29afc0628daf8de20d40de49c4b7f27ef850003754ec35cd62c122b6e00e7fddb1a403178475702ba197932887dfd54bfd025e8ebd800d138073439f15de82fc4d0149f8f067197146522b720f60f8ea20de763fb4e0d18a8b6417cfe6d6610dbd6581fc70544f23033c0fbb86310df12c94d3620ae5a93e6d82046bfb9002879f881be050f80f2e49f41689f9db60133a82b10478de467c7205485a37780aa0fbe7f1a70e3dd3788af1d686c4b82bf1eff0006f78e01b57cfd505e5bef1984868235b0218faa0437e035c8620f8a7c4a4d57fa2e2ab702dc479d5c1870f5ac8f1ba8a54f06d1d2c8c7b605b1908bb901d1d74d80fca47fef0dd02ebe0d3857fefd0231ff6b806dfcc300dbf8cc208c5d3d3d634c76e4f34b034cf27b0658151e0c68fa8f41ccf5fec220b69caa268cf2491fb3b2edb7353936204df1a10177f3ff0cb809981b704dfaf70c32ea7c1b101bb9348839767f2780c76e85cff7e5743134c0a16c6210fd513c197068ee19a04abe0a705b9db019f8e08d1f5bc307ef616e104399540d10e21f06c436e96b3130a757a702a127974440347c5b7d9e187089513340a75b187089a14e26af757a760dc080588ec70618327383d84f3f5e0c62a02eff0039cca606314f179a0e0ac73e63edaf35a5d8d88a4f037cbe5f0d50ae7e0c8824d7d82a88e4f6c62036949edf42c6d8332490639b964706d141177aad8a5344ea922d08652211c05fbb7cea1b1083d73388bfbcfc18f02dbf06d163dfeac2a65afa05506aacfe9b41689ff39a410c76f661c0bdfab341ec96c5af41aca89f528071b9f36be9535c8a1a0ebaea1e1a84b018697e905f3b196b451512af0702083ea2e3362086a18bbc55bc7a7aba6380f7e0a5417cdc4a6b9f5bc0b4a5dd61a05acf2b03ac2c07063150c58f414cbafbbfbfc46b5ffe003723158398303fda0bc99753ec6bdd0e32b224aae9c48b25030d94a2e2f752839874ed2b038237d8826bcab7f5991a7011a2719187fbc18341ccd3236d7dba8018bc19a0e3b20765dab5fb35839863bd1f0342ee9e0db8e66006f98a62b06310331937cf46261d574dcf6a3a15bc1a44d3076c9f9976bade9701d992d83f6c87794b0538ae9cf50cc8f9c7b864e8964997bd30534c628dc9af9a07a9267fa6ca707de47ea6344efbea20651a3854079124a168b00765b87a151d16a1420092435aaadb95a4c5337c2a98b3807225fdc7f6b901f19676dd804b61266eae936794696a36eda348f860d391f5d929a025c5b904c8b457ef0074153fd2434bae4a88e5d980d80ba3cec816c4845954000345629c1a44c37aa980f4e47d83f87ca2d29a52e1924ec7206c8e5160bce912f5451409dd00f4c2796a40b2a89641bcb6db3040c79d0bd44817d406e80282d8eaa66fde7f770ca874d53788451829983620e32a3e64944bd4a751fa6a0b5014ff7e11f2f6e302a01b89626c8067c6b1013178bb06f1d0d19301a2716a1053ea6224c0169c3401cac23b4a0c8809e81bc46be77fbfc007be61801a487ffc79c9d70dc254b1fc34c035e107a0aaf649dd0033c3c080c4022706e460ee19904bed4580b97ece50ba76dcd939000351f1736280db1a734c37ef252eab4dfb9e2799417ced99064ae16167bf06a870a706c4f1cd0cd81c3594d8838ab3a90166ec7f06d1eb67cce49a6e231b7303fcc75e0c70571f1b90e948632b03d199064a0e43672b83106bdcd0347dd17eaf8f2b5137be6b005774bc36c0f1e0c5805e7f3588879e560178dc95076d037cbf5606d8c7d8722c800f530304dfc2203effe106c0e1ac3cd0c8718e2a23f760b329e1597c3f0970567fa1d733d5aeed30e99c496fefc600473f26bfed52a386418c6defd720e6c7959ea1f4497dfda5a568f42e808d3ea9d141990a6c11a0dccc756cc29cdecc7593c86de4e6c69fb2448c4b2e4f598cb24dc9a8f228c6a5a5b423e5c1045022c4bb4d03aab88f04b89bef2500a44772bb0b18e1c7b6d801c8f111bff1960fbcec856ba08d3e96fa5f95eef6c2803e1d0bb03922705ca53b5da606ecb85303ee3d8e04246f5706d850fc0be45c145e6dba4a773a9a1844c36e0f041049bd0780ea7a15a5013ef01d03a2e3fa06f116ee095b0e1dbe1b18c40ceafc0264b4af65066cc1a706e4519c0bf0b5516eaee9c2dee9d91d403553b86dda00b68bcc20a6435484dd82d80ccefe015497f83c3508052dcb05d8834efc17ac70077a8b52fdb02ad700bbd4997a5d97b1677b06d1c9e8a72dedc9452335e0465333a84558d699065bd7b56f5303d2d06a5c94c47c5f9f2f17cd4661c0997026803d79c08ab2d17ef60e18ea243e36e0a0f9f70b72986928952fe7b263c05bda0272097831885effad0364cabaff36c091ab63100ba83537a0eeaca6b6d206ec8505ad659fab7e152095a5fb2180ce9f3394992ea82ef50be576d1d866ca2f75f00690cd60742ba0a4ee345dd94d8b4f36a56cc011b0c5f2c855a36c5403a83af633639b4beeefee1810db14fbd8fa1cce127b4d00351580fd0650ef2479ab010a9568cb046a54221903709b2fbbb9003b2e968942da7889ef97cb211784bbac01b6e0c76703cca5fa8502b64f2e0538043c2d0458958f2f06f8f4c6fee1bac4c5fdad00635bb93288653a2a009a0e5110a6e942c569f16810dbf86fc5802df8d9806c16330332a6fc0a70469e9e03e45d3a7a344058a406313f9a7303d4f35303dee25f48839d1ac4618474301b10a34f76a035a8212c76002acf580c0c50f18f0de8a0d4209a8e3b7261abe4f7a941b4f4c20fd5d5d19341b4f4431d9463e9edec1ac4767131338887eef50432022f7e00caad8fa9b3908f515ad78451fccbee9b013178fe852c9b9a41d6d7cf0c3826680649145cbe1b10a8f46d808c524ba58d9fed1ae09773228054ff568fc987f5f2cd2036e976d7203683cb0b00a9d3ca4423479588f46c6910d2747f6cc0c7bd1ac460dffd18e04b7b6940c2a9bfbf50abe44640f90cf601c38c3c022b800ae43c5d0354737af56c10db563f3140e0ac0ca21d4bbd455a5fe41ddd026a40d40d7828abc1e6b01bb68bb5f4881bcd596e405cd28100b6beeb36c00afcc880dc72b4c3b13c38e7af151324f2fe210027df72d50428baa5c3aacc545169470da3b86f316426e72a70f173075000eaa86510937f3236c08c7d2f4017ee223d72e55fff616c7379332c582fb966e1433ca35c4b20e280bf00b26b47cae535903d393b13c8e2f3e9f5bf62c747fb0272369e0b10167edb3188eda2f90118280eb82ba0f29d2702352edadf05a21d4975001829cab7025029f49f105aa5f30573585d036278a30ccf1684ac247fe106443b3ae70219a9174b00e69ff2e3c400c758fd45726e577f919c2329516939f7f569803d68621007debe7fa1b4defe059e5d5156a4f9574298a812570c4ea797063161cee706dc795601e41c4e6787068499ac0c109e63031ca85a06b1c11609402eab3d064ab69c74f66d80f375d7005f6bf5693d232702b350cea445143ddc027c7af70d42edb9383788bdf0ec06a0e8c9cb85016720b543fefcbb5706f818ed19e018bb32c0195d6f5100d9cfc020c4eb993a59478dc18101ae4d99413cf45403c5324da7af06648b9e024a95983e3788f5b2540729cee2c37f410b3ef836e050a4092319f5736b10fb471429df022e311e0d62933e63ed2b46333d7b3388679018ad74ee63f4c2d2fea74b035d84dc016432f9d6d74a9cac4266979913922d01ca6f892db8949f524228d3a67033c69d4f031c52f9fc4c8ea0053348372365c960670a43eaae0cb8a1d16b15fef3fb26c033763b0604a0323f32b946e2595ee69a74ab5480fdf43a26eec0698aa36a687353ca38d61cb6f181bd5d7efb06b13b44e5d1e6409e90c5d54040b69c47c0b846deaf3783e83114f841adc2946a7700f2e7271e7960ebf9a26a8049fecb0023530d505542c3a6017ff93488fdf46a22407a8b8f6b0384562e40f0c6d70740a69baf8501dbf88b0139009606789918e898f06a10cad5c3ad40c60d8d3ece87807f06eceb5706bc85a154b297747624a04c037b006dc14561101aec574b40dea56d00f6f5e26a28a0ba911a4aa9e7772b03f6533d5437ef677d03ea6a3c1b6070ff308867740e002a3d71766140acc63f0372b9be1be000f16910f2b67e6b10731d5dca65778bdd99009ef6679700f96dbdcc0c6221cff471256e7c276a6959c345e210a0ebb8f1a701bba5e6d850a2e0db20d6ede399418cdc6702c01baa3cfe67c08e3b33c051b82b2017090d8312191f5d198444e65267533138e4cbdd2f40b9e40f7605d84f5f46006d9fc4230fb2ac126b6eae67288a7325a0540cdd1f83509cf1fadd00cce9df02ecc997770025ccedeaa10a0059b1c364f20dec33713312f627976c7d1987a20443d520d7b74ce8a07a03cdf1371ab6a9f61b9fff5507a83c233379a8a0efe4ba22c09edc2d014ae0779708f016fcc7d62023dda9de32c2b6755813c0af2f3b17085d2a79b83308cb4475cf2054855e01903da8199d3cb40fdad714a0a0abbba1016544130344c18380620177050834e042660d3236d85780f6f5d58141f447a68655e920cc0c1b4064fd1ca05c9d1f570631d757a700b9011f26066c17370621e7fa7a8b8c2af77d03b4cf4703ae9e2f00bae21c95060409740d623a1060b80688a4913a595158e7cf02c897d31d0394cd7b8062ac862d83e8a0e99901d70b47802637011e28d93e1fff01b4afdfeb2f2d1541507fb4f0838dba80cd4d4d501a766b1053ea726e4072027d4ba1689f014029102ebf0dd0c69706b878df19c42c9caf008a3bb9391160ffe0e67de870a8c1ae0126a477c048f10df706f8296506e42ed52fb49f0ef605d84f4ff98bfc8392f1a101af9d1ac433bee9e4ac2a17bc7b83580d059b9273999cd60ce219eaa00c336571f463101f77f206606aa7f79f004ddcfb2f032285fb0628ce7a0b915c69bd66103beebe5edbe030f2c122cc9a686cfb6c7dba374df174db546265a3d72f067a06fb98eb8ce043b206588c4a5e9b9319bd78616c73e5b9e2c8b301b1f6cbb941ccb1ddaa414ca9cf4b01da91b04c736ede8bb97ea13caca41777b1d29218ef916b619d5c01c69466ed0d0408feaaea2fe30c0799980ea31a29cacb936b0362130e0478c6c302b013e6c114cba62b29a678ddb8926251d53364709f3f02a4382f3203b6f1330382ae9e0c082f7d1708a155e4cf00b99dcc0e0c707f79328891bbbd1060653fbc09c4e9a4c003d1b5168bf3be4134acd73288d1c76fdcd5188b5b7510e1b6c5eda14188020cff1b1067d3d3a10082afa9d7ca6377d6350819356c0a304f4f52839040a70b03ae5b4600dd695dbc08305067558014f8a465c0b1e9540053f8530f202fd7fb6383b02a50ee65f4672cff328899bc181ac4e4ef685c0631e90a72ebaf411e39dc5be7006a0c25189946d682076300a9068bc35ca0c6cdaaa68372bb5c5c1bb049f3f9f6d9acef1a84687caa0ab08f1df10cd51a281ff58caa546ba665e644a4af06b16d9dd601ca9476b02780718723e0c83aee37732c937747c260678a691edd01e4ec73c002920f49d23802e892ebf713a00544618951ae20d6ef1b802253889e1c791f2367e8a660611877de1340c1c6b61a0210e225f140ae94979ca602e80e272d003a6e4964ca7ae3e3e308675803f20692f97aec1c336785800cbbcf007dedc59901e6e377817868714cd3adc27dcd0c78e8c400ddb263800d45af559f9e2e0d08b97b02e8ac3e1a1810f0f029c00d1eae2b6b80a238d36b156dbcfb6a8045f14b0019753535887dac7d039083eed5ab417854f5340ca881c5d58980fc3ed585ca84755015e017a99e5172514684eed8bb03d1c663b99c25649e1c6b3348c915fe572bede5d52056e5a9de2203229129636d17495463dc0292129d19a0e43d18900a8a892b278a82045c636d28e542d34189d0c70025424f5bac17ab70e3b90156eb170139e70bc86d2dad1bc4a6744ac3e4b6969e4d0c888c3d0468e2be1930a50afd25cf882a51c3387b94c76a3ab3b0583d02ec723610e0f3af5b06614319a8e94dfc1d9e170674f22da090d6b70b28e5eeb10790027fce40e5e48828668c4bae10c44f063b47732cded92e72c2084af2f58debebdd3066d076ddb65c60ab687e0174fd74f86310132672a7b72af2632b7ba50006a2eb4f01b49c38df6e418c7efb1be07a162301faa3cd6b7ddbd45e1990a9752680da13e5d43720dca29387770342cc8e0132427ebd1bc4d6b79b18c4a6f4b414e0d2ef716a10df1271d12d97584a770b03ee9272032c242706dc8be9e394ef33d2e36c412ce47038dc800c27ac53806c4af3b101616afb06f8b25c18c46b2f9e04d80bbfcf00f22d9efd18e098f221a00c10cf0664e2d4e737b8e2fcba02c8f5ed561fa75abe673580c26dc3017303a46e5400766f783088879e2f0468d8c1cc80ec735d8072759eed18c45c8f4ace1b80292b52c7b72a764db8ef09b0a10c5f0d62ae4705d42d88b75ca94f090d4d5bfa5a55c98c7a9e6b80af53f9bc34e0dee350406e38fe0b5e595120780dc6c4672fb5e6b46e57bf06dc47350248eb2b07f4a9ef92beae0dd860f70ca28366558358fb834780cc50ad5d0134e982052435303d650165722bd87b3120c7eebd013156b9010d635c32f9d13f3d0970487c3e1020e97f9dd1cf94f76b7207e0689ef618868cc8b6b4f162100adaded8206c397d7d6d49a8db722640278ff42d43b9deb3e366f243b9a4d77305b1bef1b5b92ecaa24ad51ae80a6b38144078cee88ff5e610e6c148bbdadaf9f377e804a8ede0b735a90034b661eadc004caeed6b408d03deed2f006b6012d9a2378023cfc509409be37c601063fb383788afadf4059073917a6003d8d79b4f06b13bcc87006d7d51ccb6e5523ec5adbe563b5dffc820067bd60628faa97d0b5068d7ecd780bdf04d8039765a1884343dd0c7e9d63cb2486c41b42312f86d010e547540416edb48cbbb0584879506d18e874303ae5b1a00d5133fcb0d625c22707c0bf8960b01dad1d143757e491a0684b9d60d10491a8681acc57f20062af2f4ae81ae7d266a071a6cd1bc3108d5faf4c88033b23a599b635c0c6d005714c51760ac2b706672a6ea947116db008ab7debf0308d82ef79f0c629e2e8e049495e7d90061c1027242908b26402e89478f06f196999ea10c326cc13b8e49dc67e27a639bdf19c45f26d7068cfea3417c5c7f26802a1991186b2093496b6a1067a037bd16fff5a4d043ed8eccd4cec85f58fe2e01b26b0fce00f85d147bf469ae2a4407fcc227f1771a26ffe4a4c10cb24bc037abd23aeef79d41ec1f33da911398532c9852b9d2172ce8b17a4516b4f845b53ac02fa71d4bdd75578aa832b3011951cf03809201fd7c08b0807e7380f5b15d8378e871691052ecebcd803cce0b01a447642bd882586211bcb1015c83655f066c9f7fbf20bf43069092b73a312034742ea05d6a6510ab3272646e016957f700dcae14e7d706f1d023ff8219341f0134b687054006e62809b20531c79e4e05f0748b688e5655c7eaa2ab9193f7cfd1ae41343d32406c01c748f587d2599e350df073544ba5d385af64abeab37aa76710eae895fe324441fbad0108ea4d0a8dbea6655b2337d6aaa4a5ca5254e4ef06b119ec1e1ae061a6bf5459b72f638066729b91537eba32a50ba56da5914b7e0b38cd570dc873b5125092d99101f7628f00cfe4be014900ef0450598e98da8ead1e1f1bc461755a0128aef16e57400e5457062170ee970067e5e91ac46ed97a1260535afa172a15c74cd6c656ae5e01f288e8b34cadd3fd3360636b31b6ca8f9b1caaa5d8508a135a9a93cab6f8fc07505dafcf0b835851e32f0382be79a837b6df18a85aad12b95ccba86bdeaae9f2205d1d0134faab1ec0e5d4ab06dc127f0bf096c86fb90119b9f61e0d0878a803bce51c1a6051ec1a704dfa4f80a1fc6d00549826821437408e7e35807298855b74cb251fca8f4b804e8d7153b401eca7070b80e2d5d19337207e11e150ad9aaf5ac3737803f84bffc3204e7ce17edbdad46f08b9fffa64105fbbaf7119e6d46a15909dee59034579b532fc0b5b9b820e310cf3b9410cd4211d9465156e34df046847911bc4ca9e6506d1b0831d01b68bf90f0063467a742380e9e6a86f40954cfa2393e9f76c68401e92c200dff3cc20d6cba802501511b74356b8d5bd014aef18a06abfad2b039c6c5e056485635c3239b5b61e0ca2e9dd7780ac0a5df5a92e417798c9d94089bf6373ace5f2aad8a76179330f30a70bebaa9ad18bad2f9343481931ab2de7a32fb25f005598cb710560fda30a90ba11993936801eabeaa1f5c84392221b9cd43da98e05b03a7128facbd04e7f6c92a913b8b52bc0f6b95702bce6da02ecfcc52180337211c1706b805125e160b506b8f21c9e0086384e87bfd40648449f01144076a096ea8c5c7e18f097538082038f0cd8f9a3ee4a2bb3a1bbc25b744c28aeae0d628ed57a023a8c3c03746d5c3937208e2f01e8583dbb01e8fea53b34888dede40ba015857534f302dad333ea71f95876ee0076281b09e8127468809d4ebf70b6137d8b74ba3335dd59020a03c2f517008eb3e969cb803d88a1d48a2af79996725b2bfef1d01c2956ec4d00f2413b67aed709932fe68c4b1dbfada24c010a99398db7e4eb7d8b0aa86f803c8b718992ecaddcc9a2dabb005d0d7cb700f2c547eb7396e6b47d60c0cdd98700df12c95db70073c74440cb740fa05cf21161b701289bd547810c0f003d14915446569e56ae937871a0760cd9c7ba7b06544fdf35883e0d27f096f32717b55004d6c3c2661021aa5b10f74098a0d720638ecd01781f97e5c280dc0c3383981fa7a5401ef2b6a3d762d62f67fa8bc676c630b8b4d1bc2dc0d78e33019c5a07d7009db3679f06b1e6defe7e114bfde85680d77e4f0df042f25f90a693aa0095f21a4cbaac215fdaa101835d3760dd4e0dc2f47bac4e96ebfdf1b9017926be0d42118878b13550bef1e9a101b16f150384e78b006f89e8852d6033d0402922e4656610c7a6fea9011b8a7e51caed756c102b2adb37882e4cf40be5eee869064914f4350be5f6baa4937345d82d5940391143c53c7498bcae440b6f750194bcb226508fe5b1a35f70979474a261f5da0e8e4bab01a0ae5c8c5706d4ad9e0910801a77f36bd0a8630adf05c842d26909443ea5e2fe06a0f49e51dab9b5c97e4ba5ab98a775393d97ccd3ba0b6bbebc18e01fb410e02f8b478052ec2c4606310c9189a255b717f46717a0cbd8f76f03ce625f00054c156a875c02b816ac6b45a5b50b802ea8760ce4d79702342deff5501d13ba6301b6e0fc045054f0217912c0f6b9bf2b80548ff8ec2dc03b5dafd5857f45bf9003665fdf3256f2df58a6f59c0b9964c2b7e43beceb1fcca05c45b93b35403d8f96eef15087fc271f00454f1e8542d2588b35aeb0ea001fbd72804ed1ed3640be81572b0179aa7c0a64141c3b05946c7d170b808cc3fdae41ecdac30220f9d2ef1844af5feb2fca95f51e93bf214fa6221ceb3740d11cff00f2b5be9f015485b9a4e919f1eae5620c503acb65dd20e6fa6745401a6c13c0656c8911a12107fff2b827c084b93915c8b1f37f0398b8c5cfab0146ea29c0ead7a300fd1199c05b0d5937928f3d014e05477706714aea4e008e081900e4e976a767e884f32f01a83e618f2e7489fa8763004ef1c9deb30159f04600dd67bfef026443798bf37ea32e9fdeeb98c9cdf561845bf34300b97f927f034099c7426e0828735c9901a4e59cee1ae0ec3337089d7f3c0270df5046268a5633ab5438235704a4c22d0d509d0e00caa8b34b3b329713bb125086d457015dc85c00542731b931088564f66a105bceb4059071e77e027086d4b6014acd9d002d0d17cd0d084fb7f45e6f51c683e37d0139d9e8b53a24469c67eb2f59d47d09d08dd5fd4000f1aac1d6456a79f20e50c8ff470a18a26ca66a2929319395be56f1403d3d9470caa2c160af77dcf896bc03901f4a49d39592aa4c12408da3c6c7104091bfe2e309a0e3ca32044eb3ae5d6ac6b7d495dde3e513c09d788971b8e51aa991056f03c2c696341f010356c3c512c04c2ea378eb16844e1721dd1b809d2e52b9b45a72e42a273b06d80c626c5b19818e65445fb75a4e5f80686ce97c5bf40e0cf0816f03941b2a6ad76e01a9d39e0d62b0d91d5a9adac97205907ff27d2640b0d3bf07409de3db5cbf905a3c7b11a04f236eade5143bc5eed220f6c2e95480f4f3830940c539efc70055bb994d0d98eb858092fea706f8af7f009477635e37c0d16f474047f3c4008fdd7b8368fa91fa433694cf4383d893977a28de61e95ddb20b69cfb9141cca0ebdc2044f489c605c794a2f66310ead7cb2d4039652f994179453e8a0f0631b6c757003ced93156fc9757bffcc50d6f59709934eea791269acd74091b191a4a955383b6154535b8392ede2612e101692e45f0d30c829e6720ae0f393fa52004bde85dea2488c28a3d12a54c4be8c74301b8004ca760d88487d37200e384cbfc57a11a2b2340c38bc7f02aa3a9ab70d4858a75f643a24be01e4c2bbfb2ec0523f7e06c88088f2bdc921c2add7254087d5b6fe82eb7d32de3708256fbe6b40c2a95c8055f9f46e4005d47b806367eb0604917c1ae096f42b2061b16b1076dcfb4780ac4e4f8c4ba6bab3dd8180bc7f6600e5843c3e1360657fc4ae5de48a6dcada02212cca7dde92e367504449900d606c47370095047909895cd4955dec85792ac5a8783e074811880ca92da740281fcf01725b1bef00a45aff2e046ad8f90b0132a6fcfc0270252e2a7a06e235c1af6f0db87a8e92ecad52da7871be0260b52eb0d36d007af214206379fb1280276419b5b0d6a08166f0762bc0ca7eeb1ba0d4dc0014001241796ba0d47af55f01c9db3f4001e9254089f32e1f059467f3da0071b26bc0e8ebf395bc22a2e3b600ebe8b301896a73031cec0e05786d44fbb44adf37cc760d186c86529a52122931b72096d8c78b41b4e3480fd56df5f2c1201650fd1ea0d2df399feff49ed77b02688e3d7a3d5781cfc889b005b1058fbf01da94f65f0175a5c5bb03c8dd74f90e60bb28d4c97619f94a04e8a0c806bc0623ca794465eb56697d0c3b6e59cf32ca650be077915cc77418d48a9c2bbd5380f2c13e7c08c42f0aee5e07b511e2249f039455b41fbbe5261a9de5b107a8027ec606d1c99172680db4a2aebe0de217c35b012c9bb7750134c79c963adb6bc4bcaf01969a220a29b4061205e5cbd48079da0234949cf1d22036c7af7b8378c65c1f275110d95f3640f2e50aa0cb834e53404102fb067876fd1ac4e1fde2530067c16e0250a9b8d381408e81e809a055d9c90454d8eac52056d4d9d0806c054c0759258bc806bc01f85b26cca0b562c435072397ef2888a40f9073ed281360265fd2a7f2a82a71ae1d6cecd8db5fcc7700d4bd2f6ac70069c107778096aed2160042cc0ac4daa0aeb4231301f96dbd312e75c5f1cd78465d67c2517c9c43ba934832bb062d54a7db7f002c466554b76d0dad2945646c6b2d9078cbedb941089cdb3300cba39c5d1b70565f023206eaa00ea8739adf9d0b6816de03e44332cf0d42ff383e11506212bd166533197f18c494ba6b09e8eab930e0bc7f03901d264a926d01c7fb530189829a01fe30af00c71297023ae1bc0870c2b94c004acc3ab8155005900ca088a1a8dfb0051c5776056a946b508fc9f77c54378886dd696c19ec04a3ec30afd4f1c8fc04283b72e35580d3c98896e694874eeb0f0055da7c0ee139d4a55ff1c274a8cbfbf8e502d0507efe50e146ba8b2e235cbf357214d6ef1940d76047f196918a5314e7bf063cf4c0806b741eaad0f224e903b4e346d0f70610d37cde04accf38310b4b00b9088a88b1da8278cb5e61c0c6766b107be1ea0da05c049dbe418cdce00f60a3f75f64514c018a9e8cf2f26b20afdf93b980acb43b06d43c5800c63adff28b9c3350dae3b5794dc69d1783d0e97a3340a65fbc03e459fe457fe42af9f0c26bebca9931a3a57585bb3c4f0c623ab05b8eff62351e00e39cb8c698da631ddf8ade0940c909eeab06a18de3fc34f615e7691ba0aab2dcde8fd5c969fd072015aea387921db9889c4cadf1fa74124b7dd9328833d0b80b504aaabb0e40b929ef6a026ce3aba9418c7eefd500273ddaa11c3349fb1490e52400dd01c883a85e0110f49decaba5aab15cf2f94a70902ce70089e8eb6b81d072caf750adc7f59d189724a7e975550d7d1690149b8c0cc880990294ac1289bc0118227a001d35a2ea7051a9917932b96d0186846674970045739cef06d0e13df9ba03280dcbe91e00eb57115ace16300b17807a1e0deb1e0174bd70a067a800fd79db80a5fe0250a6d6da890139551a005db5ded50d62e4c60700e51089bcc55b10e392e82d4ada1d775a1bc0127b6c1a700d5604f0c4fdbe1260ff880aa85b10fafae416e0c8a52f0189355aaaecd9c9f937409b414340b58da3be47b109cc8985bcfa08207192b40ca41809a834ebf3b301cad500a0f34b14182f2a8d9d88384cee624aedd47254a7ab18fd4d38434ccbaba50169e0da806a4e96c44ca08e57d61980395686f760b1e3920fa70780badef265104deff5000d04f05d0eb085756540b1b09e417461fbd780869d1b10b9d40428db5a14c3de025edb0014f2411b1bc45e78d0362025e60f40dae7e002a0f48d831b0326ff83005b4e726d109d3cff0048364cfa02daa56a004ded38376c0017fee19258ec48ed4976bb809a72539602726cd35f7255863b33088dad1c1ac4947a5a0928c5f0af412ca077265d5ecf496ac6c729716fdabf16c064f2da17e08a33b2be159bf00e2edabb063c7417205dfb6668106bee6906e0a2bdfc642873a508ed5704909551aa760d064ca9480555ecf84226caacac010e98494e8fd56b3243e917f5887d4b9abca52ec78394e9e095fdf261106f193505d80c0a96475d11dc696680487a12903fccdc20fa744207d51534518929559511a11c97009907cb1cd0e050b4fb0160e72f76170095df8b6c5a853ddc93c1114042eba01340a6ce24321dad815c68ae9a06f18ce58f01f99406027ccbb91e4a9062b17309505280a32a40fec91fb434cfe33c573eeb19f5b8694e7b138076a9be1eaa6bf4707eda00221fbbf4694e80727a5711407af4fb001ddf22ffc71a7095961ea706d141f55004aaf5a652a66eb5bea296a9d442047eae415627c95b02a0425d513d17a821809b00aeb0caf72f83f84b8454ad810ad21de9b5984c8ad38e408d2bbd53801209fe54041a1c01af00b2391e5e00b8684fbe427aac15da3a17dc3380ee2c7aff0c08ecd35fb2d061924880b1060aa4fffd16a86f55da624fbf504855ab2ba071a919c4848914901b804be2510620b629edf37139422bed3d035a382ef5a60022428a482359d4eaca22311c00944d2b6a2ead81020d6674721d0362f91b639bc9cba408aff0227394deef054077276f2b8012804694de06b05ddcd70c70d43913a030dea99e51d6a986d4000cea84fc7f0186ec0e4f538032e83e1c0124d6c6b1a164bae32b0f2f05b03b2457009cc18ada3f801c3097550125877e3608cde0b10e5002bfa358621b67065c349b029ca2f31140dbf88867d415ba13d54bb7800a20d18e3c93d3c0d1054021663f8f022ca0ee0900f7dbe2e7c580242bbb06f1d0ee31801a54c9b20150f1815668c1b9dcc5cae39941a870ef3500e7b9a27726c0602f9b009de74e4a802242c61d0144e3f9014019b9eeee010d810780d6cb516e8052f30a90f771048f16b94f5acb214046fb26cfa82b4860dc06280bcdedf69c5dd4b3664e2ef97b80f27c8783dd1a2804a0e01716494f2f06d80bf70598307ba7028df0a35f3e02e4eff07d0da829184e0fa52e605ab9368891fb7a00707c2b231bdf1ae8be2152ea161bbf3e92dd6602ca50f6641027f1d6c480d2acfa458bb57f7806c0485d1e1e0086dc4844f44251b71fecb21460ddeeea173aab47dcc91a142813e1d7b7062a22db8eeda2de68463447721bb3b09195e1c5580ea700259abce818e021f20f3096cd3136a58d0f1a0941f605b87d43416b686c4b76ba8d535a4ca99f8941b4349289ad81fcd7c3d37e0d14c2bcafb770484cae3301f97e9d09d063e32540fe852fa980ccb63d405de7861b03b27ba8a518a993b86b5c0355208b123a6b20d57ad612e02f0bb554be8177770658d0c6008445da3f0748461daaa53a035daa619459292a7cdc7a06c52d60b834af01775a49ad0b5078e9e793009e0809d3a1aee8969ce950c7133269f0f916af91c5aa6834546defb7008c334caeb1a29a4a975c445845b1f1fdc2a8f26d10d3a135029094a8dc3340cc736c6a4a8729baa70074ba726f0850c9bac85b5c6c6a1cc660cf6e014a4c32bf1760ff387e029067a20cbbf606e41833f40c45e73fd600f2b4ef5f08c870f708d04d40dcf1ad8192ce84b16b0d649889b467c5c6b12d7ef194039426f03831400b2e0c387abd00a86553866bf51a0cf8c530b69c6643ee95431edad0316197fe68d6325c89e3a12d5d8494c36780f68f2234d856ae5c7b11aebf068ab0cbaf00725beb9d0b203c07c700dd56231ad7805a8bb55380877201907a3e7a00281eb9720750aedf7fa900eefb2f7c5c3e94bcbd31885d3b1c1f8b8d37542ce4c856506cbcf349aada02c8aedd3a0128f5516d0850a28582bf34e8b162b8077014166f695639ae0c635c0ab92614c3f8b822af66a84e9700bc298bc8d0be061927bed343007d9a2c6b0085ddb4f605143d790b50cebfeb14a0edc2af6dd63131ea2fbad21ba91d3a6747a0c116e028dc02c821e42d11604a2d7f0d580d5383386995f70218559e5e01ba9feb8d0470a2b8eb029458200a37171bffa010f3710fb406eac2f7103845c3b16f1f00f96c464cd11ac8dc110eed5b4095cc13013c224ea31d65366635acde0328cf66f29200e45b7c780a900ef35200289390ace60638b61d00389d14c702ca64f3af34880514fe411b8007c0ee1e4081f4b9daa1522d5179630310c04fdf06b180ee0e01726b3c7e048c65440885a4ac6b4339d93788797ad707c8071e737aa988a1c2bf90536b7853ae81ae1722dde91a0c337230c7942a1b729ddd8d7d6cb0d671718088811ae432ff1cfe135068c60a9033949c92d6a011d26338016889a5b780a6d2281c08d085ffaa80a2110d8b98d535285522b62da0dd520d93ffe9f1ab4188c6c8e6b9065cb5968f9941bcb6921bc4e6781c0b687d76e5ec31da0128d869d407c845628ffea8e3a09b44b99735503b22aea01834546537ad01aa4a657b0ea85164b8137bd030a7ba6db2db06c8932972bb1443ed1fe5d39940a8c5e9c73b4077d19172680ba24f776a007921bde9b552d0f60f000a865bcd0434b5e706f8398e01e4962b6a9702b8aeec250644e87e0860a73b8e1934f46ae8df0014db545900543d3d6a1dad81220efb0f022ca09b3d805363f50025db454a0759460dbf0132aa7c4d0c18ca1d80b224b662877102e1f2f50c2033d4410ad00cba68016c0abf348819f4730d683562160ef51665e48a64a6c52609712cc2e91d40737dfe60103388a9bd01b129611118c9e698864fef16c4e89fc47418d5553efcb1015081cf284c536ceec44334eeeb2ff22c6ff2da860e45914e6a0b62938eb8b5359087595a05e8bc3fbb02c82da988d11fe72d4c7bad0140718dbd6b00952593d99d80942bfdc22111cf009564efe9a123520c1fff00c635b49c0701ccc7ad387b8cebd8d7d3e385010f1d0ad4d8c73e003b4c7e56d478ddc9b1393e770574277e0ac879cbdb1980cbb6e4f20a20152efcc7d640976d790fa0ab81c8e9568c1bbaf7584e01727f79ff052868e2677b4a2a2b56d076f6015a0d070d0059f092f055588341337a7d7009902d3812e89415ed41e9b1de82c350719d0070182ae3dcb001daa5be05a8181cb6beb25257b6a4c64a405af09b417461e80e1bd0a01def00b9acde1d0164dcb91918a0d32d0550ada3e8f21ac8bb2372539695868c4c8d43808e801123b1061aa8a8915a6eeeb498eb7d80aa63f76e04b018bd3701638e6fcb2c802eb9ca710ac096931c3c1ab0e58c0558c8b37b0322fc57005545b8eb015420b85c029441263f14d0b65531e06cfa9faaf35a765587c1f0c370ff4f125a7299de0be9c91d35bd93faf447c2f2da73663c0ceb5bc418b02c59966d5b808e0d1ce92bdca2324894b79880b6b5d5ea1e4181ea986a9436330191b4fca67aa7bc16922c00dad5c01147c44b40de1453e127a0203b5be7741e7a5bb3b8f007c42eec0810ed81e5f40fa8c660a6f390016ec32b0b281565657457805e38af1d6b205d1e7d17925bf570bdbe008964328a9600bd2e9c8aa426a0179a54a6750694935a2d2844e06f07908f003df3518d5706a6add7a852db10674084b0ae00bd31f5f89dcf1f500553bbfe11d0fe0fe55fcf80fa94f38900b7a8da31351b8c800e0958ea3c64f675603c05e8edc35b1d01a138dc135b40a4a207fd971275de68531a47f9c95f0fc78500dd6bdcca6d5d3d42731cff0109271c08909000bf97698f8007209451a3822808e805d95beaad5b8e4c0041a7aa818ce0a9f54f034b573a742a1ac8156ac26506c401d01320d521a87b7f4089bada709d802bfb156cf2024459186a75640625d59e5eea024a8e7a96a62540875895db7f40d6725d2be016f49ec2050d5cb194ae026400d328ea2bfefcfce61f50cd45593dadad57bf0dd43e011950b755eb1910905e81f153efd476241e26f80402a4076cd40d017a9b37351592815ee6eb2b403e435076349016c6ed08b0b46f6ba081acb6e62fff80caa36e0bd00bf97475c164fcd6cf4f04e8d1954e5b031131b58b5906c42e94c7d7864070efff01e50f9aea9f14f5e4af9d00edeed8cd35d0d150fa597c318c96ba60a1b8d3d5c6cd0c644132b55418013d515aed2014f0088dfafaca0dc540d531ff7750c0d511dd9db100ede99dae04148b4aaba7b1005d615c7985ae1ee254539932a0644e6db243402fe17696877375686424cf52f48bd2d2a9c6c071b49349857833d042d8ff03f2a1ca7f4095345a0970552c7eb0ef08284aa364b705688f515bdf2550e530d4eafb04f4f4f4bcce3496954a5cd5c2b060ab37361f08b0646cbe2e3f71f5221a4f5552d7d65b0b9495a8bbda7711945d0da426978b0274b7494db527a0d786ea343450d34c600e0414b55faaa38129f3910d01520b8d614e809e6177f96a201f5bed0b9801651819ef3fa01e5f4da962e02aa0564924a0b712cceb92267ac6d04103594245050d0464048bc748ed154f404f6750d1e90474807f4e4aeaca4a03c6d2d340fab76add40027a1cf9fafc03e2cbb909d09369b7f2c68ada2df7933756b465a04c059504ec4e57b2af86f40276a7abc7af6f05e8f550ba8680a2ee9d0cff806a2e4637017a964db9f407a4ae8f349035ed3bcd3fa0dae48a2e980c1d05a12b20d0ae8aa900099e0c4e92a92bfb1405979f003def44b9d30998dada1a0bd0636b13fd13aba4ccf3d94a80f8b602b5543a01474f2eae6a20d5e1aeafd013eaece40f28a3c6de09d0d5d235fe80c417aa2a55e4519dec8ab7ca8357d05115b75e12a0c32bd5aef6041c3d852816a0577e52ee30066217aa9d471994e43374058888059bb900dd693e0d05687fd028af415142bcf515b1f4813af33fa0ea477e2120d1f391a5a4dafbe557ce7f407dec282780cc3c25621d01e20e0bba430dc463345b0ab0651c28713470d5e3ab28570645e98c78025c71643e7d014599ca94285baa54fc9b06adda31ff4f35d6861a48b55443580424262f884b027ced65996820baa10b01a15616ad3fa0c4c3df6be08a517310a027e4aa410c027a1b1ee53d0f7c574f0e1c4bd1493c64fc7624403bfe4d7916576fd156f6fe806c69f8d3a0283ee981003d23244d050462a1a8a1560212ba62a4770dc46552790bd0d3e4edad001dbaf2523d4fbfa8072072be063263a8aaaa14692d3147eb45017aad4eb5ca19015f2be0a98040bff5f11f508fdfd8080845bdaa997e04f4cc94485fa14b9a5ffe01f105abf711907a550f373704b87aa5e79780a228604332757544c42b14a017b94f13013a96c5969f14f54abea6fca4281e6743cd6e21a0979f575354099825e530bb4a398a7af9f996fa723c294fa9577f2d400242828bbe421b796a07f6803ddf2a8f93f707d4d3aae58109483d0d0265e287ba9e426a21033104f21ae8b5f67a02f4c4e07a2240af6d3b7c09d0f130ad9600ed61adee04e8b8f1ca4580282dc394677165a116432d56c9a0a46a5032122093e0fda392ecb028432501545b18e9a10174660222ad3c23017f9eefa206ba9e96fe806a0bd772858e55a0fb09d0739bc411c140697571cb1128aa2fb7cb0bf85b6d2d1660ebed09fa02640299df7809d09bda7dd7024abe12f5df5180de79542db84d408f8b25f2f86e2ceb8d57270212f54e0d5b6e5bd47334a5a31915a5dbe4ab3dca08e89d258ff33fa07e22867354d4e12f6aa24110d35b974979860099d21da8f04a027a2d5757bd8fd895291181f110f0376e9ad340a27eebbe00bda56167a181295b5f4d048856f7d7a53f20ce615b03d9b8b963fd01f1e33605e870a0aefd07d4b04fc7d14076ba32df022486d59fde34103b28a7efa263cff333017a5dfc75ee0fa857a8769822a0a7dba64a47c545dd25be7e05e88143357389819acae4ab3160023a92a974fb03ea059dc70264013fe375d4a028b3d1d51589ab37e6bd9f04d8629e9b2f01aef44e5a350125517c83b2001d4352d33fd10bd7dc954a4a8afacba9303e023a62f73c12e0ea89394d0dc41abfc77f40fd64dfff03aa91566b983128291fdb712a40fbe823296951fbe9d472c90c5c2540efec2e614ebb1902154045407aaf81da6b31034aa2f20505b41f2650ab6713d05edac3e40f88e36e25c0164b49cd0326e0eaf940953f20fd7d5380848b053b7d5bd173464e0ae6467a03fa9180581a58357d30cc15f51c5e536e5bd4ab9dbce4698bda4fd7954c8bb21296af1cbb047c3d6691d5d330efc8ca3e8172968779d7d4a2ee0ab06417d58e2940440c9d820612efa09cf604647d7ee4e67f407eb2d4c094e0eb5880d651c64540a0cd0d7d85f64ca8e88e0c88dceabbe82d748eba60914843efa74051b70ebfbb003db6764f04e8e03835e58e819ed5dafb03ca707e180242a9b881dcb65450d64550cf5a98905dd0a2e7d61ae837960828e916f72cc0d7cbbe3b0242f10735de0222196d1a0702c4a7e4ab585aea53eb35764b1ae8b89cabe4f1f7b46a4561067ab86526402f68987cfe80ca63db12a097f45fab87335d89f80fa244807e636af88981afccaff14580de714b85ad11d08e88b5aa7466512f8fb3ce0bd0469e1908d0eb7f0cce1ac89686bba20089cb314c5f80349f86d910a09be0572c40b7526ee50f289b4e2dd343a028bbdd9c3d01f2928d8bbe6de4cbb2ef59031b5aae8c130657478025b6a55a6892c0df8220910625593dea23409b70e6fc0f881fd710a0e737a8309cd02aea998fbf93003db656b9fe01f1063605e8a1927427403bfe3f3a0f3d49f1bef90352c7a4a445e9561bf3a1003125fd6d5b80f608e4331f5b68eb1637f8be05c86a05be726610d0ed586da08176ba5d05687b6c1a0890c014dccf1a481b1468a0a766c8c7b6752d0cd4ba2c0c4479d654c5b5b5de3754bf210362b05a02f494eecf5303e9789b3f017ae58560ad817434d50afe04f4a0df4fe7e11465b5a48e803f13fffe07c4b715fe01655c5dda1a286bdce85734f065111e791f7f0af8f30f88efe2f507d4975bea3c4a22841ffd827483f24afe8054a9ad80403b889a7f40c6e7f43bd5f5a3d1f803aa231148fd28eab89c9da981ad3e43f215a07b8d6a137b06d2183ca57ef096631998c84f4a4e2083074ac41c37d022e60b90757a03b58030016d5d98ca76708a127e6bd403017a9d9ae149037963ca3143403b32d705017ab9a0574d035f3d9c5aee8380fe0c3943809ebd90fc04e8b5f65a9247a964cab256ea43b9240d32ec130b9061f460a7aaa55bd473795474070153afa5166a205fce9c09d001fef9fc1f10f7f14780abfd0e670125a51b02b5763a01bd3ad0efaa8184be053ad35002fd6afa0a6d9eabd51a33a0be8b215794745874f4f903cac853e1a6dc2396ed194b4a5e8aae0ca4fa6ade2b011d1ff4548d74b19893feaded09d01155d23a14c9fe500fd71dfc01656caae98304f4c623759da9764418af3fa05a87ca538056c0bfdb1f90519e8b002d40d157808e4eefcb1525dd5d199f0588c5163c956497b4f6088c9300edc854b1c504b4e1ac9662c880ccb1b204586205b7aa02648833d8adfe80aab86b57800e1753636b19908d241f02f4fb506105199020ceb60632da54b909d02b61ed74c14ae2965bea67d16349157d97bfe0c9a7003d5347057384a5bf1a7494c7e76544b32b94178e802f913b63d57ef86ea8671babcfe0936e9025432a7f405598e02440c25e83ee588023be8bfe5780abcc0dbf7616200310fe5c5fa187172a77015a578e230dd436a2be7c6cff9fbcfcfe80527cb98f0099bd60a8b9d521cf1290a012f5e57cdf75641b40f513f6f549b0b1b20c82a29ecca2567f0983bf6f9b5f0bf0f5ea2fbe001d6730ba08904837dfad6b20459f9fff807ab84a2a407cf4862db7fd7bdae0f507d4d3964e02b4de2f5d05e8aed760a581ecc4aae62610b002055a4d01b2b8bcaf860533a01eee5b1590f812b9a3544158d401546a0604016d9f3e130d741b3412a0f718da2dff809286f946c0df84dcbe003d70d8f5fe80fab6cfad003d4544ed79c040b7eb650112f36da87d6818f8b28db97ac9e1df4baedd041474185f5f035923b351172006ab31ab08b0c50d5599097002e5dc51837e0402a5c40db51c7f18fa4120c3f9aabfcf1e3459636623407b8cc42089fe8cabc3ec0f8888a502743850adff0764bf82ae0059dad75703100c74c3a61e2e2a897a359ea180bc6ca7ae56df27a05dbf2a5c8c805e3c4b3acd0c54b3d5a809d0db23b5fb1af8aa8ea909b91990ad38f4154e209315de025cf1c18abe8d4aa11a4a33d456e80412d942a7a15a87b8a8c381cce91f50efa3f1fd0332c49913a0a7fdd6ee02f40b9aabbe475cd2833a6b47805e395f2dd99e01f570a5b300fdc6dcaa00d173c6ddd640ea47e922403b54839c06e2a55591a1048a81aa85b99700bd755eada9816e4f1301b2c0923f9282f97931be5f5230df0994446d94982645f16b07575b80369cd534820cc8386145807621d5771a480bb3d679889dec8f620d64694e35564020914e910ad10c9392f6490f7e7f40bde4e7e60fc8361a6701f25dfc9694b4a41d558daa00474f126808d001fe734f403190ed1aee02f412d45d4b40ac96e909022987af577cfcae340854d0d1a52bc055cb5a05d5ac6a4739ad5f82c3540353d687390ad0a33c2af0808134b0aae804f43bad3d1428e905968244804cdb33527d85259641a32c404feb34ff80bc0fb72640165af01b1501dae65741360474284feefd07d46dabba1cbeaaebfee02b40e6de076a7b2402b16e823b0212d909ad263fa18a2bfb15c8fbf00b3a1232d35151bea897f754b63601dd8bae7515f8935bb32ec0d60bc5ed3510c79d5a4098809e39ad16ba26a03dcecac863202f48de180359633714a0c3f86cfd1359b9c59f5f34106bbc3e17a0576e51e241402fbda8966f8cf254c75475e848a6415e2f76abaa54a1487d9c0c842501dae5da5235a85092313e7ffe11a0272a6d561ac8b3a8903302da52529b621290c73794d943403b8854f4310199a567d8ff80aa307f570412903a1f09087d655baadd4c08448192ec91fa2e055e0a4c551829074f7455efa323a024fdece5e80fa8a64fadc8454057cbd14e80de4a3050d5d22c8aab3350dbff466649cfaf9cef058888f92a32838078e08df958403190d137c9b414aad6c19f1f05c86dfda97a38d3cf8997762a57f87a6dcac15580259d4415d648400697fc5e4380ac0f63a8ed3c0804ca2e84d22f0442359dc118aafa6115e93b644fabe6031188451a54086f64e93639d82dfe80ea15a8c5f7789155094b520bc6309082cd25d3bf37a6b63721a0b79cfabb8bad1e2e50ebe21328ea95e38e7f405ee15b80deeac9eefc01d56a9b35017a791cb3fa07d43b9dea82e9eeecf81f90be475e0371208ef4153a3a4c4d45882c5f0f60f64f02c480f7e7ff80c4af17fe8004821efe807aa7f5c51f909ed64f032dfb5d015acfcd5301da43126c35902e60e323c0b2a4db941360eb61e3a906b60c60eaa28b7d6ae4f27f407d865e534051cf041d69604ba69e00df51cf32de09d0d5f22899067957b596af8a06a1ec532405e3c5d33330904a17b881ecb7269f817a67eaeb7bf2b4512194afaf32b575136ce4d602f4362b6a7fa00c484ba79a60db97ca6fcc4c010553568abf0ad056b03c1c03a9a70f0d640cf83bd340995f46575fa15f905ac02fb2e959d4152fd5e4d8a1a39ec59766dc29e98526a79106a25e955792818c15a82e20011dbe3f7f09d0abe1a8585a023af2afa6ef128935ae263c10109d1de84c79854b558eab80827445d5fa85044ce98aaa056209d8ca5d6a4c67024815aa964e0ae68b5d6834542de47143f52ce3bd00cb92b0a48d8050befeddd140bebe1a5d211069a064ce0973a12cd5a11ece2d95b4469e0890b9f7febc2c400f5fcf9f02fefa0d4a4c79c290345b670196987083b700bdb7e04b32f58ba16a2d55dc27815876cc99c84fc2bc94b4225784a63c8bdae69e401ccaead94a03154bae6e820f02b46f5cf54e08e84ed1742f404fcaab2a6babc84ba266e0dc1160ab812163d61610ca4282d3b300d90cdba8aaaf5f0ca4d90a8e430156513d5ce328407fa8a96afa8aa115aa765d0d1b13f0a531506e7d0261a8aa94da2780402c79a839cd0492506c6df58248211795107655fd28951c5ba600e40448d89a3fdd09d01ef8fa5a809e82385555aac42b5caa977c17e0e8b9f72f01be29f33de427d4b0a93668ba14207dc2a0ae5a3a9f644e2cd8b600994016a88d6823ca53a6304be5f77d31f10d151240a0a896a03644b209044aa2baeab63ecf1754058380bcafaae54a320d6c154362783f01c55096c43404c897f3d59c5502b10a9e34949329f2c382dcb6321160e98a7bf803eadbf61d0151288bcd350524f2c61a4aad057e24aa40cd7e2210cbee72839b80448617baae06ca1b688c55bb1e047ad9d56f4540a88410c944405c54b79dc94f789c589563af813ccb772ec091a2abd03702be145d6d9c48209076fdbb1010ca3b55abcd67402a8cf107d44fee250151a8149ff71510cb4f2a230189ce54754682281fca0b52aa31f44ded856b696089be1d08b0a5c59ddf044820b95f52b70d035954c41095140696ab0a26667118c81ea950638d04024742ac2e02629962363afd0159935ab56361988b6419fcbd80bca39eb6b1d340357dfe485f61c9fca8f14140499c2a623a119016a65f14104a03dbe8088854b50caaaa618b7c09d20baa973f20c371eab65120fd7dec5c0145d51733d4fa1f047c2d2f2d017a1fab5e574024cb8ea815fd18481f687e1590a855cefc81d2401147f728355f159057c0f82e0514e4e1aa52d2d096e6732c0f173a6a18cc68f40494a46adf8b02fc5016af780b082453e55f8fa27f15579e96aaa5ba4b652c40aa258eaac58d22dde4a829ff04c4620beaca3288a9e953e5504b6246b12f02e4f77a0262193c8894691d533ba6c4742957047a7ddc876ae962aa3022b72f019688a9dafa8a802b02a4b6422740d9676020050b036db2a47f4044bdae41a0ea5875ab81bc0fb5cf2a81d097f6b4a3812ed8fb0fa897acc2b309683d57f10424d27c56477f403dad5aa5394a829cc8fef4224062258d3904d8325452fb09284af4e0f824207095a726519f32092d3104be650db4817610608b493b980a707c09396b0b2895641063ae81788bd5d47206ba3d956709834816353b0a88d44bc6712d2051ee52ec943a49a25c28515959431fe7783e98aa1f7d0538b64be5b11250288929e90a7054e5875a4d8b401489499b49659ce7b1a3ec2e6a5e0181501e5fed7049202aaaa79dce0524aaffe24f1305385e2ebb627816507064b5b59d0053adfe62a8cd390958d218f4da02c4fd630c167f4059b0bdb206524fd5b72550129fb4da4e8c402855aaa3338da4168ebb0a445a898f7b7f40321d08b0438961cd2a7f5c08646b01633016604b33aef6a0229048b3a5fcb8313505dad8ac09704b32d9ba23a028b306860f05fe7a7cc3ac718c4d7aebaa611b2c05c4ae54ed5881d094a64f39bb0838ea2e86da48924049cf79af09908032433eb6f9a7e6ab9e8028521f7b785120ca4963304c0598a281c6fd3fa08ade2d08b094dc1adddc1f506f6ce66a2077512d7f6c71f4977ada89005bc64e6696809218df7d570355b57d656dc55694d362da11900f644fe1b5804228719f790196e88699f307541bf4560f6707b25cb23f50efc30e8baae5f707770125f9d8dd9580481ac7e15540226f6ca82a0c2f41a484702879d02b94613025ea8eee47f923f5291d6a839454ce4a0ab0434c95e3a1817c86992dc092c79f1505d8a16c73afaab61bc8ae5d86da988640a4ad9ca1021c21a41ece14409d3155c722016151469a6301b1c8ede8a8009970aaf297949852b7c9116950d5b2a8db425f358e71298c4bb227685540e2cbf0f54101fab6caa1aa4213623f141f8a97c24b0d2ff5bd34f0d2d04b232f8df531f1d28d976eb3b4f3d2bd971ebcf4e8a5272f3d7be9253b2772f5d29b97debdf4e1a5a9973ebdf4e5a56f2ffd78e9d74b7f5e5af6d28a9756bdb4e6a5752f6d7869d34b5b596a7b69c74bbb5edaf3d2be970ebc74e8a5232ff5bc74eca5132f9d7ae9cc4be7d9c9c24b975ebacad2da4b735e9af7d282979a5e6a79a9eda58e97ba5e5af4d2120cc03060f830822c8530a22cc53012181b185b183b187b18071847182718671817185718371877901961a4309e305e30de59fac0f8c2f8c128c3a8c0a8c2a8c1a8c36864a909a305a30da303a30ba307a30f630063086304c3833186318131853103e96463016399a5158c358c1c8c3c8c020c138605c386e1c07061146194e003be01df871fc00fe147f063f809fc0dfc2dfc1dfc3dfc03fc23fc13fc33fc0bfc2bfc1bfc3bfc07fc14fe13fe0bfe1bfe07fe17fe0f7e197e057e157e0d7e1d7e037e137e0b7e1b7e273b76e1f7e0f7e10fe00fe18fe07bf0c7f027f0a7f067f0e7195fc05fc25fc15fc3cfc1cfc32fc037e15bf06df80e7c177e117e0901101859f2b3142008b31421881124083608b6087608f608a86abd111c119c109c115c105cb3744370cf8e2a3d10a4089e08a8d67d11d04f3e59dda313faf397a532820a822a821a823a824696a812d2bd5a08da083a08ba087a08fa080608860846083c04630413045304330473040b044b042beae720c821c8232820301158086c040e02aa7e549e22821242203410fa08038421c208618c3041b841b845b843b84778c8a4323c223c213c23bc64479228fa373d5b13e11de103619a95cf43f8e4c4272f2e65f846f841f845f8e3bb8665841579f8b08ab086b09ea506c226972f6c216c23ec20ec22ec212491a3f30142ca6a84d0cbd218e104e114e10ce11ce102e1324b2b846b8439847984058426420ba18d90b2751116b35442044406221f5180284414218a1125883688b6d991ce7788f6880e888e884e88ced9c905d115d10dd11dd103518ae889e885e88de883e88be887a88ca882a88aa886a88ea881a889a885a88da883a88ba887a88f688068886884c84334463441344534433447b440b4e4441218ad10ad11e510e51115b273139185c866398c1c442ea222a21262203610fb8803c421e2284b31e204f106f116f10ef11ef121bbf288f884f88cf882f88af886f88ef8813845fc44fc42fc46fc41fc45fc435c465c415c455c435c47dc40dc44dc42dc46dc419c49604ca98f788078887884d8433c463c413c65398c6788e788178897885788d78873d9318fb880d8446c21b6113b885dc445c42524406220f1591493203b86482224319204c906c996252ad921d9233920392239213923b920b922c9a42eb92379f0799222a18b5f5cbb8217126afa7f54f7a81eb204269f2cd1c90f096902fa550549354bb54ca9d02dea481a48482029ff169d2069f391ea76d241d2a53c91f490f4910c900c918c9078d447413241324532433247b240b244b242b24642c2498240c5ce232920319158486c240e12174991a5312961036c0c6c94ef62136013b2d46ea2ac64d40875b885a0df07a4b50ed8c4599330c08614e3598edcba68b1a4f7b549b0d9648257c3668bcd8ec56f430f4947cae188cd89dfec867e4ea2dee7066c73c5868ea43ffbd83cb293313629367419bdd359f6e71b9b0f365f4ea479363f6ccad8d049151bba1165dec0a6894d2b3bb6b1e960d3c5a6870de5396039dc0cb119657a2f93c38dc74752771bca7c82cd9413c9e16686cd1c9b052bc9cd129b15366b6c72d8e4b1296063626365c9c6c6c1c6c5a6884d095bb0f86d0d6c7d6c036c436c236c636c136c37d86eb1dd61bb67edb73d607b64d9db9ef8647bc6f682ed15db1bb6776c1fd8a6d83eb17d61fbc6f683ed17db1fb6656c2bd856b1ad615bc7b691491dddab896d0bdb36cbe1b69369c5880575dbc5b6876d1fdb01b6436c47d92d3c6cc7d84e5808b75396c6ed0c5b2ac902db25b62b6cd7d8e6b0cd635bc0d6c4d6c2d6c6d6c1d6c5b6886d89457107ec0cec7cec02ec42ec22ec62ec12ec362c87bb2d763becf6d81db03b6277c2ee8cdd85257377c5ee86dd1dbb077629764fec5ed8bdb1fb707bbafbb21adcfdb02b6357c95215bb1a578f5d1dbb06764dec5ad8b5b1eb7062a9a37f75b90a51534e27bb1e57ad5d3f53805316c5dd00bb217623d6603b0fbb3176932c4db19b715de57a4ba2db603909bad931abe7ac21ebcad66321dccdb15b60478fb0c26e8d5d0e3b2a70013b93ac49ea29530f1c3b17bb227625ec81bd81bd8f7dc02a945b900eeb2712a27d887dc4fa701f639f60bfc17e8bfd0efb3d8ba27275627fc0fe88fd09fb332be5fd259343caf78afd2d2bee8ef51e8999d2ecdc381dfe77a2cef777ec1f2c697bd29cd420d109d9a535ec5fd8bfb1ff60ffc5fe877d197b6a2728c32b6bc27d157bbaa68e7d03fb26f6adecbc8d3dfdab8b7d0ffb3ef683ccce74b01fb2f6db8fb0f7b01fb364ee27d84fb19ff1f72333663fc77e81fd12fb15f66b3647f739ecf3d8d3bb3033b3931ed3c2de66edb7a70c5dec8bd89770009f1f0c1c7cb6420f019ba38710870887188704874d76b2c56187c31e87030e471c4e389c71b8e070c5e1c6a278a05fd1cf95daa193279f1c5e38bc71f8b0e579f8b2bc1d7e2c9687320e151caa38d470a8e3d0c0a189438bad4a123c528987360e1d1cba38f470e8e330c06188c38855221d0f1e8be2618cc3048769f6af190e731c1638d08d56595ab3ad4ba278c8e190c7a1c00dc4c1c4c1c281f27732d54737753333954a52c4a18423703458131e7d1c031c431c231c631c131c37aceb8e5b1c77d9718fe301c7238e271ccf385e70bc6627371cef383e704c717ce2f8c2f18de307c72f8e3f1ccb385670ac72524278ace158e74f49dff148e2418d1dfdbc89630bc7368e1d1ca9303dd681c74cfc8e03b145e9cf23f55fda5cb58e231ca91743d98e719c64ca70c276d9718ae38cea6766a08e59c1b25251a2d8d1b2a744f1a84e38911052b527e93d7a2c84a4ab8f731c17382e715ce1b8c691de431ec7028e268e168e368e0edf9425c563bbf7e8b21c1d8b389670026bbb93c156e7c9c729107d78527eed5384538c5382d3864ea81f88d316a71d4e7b9c48c63cdd033ce344866b858fbb41d6f7fb65c6e74f1900acfa4e276e9076d4ba34713ab31a64097ce174c1e98ad30da73b4e0f9c5256832712d1114ef4df374e1f9cbe38fd702ae344223a65cb933a09a72a4ea49c0b3851e60d96c013e5dcc2a98d53872f3b75b39ff770eae334c88e439c285b0fa7314e139ce89a194e739c16382d715a7122213cad5913920e24d93be570cae354602d773271b270a20b9cec48ff2db280d1f154c219381b38fb38073887384738c72c66e784bb7fe70d8bdf798bf38ebb64e73dd77b3a219123f13b1f703eb2289e4f389f71a60bae38df583cce779c1fdc973ba72c8167caf0c5dd3cb230cf6f96ae33fdea8bf30fe732ce159cab38d7b2933ace0d9c9b38b732c99ce0dcc6b983739775e9b98733956ac0762915e64cf98c70f6701ee34c574e719eb1f63bcfb9374bddc2f302672ae70ae735ce54aa3c6bc2335d60e26ce16c7322d93b3b2c756717e7627652c2057c7231d840bdf8b804b884b844ac212f312e092e1b5ce87c87cb1e97032e47ee1c5e4e995d9af50f59251ab89c71b9e072c5e5e6a52d5ceeb83c58135e52d1819727f709a9f25c5eb8bce9c866d4e5c3f58d5af9cb374b3f5cca5c27492b5c2a2c78970a2e55568c7c526181bcd458c591f2500293a9c7ac92dfd9b22348479202ea8b5deab83458d95e9adcb85ca8306d5c3ab87471e9e142771fe032c465848bc706f365cc9a90cdc3361bb4248497096bb5fd8585f032cdccd1130b218922a9c4cb2c13c2cb9cbb7fdc49dd9332cc0cd13c2e0b5c96a258d9c07d70137259b1045e562481d9f1a3c5afc66d0fdb03f4c06b6ea8c852bfe4d8f8bc5056055c4c5c2c5c6ceefe5d1c96bd8bcb89b41f1b9c6fee0f5c8ab894b8fb7705ae06ae3eae01ae21ebb46b846b8c6b82eb06d72dae3b5cf7b81e703de27a62ebf47a662d4a027cbde07ac5f5c6bdc1eb1dd707ae297b5fae4f5c5fb8be71fd64e98beb0f57babec27a8f7a7a2487d72aae355ceb2c6cd706ae4d5c5bdcd3a323a736ae1d5ce9cf1eae7d5c07ece7b80e711de1eae13ac67582eb14d719ae735c17b8d283acb24a4f455db3245c7392483b5df3b816703571b570b5b33f1d5c5d5c8bb89670838822750b6f066e3e6e01cbdb2dc42d6269bcc52c84b704b70d6e5bdc76b8ed39913e24b9bd65bfbd1d713be176ceb4628dd5efed82db15b71b6e77eeb292c6a6e3ed811bfd8bae7fb110dedeb87d70a35bfc702be356c1adcad245f5cf9fb1f8dd6a7c4e272c842e2bba1b15ac815b93e5f3d6c2adcd57de3aac006f5dbefed6c3ad8fdb00b7216e74478fe5f0366651bc4d709be236e32a4e72482af136e76ec58d4ab2c46d85db1ab71c6e79dc0a5cb56e266e168bdfcdce8e541e7a4545dc4ab803772313c83aee3eee819756b85ab22856f9a82a2a9d28f5487d45b6e0322b8f8ef7905d32f708f7984f4821dd13364aef74b2c59d2ed8e37e60a3e97ec4fd84fb19f70bee57dc6fb8df717fe09eb2c6be3f39dbfb8bbb6ff737ebc32473ff909e237d48fdbefb07f72fee3fee229262bc973321bc5770afe25e532e50f61addeb99e7937227abf29465dae027b95399a8fbebf2c3dcdb99d768a29e2d737e563221a41b74c47fc50e980177fcee5ddc7b6c60909179a747a5f20d588fdd496b55b8474bb2771fe1eee13ec67d82fb94adcdfb8c45f13ec77d813b5db0c27dcd5ff19ec33d8f7b017713770b771b77077717f722ee2536591fc0c360b3f3e1b309ca0a708947c01dbf478847c43e4c12b9478c4782c78645f4b1c56387c71e8f031e473c4e789cf1b8e071c5e386c71d8f071e291e74e50b8f371e1f3cbe78fcf028e351c18372ab65bc9ea5061e4d16d4478b5d948f361e1d3cba78f43891303cfa78d07f8799b14abf1de1e16527633c26784cf1a0a2cef158e041255fe1b1663bf391c323cf624626e8a3808789073da38d879325178f221e25a4406af0af521f698034441a218db951203327be65ca398f7403aa1cf4c5a9f1a51e1199612496f4b9a301f70fc9f0893bdc094c2f99af658cf40aaa12a42b48dd51a34caa8fda62ea74a42ff6d350b5264b8cc4922490d4209ba0f4af2f4b29e9436aacd332a88690ad483d149293b401525fd44b248d171741f62475f2c9fca11e4adae58e46dae3569b4cc1e4045277d4df4b0760cff9585a706aca530ff4674a925c62651838dcee53bd4ac9f87cb134a6f3cc0afdc951c9a1528f6467fe391a490ed305ab3e525ce9324b2baef6e99a7b5829958a5e48813bb1a9c91de6d40219ccd43f4aa91854f8129ec0d3c0d3c733608d4512fe0cc52b4bd9b26e935186ccd5d2c133c2336665f854ab7b90a2a43ee1535ff4dc6a13b98767d6425041ef0d2e374920a59d2726a86a5da888cf3d3739746417adc3b2478d16abc4be47aff279c83a7e2a75d9f23c36580ed9f22ce379640b843a7ecf132bc0271537cf82c469c48e50a382e705cf2b9e373cef783ef04cf17ce2f9c2f38d2775b4e0f1bba007fbe2f9c393f2ac70dfef59653924f17bd6f0ac676216b287f3d96031a33edeb389670bcf369327fdbc8b2795964a32c07388e7084f0f4f7aae099e5396c3e70ccf399e8bec4ffa09e5bcc2738d678e4702d4c9339ff1029e269e169e76961c962e52924f17cf22abcd67895d32c6142fe065e0e5b390bc02bc42bc22bce83cc16b83d716af1d5e7b96ab6d9905ef75c0eb88d709af335e17bcae2c8aaf1b5e773e924a7c3df04af17ae2f5e266e2f566d3f4f561517cd19f3fbccad9b1c236eaab9ab50239d68d8f6c1c85fab7af1a5ef5eca4815713af165e6dbc3a78513e3dbcfa780df01ae235c2cbc36b8cd7042f7a90195e739655ea34be162cabaf255e2b96cfd71aaf1c5e79bca8fc545a8bbda624212f9badd397932517af225e25bcc18ed3b7c1caf0edb3e7f31db0df85ac4dea07be43eeb6bc236e734908df6440996cb8920e54be99dd2aab84d90929d27782f72633d30e74e2517b41fd43b248a91a1fa7dc7f7b6f3399c9dcb0caebc13e15718264b6de87f309c87cf3f0dee1bdc79bb23ae27dc2fb8cf705ef2bde37bcef783ff04ed927c4632154362af90bef37fb3bdf74232af60fef32de15bcabac031317ef1ade75114296bdfd99fb8197857887de0d6e219eda25f36e664afc936942d2546d91c3e0c5267840056ae1ddc6bbc3e9d8cf86fefa6cbb93e09da834dd4c02eb2c81a2066d76ba90fdf9ce5c2f6f4af48a87acfadef4001e7b3ba9fbf71e6769c212f89e7277ff3dc37b8e377da125de2b1e007cafb95bfcce719ffe9d670bf65dc0dbc4db621394c715e8c9ed2c3978d39317f12ee1033632b73e3e06bb553e741270bf8eeccf4f884f844f8c4f82cf069f2d3e3bee137ef6d9f9019f230b219d931a64d95b6792b66651242b9444918e9f133e677c2e9c58c00c7ce82e374e248a9f3b3e0ff65e7e527c9ef8bcf079e3f361c12369f97cf1f9e153c6a7c2be1992a54f159f1a3e75d6789f065b9e9f263e2d7cdad9b193252a798fbb8b9f3e6bc8cf009f21e7f019f1d1df67271e27ee3156d9f435726ca6d2d767c7e9119f310f669279fc99e033c567c6c6ea67cea2f859e0430fb5c2678d4f0e9f3c3e057c4c1e35fd58ecb3f9d0afa86cf468f4a24af8025f035f9ffb93f10cdf00df90e596f4e737c23766dfe937c17783ef56f4e177c73dffef9e5d359b1fdb47bb2abe0796c3ef915d32df137be0be676ecaa916059bcca97e631df8bdb063e67bcd646fc97248162c8f87b5f89c44942dd5acc7c8c3891f7c6ff8ded9a6cbfa8d996a9970574b4b26a72fdd31c5f7c999245b16b3ef0bdf779628872fbe74c732be157ca99cb5ac84f52cff0a4bcdb7812f15ac25baeddb667394c7ff221eb1201df8edb045faed664248ca9154e4b7c742f8ed676af0a4faa9ec9851c30f4a07b2fdf965b72febc02a5ba4df01dbdfef16abfeef301b792738c2d7e3e32965d9fb8eb913f89d64e75391c3ef2cd3845d7ea7df39be0b7c97f8aef05d671f26876f1e5f2a9589afc512f8b5f1a512bbf816f12de107fc0cfc7cfc026e907e217e110b21e5f68b19ee4dfc12fc36f86df1dbe1b7c7ef80df11bf13cbd88f2ebbe0776591fbddf0bb67270ffc52fc9ef8bdf07be3f7c1efcb3aedf7c3afccf2c92715fcaaf8d5f0abe3d7e023891c99a34a0dfe9af8b5d8b0fcb5d9e6fc75f0a36c7b6c0dfefaecf9fc0df01bb2ff93ecd8df883d343fba788cdf04bf297e33fce6f82d58f5fd962c84bf150be16fcd52f4cb6526a8c31aef9767a9fb51d9e8312dfc6cfc1cfc5cfc8adc69fc955006ca06ca3eca010f69b000d7782c914f94588e582ccf29ca212bc6f3913d43e588c74eca312b4692c3f318e504e50dca5b76a596773c7a51dea37c40f988f20965baf882f215e51b0f6394ef285326292b43fa2f8bdf843d3ae527ca2f94dfdc6f2c7fb83f59feb210967f289751aef0b053b9ca63bcbb27f712cb35f69a52f528d7516e70bfa64c3f6ca1dc46b9c343565c6dbe993b9d2ee8a24cb7eea33c407988f228f3ba13f4787cbf3c66d3b43c610f05fd8b8533c77245e247894ec8da2c4fb9329767ecf2a0ba5d9eb3ca2121a4aa7e69a0bc60070715a3bc649f5f7985f29af5b0fc49d9e6512eb0bba86cf2500ae5495951a29f53cfb04c8f4faf8e1ed9cd5291bd2de512db9b15a062a0a2c609c916258d5909b8efb8bfb1578644f1b24425cc8206aa2cd99588ede3dd80b5792516cd4ecf564950d9700a9ba86cb3e30e953d0fbb530f907460e5c0fecfca1195132a67542ea85cd91353b965e98eca0395149567965eacc72af4df0fabc1ca979d31a7162a3fb63c2b6554a82455546aa8d45169a042d7d07fdba874b8177124d845a5874a9f071e2a035486a88c50f1b8a75719b34959a1324f59015666a8cc5159a0b264fbb0b24265cd2310951c8b5325cf8964a952e02369b68a898a95fdcbceba6d1e6b333a561cee6811a9b82c7895225fc027255481aa81aa8f6a806a886a846a8c6a82ea06d52daa3b164ed27ed53daa07548fa89e503da37a41f5ca9a901460f5c663f1d53baa0f5453549fa8be507da34abfa2fffe502da35a41b58a6a0dd53a8bdfb9ccb2576da0dae481c46a8b87f26f947f9b7d3cd50eaa5d96433253a9b759edb1363656e219aaf6593156071cd9b39da13ae4f1c3ea08550fd531aa1354a7ecaaa9d2bfe6a82e50a51c56a8ae51a5cbf2a8165825f2bf4c54ad6c287fc2238a551b55cadce5389b3413c26a11d5126a40cde05144df44cd472d402dcc52845a8c5a82da06b52d6a3bd4f6a81d503ba2766241ad9d51bbf0407fedca8e9cda8dfb63b53bbbf76a0fd452d4e89a176a6f1ef9a87d50fb66c71f0f41d7caac216a15d4aa59a20beaa835506ba2d642ad8d5a07b52e6a3dd4faa80d501ba236428d7e32466d82da14b5196a74d3056a4bd456a8ad51cba19647ad801a3d88859a9d9d38a8b9a815512ba18e2c19a8fba807a887a847a8c7a827a86f3221ac6f51dfa1be47fd80fa9165b77e1221ae9f51bfa07e45fdc6525ebf73223bbbfe605d7fef7023514f397ca1fe44fd85fa1bf50f370ce100f52fea3fd4cbec92a95750afb21b8652bd867a1df506ea4dd45ba853b61dd4bba8d3657dd4e98774cd08758f2defd304f531ea749ca23e437dce63c1f505ea4bd457a8af51cfa19e47bd80ba996565b1b15ab751775077512fa25e4203681868f8680468846844d9314623416383c6168d1d079a35f66c7d350e3cc8de3872da1ed038b177510d7c9f0fdca16a9c592ca9ff66ac5987342eec206d5cd9b74917346e990b549ddfd1a00cd3ecf844e385c61b8d0f1a5f34e887e52c55d0a8a25143a38e46038d261a2d34da685079ba68f4d0e8a3314063981de9461e1a633426684cd198a131476381c6128d151a6b347268e4d128a061a261a161a3e1a0e1a25144a38426b264a0e9672940334433423346334173931db768eed0dca37940f388e609cd339a1734af68ded0bca3f9403345f389e60bcd379a1f34bf68fed02ca35941b38a660dcd3a9a0d349b68b6d06ca3d941b38b660fcd3e9a033487688ed0f4d01ca33941738ae60ccd399a0b349768aed05ca39943338f66014d134d0b4d1b4d074d17cd229a253eb6809681968f568056885684568c5682d606ad2d5a3bb4f6681dd03aa27542eb8cd605ad2b5a37b4ee683dd04ad17aa2f542eb8d5626a5ad2f0b5beb875619ad0a5a55b46a68d5d16aa0d544ab85569ba3795a1db4ba7c42624c366aab87561fad01c7cd85dc47436b8816e5e3a135466bc23d3b527dad2927eeeb51a96668cdd9166d115fa2b552e6e88b536bcd3141941759b76482932d4e0d432b87561ead025a265a165a365a0e5a2e5af4164a68036d036d9f1b89768076887684768c76c28182f49ced0dda5b1e966defd8626cefd13e70f044fb88f609ed33da17b4af68dfd0be67e98136fdea89f60bed37da1fb4bf68ffd02ea34d9954d1aea15d47bb817613ed16da6db43b6877d1eea1dd477b80f610ed11da1eda54a409da53b46768cfd15ea0bd447b85f61aed1cda79b40b689b685b68db683b68bb6817d12ea103740c747c74027442742274627412743659daa2b343678fce019d233a2774cee85cb2e3159d1b3af7ecfc814e8ace139d173a6f743ed9f18bce0f9d323a1574aae8d4d0a9a3d3c852139d163a6d743ae874d1e9a1d3476780ce109d113a1e3a637426e84cd199a133476781ce129d5596d6e8e4d0c9a35340c744c7ca928d8e838e8b4e119d12ba40d7e06cbb3eba01dfb71ba21ba11ba39ba0bb41778bee0edd7d767240f788ee09dd33ba1774afe8ded0bda3fb403745f789ee0bdd37ba1f74bfe8fed02d67a9826e15dd1aba947903dd26ba2d74dbe876d0eda24bb7eea33b407788ee085d0fdd31ba1374a7e8ced09da3bb407789ee0add35ba3974f3e816d035d1b5d0b5d175d075d12da25b420fe819e8f9e805e885e845ec6fefc5e825e85147668cde16bd1d7a7bee22f60ee81dd13ba17746ef821e911b7a77f41e5c697b297a4ff45edc45ecbdb301c33d77e77a9f6c8c814ebe992598a5de0fbd327a15f4e8bcc611a7bd3a0b58afc1aaabd7e4e18d1ec1367a1df4bae8f5d0a3820dd01b72940c07acb9e88dd0f3d0a35b4fd09ba237436f9e09618f325af23823897b6f85de1abd1c7a79f40ae899e859e8d9e83959a25c8ae895d007fa06fa3e774cfb01fa21fa11fa31fa09fa1bf4b7e8efd0dfb3fbab7f40ff88fe09fd331b8dfd0bfa57f46fe8dfd17fb0dfac9fa2ff44ff85fe1bfd0ffa5ff47fe853b615f4abe8d7d0afa3df40bf897e0bfd36fa944917fd1efa7df407e80fd11f65d0437f8cfe04fd29fa33f4e7e82f583efb4bf457e8d38d72e8e7b35440df44df429f8ae7a0efa25f44bf8401303030f03108300831883088314830d860b0c56087c11e830306470c4e593a6370c1e08ac10d833b060f0c520c9e18bc307863f0c1e08bc12f4b650c2a185431a86150c7a0814113831606ed2c7530e862d0c3a08fc100832106230c3c0cc6184c30986230c3608ec1028365965618ac31c86190c7a0c04a7260b29e1c5818d81838fadcc5a08841294b130c81a181a18f618061c871aac388035687318609861b0cb718ee30dc6378c0f088e109c33386170caf18de30bc63f8c030c5f089e10bc337861f1e121c7e31fc6158e65edfb0826195636886350ceb1836306c62d8e2e1b5215dd9c1b08b610fc33e8603ae2dc32186230c3d0e46198e319cb0d1389cb28978b3319c6138c77081e192d3938abdc2900a9cc3308f6101437a048bc3c48674b183a14bff625d74f4d8521b16312c6104eefb8d0c0e6c1ef9180518853c78308af84a32e24631ff769460b4c91c2a3f3d82af62beab186d79c46fb4c3688fd181f3a1c28c8e189d30ca62304794ae18dd30ba63f4c088f27c723cc0e8c506e628f38e8e3ed95c08bacb17a31f4654980a4694790d23e51d1d35306a62d4c2a8cde5e678d63a0f12f2719fa51b8f5d8e3a187531ea61d4c768c0fdced1304b23b657471e46638c26184d31a212cc315a6044c55d61c4631518e530ca73582099a9d4bb1d15784c6f64b2ab666461646344ef82de5791e35d4625788067c0f3598b7a01bc105e042f8697c0dbc0dbb247d4db71d7c2dbc33b64d71fe19d389edb3bc3bb70a89a77e53140ef06ef0eef019e30427fbee0bde17de07de1fde095e155e055e1d5e0d5e135e035e1b5e0b5e175e075e1f5e0f5e10de00de18de079f0c61c68e24de04de1cde0cde12de02de1ad323886b78697839787479799f0a8a8363c87a3a53d2a5e115e0963606ce8e4631c601c621c611c737ce9386107c978c3e1a6e32dc63b8cf7181fb274c4982e38637cc1f88af10de33bc68fec24c5f889f10be3777642fffd60fcc5f8877119e30ac6558c6b18d7316e60dcc4b885711be30ec65d8c7b18f7311e603cc47884b187f118e309c6538c6718533116182f315e61bcc63887711ee302c626c616c636c60ec62ec6458c4b98001303131f93009310930893189304930d265b4c7698ec3139607264f19b9c303973e8f6e482c9953d8d931b26774c1e98a4983c317961f2c6e4c3b1a37b9f8d35eaf87090f7851d36e92c0b316df1b0c4e48bc90f9332f77426952ccc83ee526547e0aecbfffd3e79c891aae8a4c67da8491d93062bc649932b6742776c710feb9d728ccea4cda3237447faf396e7306efaf96ec5394c3a1eddf79ec384b2ed61426480c9109311261e26634c266c484ee846331eb1e361f32b3fd1971e768ec9029325262b4cd698a8a9a1933c26054c4c4c2c1ec83ff93c97626263e260e272ccdba4c883f8c712bb6dee31b70df7308bf179b0ed3b29610a6e7ba61caf80a98f69c0be2989665866c1b82b1ecaa7d24f437e2a3277a97cd3888ffb4f76a5c9a59cc63cfcca9ee82ad221c7284d134c373c3a744f31dd62bac3748fe901d323a6274ccfb81ebd34c73116d30b07464d29931ba6774ca93cf413cae185e91bd30f0f704dbf98fe302d7330e4b48269958dde690dd33aa60d4c9b98b630a5ac3a987631ed61dae7f8c9e900d321a6234c3d4cc7984e30a5c2cf309d63bac07489e90ad335a6394cf39816303531b530b5317538f2eb4cff72312d625ac20c981998f9980598851c08be25126116639660b6c16c8bd90eb33d6607cc8e989d303bb3de9e5d30bb6276c3ec8ed983d5d72cc5ec89d90bb337661fccbe98fd302bb348472bcc2a985531ab6156c7ac815913b316666dcc3a987531ebb15d30eb6336c06c88d908330fb3316613cca698cd309b63b6c06c891965b5c68c2ece6356c08c4a62616663e660e66256c4ac8439303730f7310f300f318f308f314f30df60bec57c87f91ef303e647cc4f989f31bf642757cc6f98df317f609e62fec4fc85f99b3d31d49aef061cb632ff60fec5fc877919f30ae655cc6b98d7316f60dee471c5798b0799e76dcc3b987731ef614ebf1a603ec47c84b9c7a311f331e613cca798cf309f63bec07c89f90af335e639ccf3ec56a14a352f606e626eb1bb31a192d8983b3ccc469a79ee625ee4a84caacc64e85ee85f2576de2e80858185cf66f022c022c422c222c622c162c3a3d6d4d5bc385c93df5d16daca158b2db7358b1d0f462cf6581cb0386642b8386171e6cee2fea8260d722792236e46585cd8fca5440a7a71651f2929e2dd108b1b16771e03bd34b885583c38822198619162f1c4e28505c10f165f2c7e5850712bec95e200bc4a36edf0cee31f8b2a0bedbe81450d8b3a876ed7ab58d09f4d2c5a58b4b1e860d1c5828adbc76280c5909bc00595cac362cc255c4cd8f4bfbb584cb1a0bbcfb15860b1c46285c51a8b1c16792c0a58983c6cb5b0b0b0b170b2443f296251c212581afcafa58f65806588658465cc7376f8cf04cb0d07702d33e7cd72c7f2b3dc6379e02908cb2396272ccf585eb0bc6279e3443a8db59c81e59de72e2d1f58a6583ef9c4b058232d5f58beb1fc60f9c592fe2c6359c1b28a650dcb3a960d2c9b58b6b06c63d9c1b28b650fcb3e96032c875852a93c2cc7584eb09c6239c3926e41255962b9c2728d650ecb3c96052c4d2ce9a16c2c1d2c5d2c8b5896b0025606563e56015621561156315609561bacb658edb0da6375c0ea88d509ab335617acae58ddb0ba63f5c02ac5ea89d58b1b85d51bab0f565fac7e5895f9615715acaa3cd16955c3aa8e5503ab26562d9e96b56a63d5c1aa8b550fab3ebf81d500ab215623ace8fa315613ee56aca658cdb09a6771d594c902ab25562bacd658e5b0ca6355e031c615ddddcaa62fd20f6d762eac1cac5cac8a5895b0066bb0b5c11326ee15ee7dad7d8ef45a075887584758c72c45eb8475dae58af586edb2b4c3c7538ff5c137e5eedf7acbd1263cd1e7c9e310eb1d8f01861376e96fda5ebae021ee3ddd688ff5817b927b920e9fbb7ceb23d6a76ccca3ce03666b4a17ce76f3e2b1f1ef0c6bbae90deb3bd60fac53ac9f9910ae5fec175dbf79dad56ec211a8548edb9a8decf507eb2f0fad842deee6be133e27ed4c8a9bdb067ab62c749d6eb32e635dc1bacacdc3ba86751deb060fc1f30c407a862cb093336c614d45ef60ddc5bac78148eb3e35455ebacc22450f1ccebb2aa979baecd1097cf64493a25bd39543ace95e1ed6f46627584fb19e7126013dcf1ceb05d64bac5758afb1a63be6b12ef004c2b589b58535bd77274b2e8f2faf8bdce1dcf9589790037206723e72017221721172317209721be4b6c8ed38d039b747eec0a15e8f4e36137781dc11b9137267e42ec85d91bb217747eec17dad5c8adc13b917726f9e9e9bfb20f745ee875c19b90a7255e46ac8d5916b20d744aec5f1e2b936721de4bac8f590ebb3b33437406e88dc08390fb9317213e4a81833e4e6c8d1dd97c8ad905b67e30a0fe4b2d9c01fba511eb902fb6673745fcad646ce41cecdac5cba57312b6a0979206f20ef231f703f301ff2787d3e423e463e417e833cfdb9e3e8edfc1ef9036be9fc11f913f267e42fc85fd94396bf217f47fe817c8afc13f917f26fe43fc87f91ff215f46be827c1579bab28e7c03f926f22de4dbc87790ef72645cbe877c1ff901f2434e672bab334d9ed7caf5f8cad1dbf911078bb5e8d61ef263e427c84f919f213f477e81fc92e530bf427e8d7c0ef93cf747f205ee16b6a6d4f940de44de429eae719077b99692d59aa7e72a712c4b016ce8d6b2282b325f0b465663db28f86ca616020eca2984284428c428241c7459d8f05cd6c296c35ca96355d8a1b047e180c211f7320a2714ce285c50b8f2af2a747e632b8f84b94cd9de5178f0404b2145e189c20b85b716c2c287dd3ba4490b5f6e3f0a3f4e3c5249c5a27ce95845a1c61394d63c533e6b42be38f6789a1299e0745ea8f378e066830215bd49273c9d84ba7f855656fa76363678e54993ad652681cfac63dae148bf3d5dd661ef16c96da18b428f83b00b7d14061c115a18f2b4dd3b3d36bd0e2ffbd71885090a5314a8a8731416282c5158a1b0462187421e85020a74bd85828d82c313bd6bf4682e0a45144a3001d380e973ccf735e23838338019b2dcd63b30238ef437639809cc0dcc2d4cfa730ff300f3c8e1cee609e619e605e695e3bcae36cc1b0f6aa787ec9c727b6453991e1c90495d413385f984f9ca4ede1c566e7e607e61fe7896ba5986598159e5e02fb3c6a161a44ccc3acc06cc26cc16cc36cc0e7b95cc2ecc1ecc3ecc01cc214cfaad07937e3581398539833987b980b984b98249d7e760e66116609a592654122a9e03d385598459820558062c1f56c07ac90a6145b0625809ac0dac2dac1dac3dac03ac23ac13ac33ac0bac2bac1bac3bac07ac14d613d60bd61bd607d69727135a3f9e7a6f956155605561d560d5f91656035613560b561b56075617560f561fd6800df51d789294358435e2e84d8b7218c39ac09ac29af130e091b29ac35ac05ac25a71ffdf5ab3c05c0d8e341e523e391e6fb0f2b00ab04c9e476e59b06c580e4fa9a32b2b7453378b55a6bb1461956003b64186182b00db871d7044a41dc28e60c75c33ed84a34da8d6f1e49e37ec0dec6d3667b50d7b475a81a7e3edb278327bcf91d96451ee2fb00f6cd0da479e0f699f609fb9fb4afdb2609bc51ee4d82e4d0b3c4c6fd3c557d8b74c08ed3b271ea93ff3fc0b8e1d6df0547a9e505f655fedfdc5ddbf9d07fb9119eb07d687fb073f9ec41fdc79689eccc8ef0876cae3846464b62a3c5d90da036a7eec2787235029ed176c7a9e0fec2f3b78ec1fec32cfb6a4b6caae703c845d855d63ad4daa9c34fea8c0edcdbb079b1e296b9c92273fb34d3d69222dd8f4463a6c6a6fe82e5d6e0e6dba98721ec01ec2a6f250b1c7b0e9bd4c79e6259d1c4f1cb9bfa7cb66b0e71ce8780c602f38b4caa6efb182bd869d630bd6ceb30f907328c036615b6c8eda545407b60bbbc8ae1abbc4b2e7008ec173261c1f4e00278413c189f95f3c6561cce74e0267c3f1bece16ce0ece1ece01ce919d8a7423e704e70ce702e7ca6ad6b971ffd0a19c1f7052384f382f386f381f385f383f38651603a702a70aa706a70ea7c1e6b1d384d382d386d381d385d383d3873380338433625fb1e3c1a1f24ce04ce1cce0cce12ce0105fc159c3c9c1a1c214e098702c38361c078e0b878a5d820bb8065c1f6e0037841bc18de126703770b7707770f7700f708f704f70cf702f70af706f70ef701f7053b84fb82fb86fee0f53f7d8fdc0fdb257fcd6e52813f707b70cb702b70ab786ffc87aaf1d577a9e0bf3629e73a19ded43e79cb3cf54e59c73bafa59f47ebfc13f9846c17097554a25928b1449a58a2e55b29dbd54d96c2dddc0eceda98a455da4aa2e553397b7ac0ad75daae1524d976a9926962dbb54dba53a2ed575a95f70cf67ec527d971a1825349e661749a986914b8d8d3c4e17f3914c4d5c6aea5233f3bd966a163e2c82514498fa333f9054c422f252510b062a3ecd233a15b355bd3cd8f65e2a6e2117a9849961a5ef9d9ea6a0ad341b49974a99af8f90ada4d146834abb54c61c3cd3cea57169ff3f227c48c29efef977ff7cbd2d918ced7b045d735bb30d93ae49485d5208d3814b87e65fd79e999d6a7972fb9fc7f63f86610942566accbc5d8586d373975efc7cdea3bf07972ebd72e9b54b6f5c7a6b3d4eef8c695da62ead9a0f2e7db4a8dcd38f20b713973e99abc4b5654a63fa97d720adeb6ad23f7d73e9bb4b3f5cfae9d22fb3c1a6df2efdf995fc9ac04c674dd649f47d548f6a6b990156fa80ba97ce1bccc86dcd157819bab41e299a0bf859f352326a4f975dbae2d2554bdc90aeb974dda51b2edd74e9964bb75dbaf30bc0d3cdae4bf72c93c54ecdf5cdd372ad010e8c2c170fb36abe07bf0085ed4f36feb9f4d0a555e1d8a5d5257578e6d2ba1971694d4ecca5e32e9d70e9a409a574caa5d32e9d7119e732b88c7799c065429799bbccc28c9c924599a589b5cccad4bfccda65362eb3b51d85ccce65f62e737099a3cbe8dfb3cb5c5c46e56f2e73779987cb3c5de6e5326f97f9b8ccd765b22e937399bccb145ca6e832259729bb4cc565aa2e537399bacb345ca6e9322d9769bb4cc765ba2ed37399becb0c5c66689bf59991cb8c5d66e232539799b9cc9fcb445c26ea32ea58dc65122e933483732665d8389376998c65ffb1806bffbb02f3ad394a2e87586e92056e29cac1723facb178fe2d166dbd779f2ce6f86c3187587895e4e0e5e7157ac3c2461e2254dcd3a82ed5c67cc0df2efc623eacfad4b3923e795b03d2aa1a035cc15669e28179b2942ca6a7d3b62043d1d22ac095cd45ac3f705901f70aaefa5f849085f34be3ace1ea5ad8b88699242efad2c436b8da92ccb88eb9988b3ab216498c04f76061b2742b6532f1b386ab8c3a20263412d5fe478466fccc49c808144fdc6a617b1216f67ffc9940e6bff8e2ff11a124a119579e26032df7ceddc29fb5a643d528ad7f661c62b032338c98cab6f22f5249221153bc341e352138181791636840e2236546e1edd8d8955087a4dfe161b6a97b5d34eafaa7dfb31993b4a799de9ba991411afbf31040683236b898cc0cae3087052c4d919016fad9c2cadc4a85872d47c804d6964d48e07e2982c9c3065466077b38c0d17c055f653881707c1b2ea06a6f70b7f8d48dd6ce039ee6d09ceec2cb92590876def7bae06d592dd0fd8f397f59ec6ccfee880ef5995dc1d7f645d651c8420ed481825be87b114aa076b5fad59c08a009126505a883909bfaa33b2d307b06745c4b1d53433de8c30086308231688c53b07d51d015013517833824200929d00466244b2d099666d17bbca4f7071fe2e7f8859b34f14bfc0abfc66ff05bfc0ebfc71ff047fcc94d55f22cf18bbfe0aff81bfe8e7fe09f78295d27fc1baf0abf267bd349bcdacae1f3f8821b2df07ab0e4166d7cd91cdcde517c055fc5d7f075f3fcdc975c539568e0637cd3e49e74b097bad1c2b72d4dd12cc077f05d7c0fdffffd34c00f9dd4703f32d5eba45ee9d9097e8a9fe1fff0d22df545d84a35c7f071f3423968a409d7d56093b68c2d885e4fa55ca861a6dd4b5dcd981ea8b534087f09c3b4e8bc183d41f04becd0745a9f41e8ce1b8239c182608979961790d4d5820d747f6b711efd93f0dd2f17d9c0028fb62b821dc19ee0f07f88f05f5e99a0fccbbdb1fb478488ccfe8532fd23c27f014dd9817105db7bb8bafed044f3ea46702290261d43f49053c357829b0b8580e34615c1fd67ed15954e081e4882170fa812ad8ae06dbb88a290e8ba7b8f107c08beb64521797889fcb0ee80204b90b3b8c44383204fa01116094a0465828aed7c3cbcd1e1e722d14f5025d0bb1cfc1299dc11aede3c4487040db7f458e0e6d09ce60a4f51a06dad7ed493966ba8ddb6eba8fe0e4197a047d0c79a1e5ab8d6256961e0f38060e42e6f82b1f3ea83c63235f293d6971e11cc8cc68c087be6ef66e9640666f358ffcbadd635c19856857f56ec9c20d060a3b697b8781268dee204ba992448212e13642c3b5c2826e30903c290704eb8205c12ae08d7841bc22de18e704f78408b263c119e092f8457c29b6b7d08ef840fc227e18bf04da83b5fc22c618e308fc6d5d32345c2126199b0425825ac11d6091b844dc216619bb043d825ec11f6090788db8623c231e184704a3823fc238c104609638471c20452e39bfa374598fe9511621fff32dd89497a348d9284f31069c87bfdbb70f70bf325f3957ba799af996f5c61c57ceba609e63be67be607e647e627d39fdb4de667e6175739db3bddf6995f994b18866eeebaf728f33bf387f82cf3a73ba9fe17f3b72babd887f9d72280976a22cb3cc73ccf5c2f77c4bcc8bcc4bc6cb1511624b4605e315d6ca39b555b9c8faf08dbaa3dcf990b064798d799375c53bd527f5accdbcc3bccbbcc7bccd5d6c028d336dbd4b72173493c89c1be847af73e9230ebde35039a9309f3e9bf94879b7f21854684093d33b3fdfaeb2fb9d3a76970d422acde96af4a605a02f0ae56ff2cd6cb04e0c05233e953cbb498b590a586a64f5dd45cc498c7996b1e9336d48d069c326138d744672c4edf02a2c518b2963b6469e28b85fb49c29779d02e3f26d2169e45e03653165a8fa2b13f1673160b162abc62b176d1908550788ec5d68d7a2c76624e4217668fb9c6b1d4637f2ea75f0f2cec2db2d075b68835894af3c77f19bf585c58e8f3c6e26e42be7875cb17829462a8b3338ba78bde59e8ce9b857af575f30a125f8b9c996a844225071679160516c51f110a97aa27a5ffa5361cb228ff3ef5549545cd8dd4f93a8b068b268b168bb68bacb14d11298463165d167abccf62c0424f8d58e8e684852661c64233106111651163a10126582459a458a459642c93e35213e959062c43967334b1cb25cb155a79cb0dcb2dcb1dcb3dcb03cb23cb13cb33cb8b11e1f2caf2c6f2cef2c1f2896660f9466f61f965996599639967596059645962a9f215965596359675960d964d962d966d961d965d963d967d9603d79cb21cb21cb11cb39cb8883a36fdb93ae99119cb3f96119651938a4b61287d8fb34cb04c9a885ba6b064102a99919625d9f2cbd7c5cf593fc7cab3127cbbb20a59cd592d0ce69c0ed8765acec211f7fa69c56a6d68b4d866b5c1328f352c50a39066b5930ec26acf4a8f1c599db0c8bb91c5061555f2e234c6d5d558d8eae6ae235677560f564f7305111758bd58bd597d587d5965dd23cf4a5dd2678155915589951eaf1811aeaabfc8e08eb31a6aee75635567d57033fddb64d5fa5f14c53f0ad45c1c963f47991d2ba15b35dff92feef8270959756d5770fb43c60755d163d5d7652eda69756be0ce0b562aa61e8f594d584d595976d09f6db7e84e2affc72ac22aca2ae61a2fc9555671f3b3914e28947ae8189f18696a12ac9248770f0f92ccac34651983289b9ce6d9b5e3ac35e1de42b0fb47c318eb807568265989e2f59cf502a9675fdd5cb15eb3deb0deb2deb1deb33e58f4f021cbfa68194d3a7133764552b61734086d7b468ca6ad0a4faccfac85b512acafee50b3008efde93f22f439b713a87bb3be999d7da29277377cff8f08bbac1fac9fac5fac55e6a34ff7a77abe520b59ab698dc2721cb12eb02eb22eb12eb3ae38f1cb7595758d759d75837593758b759b7587b5eaecb1eeb31eb056252a39663d613d65addefeb18e2094bbd6f035396a2bc93ac53acd3ae33a43cbae2adcbdf16c0236219b399b059b259b159b359b0d9b2d9b1d1bbd85269b039b239b139b339b0b9b2b9b1bc2149b079ba79b4cd9bcd8bcd97c5caccce6cb268bde8b98eca6e0baaaa4e8f66aa5c4a66c561c23c22a9b0a1b7dd6d8d45dbecba6c1460db52c2c23a9b6da6c3aee22f8aaa7f46b8f4dff6723519981b94747126805ced4ee08c1c5cd84cdd455d4ab199b3f36113651f7513f63c6c4d3aadcf29db249b87c9f4d924dca98f526cd26635966b7b60da6e5cad65bb28c4ec46cf2af3fb68151bb90c836643befde1f6c2dcb11dba513d908406ed76c376cb716adba5b88d5b3ddb1ddb33db03db23db13db3bd20c2dbded8ded93edcd5b37d3ac10dcb7586696d96f14c8f7cd87ed966ff4784573523354f0dfcf231fe8b275c6e4c27d4c885455f65dbae183873b7139fb84c0cb56ff554c152bc088d6cd546896d996d856d152181ad3addb0a876e96ec2cdb993391c74d4440b0b993f3881c96dc7dd276cbbe614bb5cbb6bdbe0b23877a08ef610ccd86a9af47d646e03c6b7f47dcc568f4cd9ced8aadd8845459dde96f74e74b595c279339eb78db18d9bbab34db04d9ae7caaac35694a6bea5d96604d3d939977e4b1934feb4f3bf2b904ac02e6437ffa191229ae85394dd92ddcafc21234fb777ecd69698ec3d3231b8b81bb0dcfdb6da7a73765be74fec76bf6b6f71eb3d3d7b607764a7fb6776177657f7dab3bbb153bb0f764f762f76eac987dd975d965d8e5d9e5d819d3a506257665761576557635767d760d764d762d766d761a70ef7d8f5d90dd80dd98ddc49fd1fb39bb09bb29bb1fb736dd51c71ad0d3bad54dd89b18bb34bb881ca241137d9a5d9652cfbb1b45ae14391d03e643f67bf60bf74657daed8afd96fd86fd9efd8efd91fd81f1157da9fd95f90ccd9dfd8df7f5f1eec9fec5fecdfa697ee3fecbfecb3487689efec85173aec8bec4beccbec2bbfcf2afb9a0946fb5267df602f05588db6d89b7f29fb8ee91a09dde9feeef7d84b13569706ec87065e7e6181b8bce19afd88bd38a9ba31613f653f3313f76c6ebb6ee39e79749dd4ff3f5320e329db1e14079112b48fb08fba688d7d8c7dbc7b17f8d4b812eea16949b24fb9cf927ddaa0e63ec3c159e0f52d6d668483e710700839cc392c382c39ac38ac396cdcf8cb61eb84de0fa2d5973a6c887419e5b0e770e07074e90787d3ffd25bfc725bfc47842679daff11a179b2b67f28b92222e470fe65dd99b853dc2c4b859b3b54395c385c39dc906c3ca8d227c2788737870f077522cb2187d8c962c3a1c0a1c8c1eab7cac3870df550e650c1eaa9b9e0cca1ee023ddb401af3a1c5a18d89c7ad14360e5d0e3d732fea6a00ea9206337039b53ec4a9fcc83df4e0d8e5f4e0c4beafd4f4d402eabb6a65665ea9527b0e7f1c22c64d84bb0eaa21c6216e5ac121c1e19716211be790729d2387b43b692c1933081d1d47387a1769730c9cd6ae64e0f1c431340df02d25f0c871ce71f15f62b567ce36caf52909b938babf0bc7a505ef55ab1c57ae58e7b8e6b8e1b8e5b8e3b8e778e0a81a54e199a30a5f39de38de5d5cff3e383e39be38be0d0f1f3f1cbf4687c72cc71cc7bc6b0e381638163996dc54dfcb1c2b1cd5508da31a6a7054e116c7b6698cc78e8bf4397639f638f6dd477dd623438e238e638e138e538e1aef1fc708c728c718c738c704c724c714c734c78ce5283f81d4ad538068fe34e7b4e0b4e4b432823fad396d4c6e9fb69c76bfcf3d4289a723a713a733a70ba72ba71ba73ba7079a64c9f3d3cb3d2b889ff6c69c54c99753166919a7bcb19e538153d125d39c4a9cca2eab6a2b6eaaef554e5a366ab16e80e8d4b0943312d13ec6a9c9a9c5a9eddebadfb1dd8b4a9753d7b5a56f3710e499b50c6b9cfa9c069c869c46c6704f6a7dc249fdd1537f041a63c47219173b2ea192c21a2b4bd39ed324a88938a704a724a71427f52d63a9dbcf70f69c03ce215220850d4f03db20289e394ba22e38af38af396f386fa510bac6f03f9df097dee2f04b73b8b16cc49606bcfb9f2fb93e375d8be56da77fc9aa6e6613be246d0f50c338ef38ef391f381f399f5c696e72f9acf62e9caf9c6f9cef9c1f9c9f165152bcb84fdb82f1033df8fa25867a9805e9fc36a177feb89cfafde59ce59ce39c77e917e78213919f8b9c4b9ccb66ba8c2e8c60ce15cea2db81d15ed87363fd5be35ce7dc703775a6c9594db43977387739f738f72dbd6cffc3590d0d5d439335e23ce63cc1b55c4acf4e39cf38ff718e9829e81ce51cb380d151c3f67cc6ea40dcbdff8c54a4c69c1396ab732df578675696c9897392738a73da0dd5f38ca97f17c9cc3d174c185e3c97804bc865ce65c1656919aba4dd5d565cd65c365cb65c765cf6ae3ae072e072e472e272e672e172e572e372e7f2e0f2e4f2e2f246c8edf2e592e592e392e752e052e4527213b555e6a2d6ab5c6a5cea464b9706972697169736970e972e971e973e17b535440ce832e632e132e532e3a25ee991289718973897049724971452e0e36a2e6387035c4128eb1a700db9ceb92edce8c875c975c575ed3e1fae1bae5bae3bae7bae07aefaf5c4f5ccf5c255e467bbfca69a5e4587371757c93bd707d7a76b96b8be6c4b3632e4fae6fa71597d7eb9665d29c2358750dbb5c0b5c85525cb5c2b5cabd2d9a4b9b9d99a6bcd7c3b7375aeba1aee3de1da94e8e0dae2dae6dae1da3505afa8ca7b6e5531a3f8b5efc453ae03dbb6cd7d5c439dd1af23ae63ae7a7cea621ad78ceb9f1380bfaa0f513b3de176e31ae31ae79ae09ae49ae29ae69ab163136e70f3dc026e21b739b785451e9f8feedee0b6e4b632973a81d8dbda8df7dc36fff398d9eefe97723be2ec8c87fd2f1d7ffb3f0afc6579326168e1bc2b83a3abc32f71e8d3d0e34a556f6db9df54af2a3d703b723b713b73bb70bba2eedeeedc1edc2ce4979baeb7bb16dd72cbedc3edcb2dcb2dc72dcfadc0ed774880604550b37d825b899b3e2bdcaadc6adceadcd45c935b8b9b5aec58b2c7ebd08d3adcbadc7adcfadc06dc86dc46dcc6dc26dca6dc664687b73fb3cddc226ef0e616e516e31637cbde2dc12dc92d85a0c52dc3ddb94e13f767a91913989e307f98ff6e3ce2b25fee5866b18d6d3173f796eb61e8b90790b7f77d0fb9cff569011391a6bebb4492fb82fbf277ad9cf8bde0c67d8304bba8f7bee77ee07ee47ee27ee67ee17e45d0e07e47dac8fd8914e6bb1afa20b9dd545559ee39ee79ee05b3afdc8b46abf712f732f70af72af71a521392faa9c1bde92601f716f736f70ef72ef71ef73ef70142f5f711f731d205eeead28cfb1ff708f728f718f738f704e829b598e29ee69eb1c3381ef0f03c021e218f398f058f258f158f358f0d8f2d8f1d8f3dd2b70b7d1e471e271e671e171e571e37ebf6e3cee3c1e3691ae6e3e592438456a4e53e3e3cbe3cb23829f9311e391e7927ddf551e0513425ea51e251e651e151e551e351e7d1e0d1e4d1e2d1e621d99ee2d1b520bb59e024881e3d33f33c54f3c0f2ac3f073c86961331e8f118b98f0a8c794c784c79cc5c34cae38f478487bea8f5388f048f240f2155dd4ff3c8f074aedee689256b0c533c3dcf8067c873ce73c173c973c573cd73c373cb73c773cff3c0f3e824819e27d7510d679e179e5737385a1f2a039e379e77d1e1ff4b84522873a62c3e1f3f8fbb9d49c27f703494105b59021cdb3a3bfe726c5fddea88ab191d3ef6ee3ae5f9e4f9e2f9e6f9e1f9e599e599e399e759e05934527c967896cd4c6a7221efc6faa9d2d5ca7b8a876930359e759e0d9e4d9e2d9e1a6dc7cdf46f97678f671f4de273e886099e2317abf01cf39cf054bb3356339e7f3c233ca33c633ce33c552cc9533395e699b1135c84d185c65f9e57c02be435e7b5e0b5b465f75af15af3daf0daf2da2155ed75e0753422fc3cdcbe631eb7e7ddcfe5b7ecc6aae4c4ebccebc2ebcaebc6ebceebc1ebc9ebe522faf2e6f5b1ef8314af2fafec7f4759ec9d9dbe32712ea9ca73bcf2bc0abc8abc4a68565f155e555e355e755e0d5e4d5e2d5e6d5e1d5e5d5e3d5e7d5e035e435e235e635e135e535e33c4205e11d7d483515e315e7104bcab1a4292973a90e695b1d36bdef0f6bc03d79bf00e79cf792f782f5deb0b2ab0e2bde6bde1bde5bde3bde77de07d74c92fef13ef33ef0bef2bef1bef3bef07ef27ef17ef37ef0f6f95c9f2cef1cef32e9839e45d347dfe5de25de65d3171faaef2aef1aebb588c77c3d8c4bb69b32d31f86eb169f16ef3eef0eef2366f6c4939e72bbcfbbc07bc55d5c8b5f5e018f1b8f714014b81917784b76ad6fd38ef841975dee2171b17686829de69b388bc334eb8e6e3f8e054fee3f904a6e1678b7c4204c255ff6781d4bccfca45f5eb9acf86cf96cf8ecfde252a7c0e7c8e7c4e7cce7c2e7cae165577f89a5f74aecce7c6e7cee7c1e7c9e7c5e78de4ffe7cb27cb27c727cfa7c0476d95f8a8b06aabf2a9f1a9f369b8cf904f934fcbe0a8ae4ffb1f1cddfe17392194f8e9fc2f15f1cd8947da3ee3f79782ea6651c6128ca79cb3b605509ba6bf35f7bf967a7cfa7c34303530e233e633e133e5a389f8e3a35988baf9954fccb6c53f71b622f2389f049f249f944baff8a48d0e3f193bef48cbe3ebf906b6a1f40d5d4177e67c177c977c577cd77c377cb77c777cf77c0f7c8f7c4f7ccf7c2f7caf7c6f7cef7c1f7c9f7c5f7cdf7c3f0605bf5fbe59be39be79be05be45cbefb699baa2aa2a99d1f25bb6203769c5b3f77fdb33d1b3d3aa3acf6c73f65b31f34f418d566dd17c6b7ceb7c1b7c9b7c5be603e9ab7cdb7c3be602aa95f455991edfbe453ce5c7e6382e99f61df01d9acaf41df11df39df09d3ad373c64edac577c6f78f6f846fd4c5542066d6976f9caf7e4af24d993cf9a6f966ec48a82c643dd9806c48764e764176497645764d76e3ea5db25ba43b65f7640f648f644f64cf642faeafa7ae646f64ef641f649f645f08f5653f48da67b364736680cde6c916d0aacd96c896c956c856c9d6c8d6c936c836c9b6c8b6c976c8aaad1ed9beebabc080ac9e1d911d939d909d929d91fd231b211b251b432a773641364936e5d6638284f98857d7a68a67d3643376d4550e729e5c402e74fb34b939b905b925b915b935b90db92db91db93db903b923b913f302b933b98b1d1cd4ce91bb92bb91bb5becd82c61ae1a76c0d3839cae27b9971d56d34f9b07e56c48ee4dee43ee4b2e4b2ee7c66a3a4f4e1516c99510c9e52ae4aa1691b8d7bf3597ab22c49b6b906b926b916b93eb90eb92eb91eb931b90539d23726372137253f37cdaabf333727f2e27fca832119384b928b918b97fc7dee512e48c18eccae94adb8ebc283597b16025e981afb505145fe75de12591d0ea6a2782e585c23ceb25f9807c487e4e7e415effaec8afc96fc86fc9efc8efc91fc84b8afe913f913f93bf90bfba6c8afc0d3f76074ffe4efe41fe49fe65bba8f9b725f19780ca7f9c0442fe4b5e3773e4f3e40be48be44be4cb96df3e9c92af90af92af91af936f906f926f916f93d7835df23df27df203f243f7a77e8ec88fc94fc84f9db88674d1fc8cbc3a16211f251f7387362b752ffeeb55c23c84c61a4b927cca05753155f269f2193b0dad00054f21a010529853585058625bccfa69fdbbb371dd22852da2dbc2de4c1985038523057d3953b850b8625989f614ee141e149e145e14de143e14be14b2147214f2140a14544fc99c4e0a650a150a550a350a750a0d0a4d0a2d0a6d3332153a14ba147a081016061486144614c6142614a6146614fe284428a8648c429c4282429242ca0e065ba993690a193be5ad08454f31a018529c535c505c525c515c53dc209e758851dc51dc533c503c523c513c23855fea62f14af146f14ef141f1891490e2db8df4fd43f14b314b3147316f86d07b20f2a358a058a458a258a658a158a558a358779b37c546f7be70d1836d17179b145b14db143b14bb147b14fb1445f02f274557a0546a5e7144714c7142714a7166fb43c53f8a119181399dfddb217ccd2dabad1053314a51a3d09784c53d2cf314936ea69ea428a629669c144e3bed0e4ade3c2e4f5a1239564b4a01a590d29cd28292fe5d515a53da50da52da51da533a503a523a513a53ba50ba52ba511268bab9d8c7f630a5e674f4c883d293d2eb7f44587a53fa582a814de977b0dbd7b6134e593b92eafe33241a11de7fe7556c0ca90ed4b92fa52ca51ca53ca502a522a51225f5bb42a94aa946a94ea941a949a945a94da943a94ba947a94f6940696867e5443f944694c666892a4d284d29cd5c2835ba6bd1c35da4f251fa431a79294a294629fef33048534a18b0292529a5dcb66362709fa1a4fb193b2050c0b9ec29079443ca73ca0bca4bca2bca6b7358296f6c97bcbc75c1c9847050a6bca3bca77ca07c749b0ae513e533e50be52be59bfb7428df293f283f29bf28bf297fcc13aafca59ca59ca39ca75ca05ca45ca2acda544395728d729db21a6a526e516e9b52555655421931ca3dca7dcaeacc90f288f298f284b2ea9c51fea31ca11ca5ac6271ca09ca49ca29ca69ca19b11e3bfeb002154f25a0125299535950595259515953d950d952d951d953395039523951395b5a34a99d950b952b951b95bbf91e551e549e6eab1aecf0022a6f2a1f2a5f2a592a392a792a052a452a252a6ab742a54aa546a54e45cf36a9b4a8b4a974a874a9f4a8f491aa531952195119539998e8ab4ca9a8e63f2a112afa3746254e254125492565294bd631270c594953c9d8b18ed247aa58ba8ae041d5530da886e6a1519d535d505d9aa524dc535d21e129d457dd50dd52dd99b94b50bc7aa07aa47aa27aa67aa17aa57ab3c45fe1c0d67321673ec9a1d6f997ea9daa9a78527d517de3e3543f54bf2ea1b6b254734eca73356fe7a235d44a816ad17c1eab25567aa44cb542b54ab546b54eb541b549553d6f53ed50ed52ed51ed531db8b39e1d521d511d9be9a43a7197c1cf08df72b39a49c2aab4aafb8f08ab33d71853fdb3c10b1a6da2bfadcfc7cf6be4a6de9b5a6844f8db3d1370eaa8d508555511a3aade27a826a9a6a8a6a966ec78cc1ad43cb5805a486d4e6d416d496d456d4d6d436d4b6d476d4fed40ed682ea0b5933994d6ce88be3f0d6a170e1d6a576a375b5b8726b53bb507b527b5979322517b53fb50fb52b304ded472d4f2d40ad48ad44ad4cad42ad4aad46ad4ead454a16a68d9ee65ad4d4d3577cde52fdb356e725ebaf4d63c9876deddf578cf2dd368ee6a7d6a7a73ba337463757e446d4c6de23aba33a536a3f6472d422d4a4d3d8f534b504b524b514b53cbd801a175a87bea01f590fa9cfa82ba3490b2bbc4a8afcc3e595f63c78614a86fec4dd7b7d477d4f7d40fd48fd44fd4cfd42fd4afd46fd4efd41fd49f2ca3d45f06cfea6f97562b1fea5fea59a7e55ecf51573d0517e9522f522f512f53af50afe247d46bd4ebd41bd49bd45bd4dbd43b08b5d6d5933ef501f52175151b539f50576d33ea7fd423d4d5a2fa1ca79ea09ea49ea29ea69eb113501bb8d58686a711d00869cc692c682c69ac68ac69e8a72d8d1d8d3d8d038d238d138d338d8b8bf6685c69dc68dc693ccc18d678d278d178d3f8d0f8ba8e9ecd9a6ed6c8b9872acfd328d028d228b968964699867eaad2a8d1a8bb678946c3498569a89e168d368d0e8d2e0db5d2a73160df34e3627b6a46c1508d0e698cd09a6f4c684ccd0df9d07209d539a3f14723c23c61c9af372a16a511a311b798be6ccf89ad2c34ea048d248d148d348d8c1d03db84a6a719d00c69ce692e682e69ae68ae9da06c7343734b73c7bc612cb869864f9a0723c2e6f13f49e83642bda95ff29c889b7d689e685afcae399445371631b57cff8ecb10fe493acd45f342f34af346f34ef361b9d25e499a4f9a2f9a6f9aaae14b334b3347334f53e58bee51a459a259a6a93e5569d62cc4e334a1593726d46c384dd6aa45b36934d96c99f75cb34db343b34bb347b34f73c0234573487344734c53cf4e69ce68fed18cd0d478343c5595a0a9cea8649a66c60ec96d41cbd30a6885ee35a335a7b5a0b5345ff0a520ca4614e8ae155a2b5a6b5a1b5a5b5a3b5a7b5a07273cd63ad23ad13ad3bad0bad2bad1bad37ad07ad27ad17ad3fa189e6c7d696569e568e5691568152d1c4ed86c1fa325bed0a355a625d5e2e3fa115a555a355a755a0d5a4d5a2d5a6dd757551d76fab54b4be5fbb406b486b446b4c6b426b4a6b4d4ff3f5a1173d16a4569a9f238ad844be99124ad14ad34ad0c6d0d6d471bdade12f209cdb603da21edb919c7dbc278bab3a4bda2bda6bd715b4f7b4b5b3fed691f681f699f689f695f685f69df68df693f683f69bf68bf697f687f696791f6d5ced32ed0568525da65da15da55dab5dff15e768433ed3aed06ed26ed16edb6f960b43bb623d5eed2eefd0b9da3ad9b039bf955d300517b8870594a9f52abe2b427887eda33da7f68894aecb4a3b4632ea10af56b8276d2cd37b453b4d3b4337626b2b9b87b3a019d90ce9cce82ce92ce0ae140917767eb045c3b3b0bade8ece91c2c0fefec65ee969d239d139d339d8b5b85923a74aeba7e991c84743ea616766e74ee741e749e745e74de743e74be74545b8e4e9e4e0131eb4ec90dd45c998e2aa9d2a999d75ba76e11bda133382a6126ee73f8e7c0dd57bf5b74da743a74ba747a74fabf03349e6617157faae6bbf73d76c2869d8f6d4c653f740db537a033a43372b3241d8d6a42674a6746e78f8ec613a513a313a7933095b7a33229739beea4e964ece8e82e743ddd806e48776e78afbbf85d4bba2bb3f52f5666e2ebaee96ee86ee9eee8eee91ee81ee99ee89ee95ee85ee9de8c08edc8ef07dda7915ff745f74df743f74b374b3747374fb78094b46e896e996e856e956e8d6e9d6e836ed365ba745b74db743b74f5bd279c2005d576578587bb7dba03034bdd21dd9109e7ee98eec44072774a7766c1ca2fd5ff6756b5aeca472d8d4f7a65de495d9589d34dd04dd24dd155f732766c760f7a9e5e402fa437a7b7a0b7a4b7a2b7a6b7a1b7a5b7a3b7a777a077a477a277a677a1773516b9acd0bbd1bbbb54d65679ef41ef699178099177cc493ef45ef4de76126551d8a140ef97e4eb53b5a0f5c3dc9d46f4bef4b2f472f4f24eebaca732457a257a657aaabc4a4f95a8e606bd26bd16bd36bd0ebd2ebd1ebd3ebd01bd213dd533a637a137a537a3f7472f422f4a2fe6fafa8cd34bd04bbae8935e8a5e9a5ec60e0b3755b661b11f7aef7d4f3fa01fbaed96fedce8b0bfa0bf14673113407f457f6d74d8dfd0d74f3bfa7bfa07fa477348e89fe89fe95fecf82d914dff4aff46ff4eff41ff49ff85f85aff43ff4b3f4b3f677ecb6769400f3bfe4852a4affa0bf48bf44bf4cbf45561957e8d7e9dbebad77407e8b7e84b08a5e8775ce7d0bdab7cd77c45fa3dfa7dfa03fa43fa23fa63fa13fa53fa33f312e9ff2156ab19e8c7e8ff6244fa09fa49faaa274d3f63150e9cb9cb0cf82f9e9081778f05838041c860ce40df972e2ebd452f6cc360f5bba99fd60c362ede65b0659b372f96c18e77d149b71eec191c181c199c189c195c185c19dc18dc19a8a52783178337830f83af1924571b0b1819646d810e7236f5a240c1514191419e812a2c3228312833a830a8da5ecda0e6bc6aa8336830689a97d3a0658eb68336830e03f5aac7a0cf60c060c860c440f54f184c19cc18fc31883088ba957e558b7106090649069a8b3483cc3f2f6b869e6160af7f18329cdb25bd79b860b86428547cb46bb866b861b875a1beec080e66401be40ccc0cf7f68e870713e943953c319f3294083dfc087562fbbf43718da839b20faf0c6f0cef0c1f0c9f0cd5d09ba11eff32cc32cc31cc332c302c322c3154e515865586358675860d630dc3264335d466d861d865d8b33869d1e4b06fa7524a371b0eece89fc29bedd812ce8a3225fd86ba3f3255300c4ca31baa4b1386eae78ce11fc308c328c318c33843cd4392a1ba9a669861e42c526ae419058c424673460b464b462b466b461b465bcb94199e19edec88f683b3bc12d7ac3ba8f09ed181d191d189910a5c9c80e2e8cae8c62ac5e86e66b0d183d193d14b379da5987b9bc9647677fd3f737416ff1a7d187d196519e518e519151815199518951955185519d518d51935183519b518b51975187519a9ad3ea301a321a39141aad198d1c4acafa329a319a33f46114651463146714609466a2ee5721a6fda8dd5f30c636782e77372a988858b8d3de38071c878ce78811496f1caf2c7afde96c6e274330819e4cd0a3a5e33de30de32de9973f978ff0badd0bffa7e647c627cfecf819bf185f195f18df19df1c3c0c074ec726ae969c7682fd58997694ae26d737d7f4b73ebded5f0c75de204ae2b2d765e333933fe32ce32ce31ce332e30d69d12e332e30a96b8e9c5b8667bfd9fb6ed9c8a08853fb5bec775c60dc64dc62dc66dc61dc65dc63dc67dc603c643c623c663c613c653538ac633b7d0bf7ae5ea6dc47cbba50f8c753fc638ce38c138c938c538cd38c3c45984ddc4330998844ce64c16cecf982c99ac98ac996c9868012598ec98ec991c8c014f8e4c4e4cced64389ebc985c9153766228de223b9275d85c99dc9c3f496c993c9cb7678276fb32189ea261f4bed3a7b181d8a50e763db179a88252f9c14924996891ecf332930294a2371cb3f2625266526aaadcaa4c6a4cea4c1a4c9440fb69974987499f45c34c5a4cf64c044f58c8c0827ea953048cb94f6c9d46cfac391e1ab6cc650fd442355e5112651974d33899948945e34893ba940138d5add569dfa29c3d45928e2d4330d98864ce74c174c974c574cd74c374ea07dba65ba63ba677a607aecde4b96d8b3b8b1b6a627a612d762257da617a6ba7333c3cff4cef4c1f4c9540be0faef8ce8dfa72a7fdbd29a7eba023ea1dafd528cb9706bb3aa6994189c6699e698e699169816999698966df99e377cda4c556cafb5cab4cab4d6bd9799d69936983699b698aa40876997698f69df32f71d96665f980e980e5d54631cb9c4d350986a900ea2d7349d309d5aa4df74c6537dfeb3d8c2658469a47b5737a24c634ce36ea59e247e9f49a629a669a61966ce4e85b0c302ef9687680633cf2c6016329bffce326a31fbb94617638645b59c66cbff11612e748d12b39511db6ccd6c63596b1e2b3ba37433344b8ee4e4f8626188d397e5d2985d7f51bf25e9426eb96426b1a0da77ccf6cc0ecc8ecc4ecccecc2eccaecc6e88936939ce9ecc5ea6982e9c45d9265606b5a534cfde48119d7d996599e598e59915981599a94b656695df4f55663523ce595dfc9e59835993991a6d9b9bc17ec84c3f7599f598f50d97cf54ad6e8e98699c1366536633667fcc22cca2e68f7e55cd31667166091362d38f4dbab0a22850d05a2f789662966696712b4f6dc09fb3f8d83fcf5fc05f68ead6dfdc50a810c8df82bf257f2bfed6c6a44617fe3606564d7e9eec1276156bfbdb1ae9feed6c6be86f6fa8f54fd7d13211855b7583bf137f67fef4f895bf1b7f7ae4c1df93bf177f6ffe3efc7df9cbf297e34f6ae7d715d5a5027f45136bc593ab74f82b3971d6bfb2d1c05fc5d42dbda3bf2a7f35feeafc35f86b1a10fd6bf1d7e64fe5bbfcf55c45fd51798d71c89f7e1df3a77e6a5c33fefef88bf017e54f65e2fca99349fe52fca5f9cb107116301cf144b49a6b44422273220b3b72681923b224b222b226b221b225b223b2ff4784447ea240d84ad8413313391239113913b9d8a44554e0660b4380623e21a2020fa38dc893c88bc8db0cd7f5ac916ee443e46b5c2f922592b32c107b114097489e478f4881489148894899488548d54ec0de2ecd2139a2ded689344c1c459a445ad656a44da463b826d235745668d881f52b0da1e73a6bcbe472574951b2ee0c5c3873810063c742d8b5e0234322234b169cd048c7442696d1303c9a162a928b4c89cc2c74e33222f267b157eab645ae1e2cebe17fe92d2afa2d62e9469769225aa31a5eac7b3f9895b25d7141da82eeb727175e88c48998e1e8971a58834c124911d15319a2ce02b8a31e31986848744e744174497445748da4b3e4586ee6f269a25bcb116287cbbd8ddb490cfec48b5d52caa33ba27ba26a7d6264193ddaee45f464cce9a356ce442f44af446f58caaa23d187e53edc3e893e2d0d166af745f44df443f44b5435e488e689168816899688962deaa233225a215a255a7342f38246d1baf1a0c88168434088a8b4ff819dddf1a91a668eb6cc962d09a977166d13ed10ed1a708df60c9f47a52325890e880ec5b92c17565b3fa98931eda14b6b5a34040d6d46f4cf14c568c474c56894a86a8e134d98d3525435a45ce54e54539441438941cc130b8885c4e6c416aed923b624b622b636c3586c436c4bcc8eb223b6ff250a5adab9cb76dac10f76b6f5d481d891d8c9cc36b133b10ba50fb12bb19b618d5296d89dd883d893d88bd89b987efd12d3fd1cb13cb102b122b112b132b10ab12ab11ab13ab106b1a6ebe8668b589b58c712bfcf7a365db1ae352d0611536ffbc406c486c446c4c6c426b6d72a76109bda5e94b89b26363ab31c25a1ba3a931ee8f4fa84f90588a40a6ab6b759f3b1342d604deceff7f627a6fac622c6d46251623162715a1b173eed28cf539c58c240a394b1d5cb683296b4dc48815eaea45968ba46bb434cb391369c226d5f8b5092309621ee8c1dc72128b9f4d23cb782a759b3b490e2de968de068e2413c3047d052ca929b655bc4433b222910436cda00e3f35ff766c417c497b6a31e5f115fd348b8f381f8c67c60e25bdbf78eefcc3cb3fdfc3bb3ded9a1a196e1776da91aa5df0bb7b497e653da8ffd72fe1e8cbf8ae7c5f7aef1b19dc3f6d424e4beeb566be2078b098a1f899f889f895f885f89df88df89abc74fe22fe22aa0e16930594b7cb6c402ed0f5bdb859fe65c4368b36d736da0b4fd9387b91f2e6d59c0d575445c3dcedbf28d17881789976c4b2d5e262e8d316b70ba55b6e0a678d53dce164515af11d767837893b8a6a94dbc83a8458a6ebcef24a2e303b7d17c0d2dbe313e32228c8f894f2497884fcdf4129f098710d7af11b326379bb638841fe251e22a1337cb473e4f3c413c495c6f3466a97e82a605a0bcd66ea9e94e13cfd85eb3de74c2994415bf97b454e5e2bbd20c13989522e10d5f5446aeb32511900849cc492c482c115848ac496c107f4dec48ec491c481c499c489c495ccce14184d7f9b21d999e3353f99b2984a24381d256d48c6a893b5a3789a719fd132fdb08695e4d8e25de085324be24b2247224f2240a248a244a081a080d6a25256a24ea241a249a245a24da243a24ba247a24fa2406248624c46b46ddbb9a1e1bf24c4c4c155497121af58cc41f8908090d3646226e36d2f2be7bd7e8125a5a368142fbe39be98d89a431bb9201f87faae0cf8b7a642c2c2e95fbe2d2de7069f929e169d325d493ffd9147a3fcf8a58d7d29409132552160ab35507d29657da0eee1d590ec2d55c0a2a890c49676830082dd18759d1fa86090de37892ba02f364cebd4986660e556dff8ef51d0402de0677937397db925c587699c4c1bc590e7b0bdbbd4f0c4b0a66a79dbd74fd559624976c55d58ae49ae4c6f57324b7e68190beff37b4e48ee42ff9af494fd1db356fdbf141cdaed9d34e54d3bfe9ada91641fe7762f0d074d3d5daf5af240fb6b37f7a595eb781606e93e491e489e499e485a40adccc873b7927f920f924f922a9de7c487edd7546676821ccaba5313321fbf88fe4844b4575927ba619febb7e9469f7db469f3e66a6cb6496a40693b75dca64c176f31a9d1f01f448165d4edd2891d46455485649d648d6493648aa872d926d92aaa14bb26703290a16c635528346d2552c47a57097a6b84f7260fb51c921c9d14f30ce0c38ad5a4e5837392639718f39c929d55f629ee4ccf2800403927f26fd4cabd1544448eaa798fbd4ccbeaa9bd28284d86b3fa0a5d526e564a4298a934c983d23992499321d3599269921e52c174cca930a4885a4e6a416964cbe1227a545d3b5dce759155b192b49ad496d486deddfdeeb3fc38cddd991da933a903a923a49872775267521752575237527f520f524f522f526a55fbfa4b2a472a4f2a40aa48aa44aa4caa42aa4aaa46aa4eaa41aa49aa45aa4daa43aaeb8b24d0ea93389b17104351daae75dce11523d527d520352eacc88d45862df74bf032eb44f9b8aeed852e69925e662cab3664670e34787467be3fdff25c21fb33e0a31fe365afaddbb379398567c6a62568ca0661bda22393bf4b3642ecab5a925f2d3326b68205383fdc2990367be19a919a93fb363ab39b366cf6c934c382815312a9258da4a51d76c442d7c6fd3b16370c3b211e12770a7a8e58cb9377fe1b5a1194d72def09404f2396f97a0b2b08f446e3765ac61f671959e05ca4b7592804dc548e92526ccf7438a6ef9f523c254d254c154ca6c590f154a5b48d1a7432ae3fa49c64fd212d311d2581445a78ff88d66e434b344834687750ba84d07167a7cdb920e49cf492f507b925de935e90de9ada985e91de93de9837b5d481f8d49a44f4675c9ff623a78b6cc6e6124d7fe8f02d367d2172348a985e9abcd60fa8618495ab5a9632f135f9a354b7310b190ab7460e9db243fd36ff3424a7f487f49ebdf1ce93ce90269dd2c912effb6407b0230a42be646b85d9bc1d3ec9c155b0d03755b2dd648d70d88a61b063ecd6aa59e3749b79c40b828b39024dd762989820ae90ee9aeeb480dab50deb9f0e3a4cda67ba4fba407a48762b1c6f2b5f2a4c644ff488f6c9f5d6a527aacb76e76cbf484f494f48cb47e55c7a2a463a4e3a413e6a7924ef21e914e99104ba74967c83833934ac7cbe04efd9f214404b636382add4f9ae12847466f4a646c9e18b6b7d14e92096c6333139299935990599259915993d990d992d991d9933990399239913993b990b992b991d1530f324f322f326f321f325f3259326a252f0148a64046b2f46a96a1e8c3e8500a61a644a64ca642a66a16acf3dab2a889eae63f39a61529fd3953d365584344785bdb17d1c68f35ff47845a6f9284521155406a5ba6cea1edc2a5218b4c834cd392a34907cbb46c53ed5f106c71ebc2abb462cb76abbe89cf7663e6dee0e346b499b6718d606fda506bab7ac41fbb776ccb7abbb0c5dfad93e990e992e9992971fbfa770cee2fb85cc3ec1b71ae7e67bc0848bb811935b56cb4543203a3e7ccd0046c666496a78c06a816a7646664fec844c844c9c4c868c61264923f221cb78cf11c8eb649a0857edb98495d0b2593b24b349d499b8cce646c6ced941dd6115cdcc0e313de39efb02482af3fefbc77819b3fbc0bbd9b7b3b2176e985c3ec48ce8db7332876deedddfee6e2336f3ae8510b84d6d908cf362e1b9614d868f5fc1f28fd478dff88705cf7eeec225a53176f8703dfbcbb7bf77095b1f9d91cd4f4d3bb97a6dedbc9d71fd7d05bcf7b3bcb37eb8d53e97bc1db198da5dfd97cbfc3bd07580e73cb5855fbd5503532ab0dbcabe9096fc7880972a870d3e84dc42320b4d2b85adeb58d7e0e4db754731d6f0ef63d3143333885539a11afaa27030e35f2eaead04cde073d3532b384ed8fc5ed1294150c96241411ae42ea9aa2b1b774fc536fa9db359faa27ea2d51b134124d75d25bc64f4939ddcf789c48c8823c1b790f76a4e4666cb1a03f38aa7723a9e8092c78eab430a09b12058e8cf1491266e6de1248e973e12d238d4aaebd65bfd8fe520cecbd858b1fbdc5ed9ebd45435ebd05c949f43cbc45f2bcbc85667c3c5fca136f8ee4395bd39982377f627d16ad1b7630adfa59f2e64658f154e98716cf1aad7b6a26764cd9fe4d6c3734492829d7574f7e5c4f18feff47842609755fd3fe98dbc146629791abf8a388c7d33019db28ba503d6c7a5ab6bbb02cfc8b5637e11c9ffff2f3b58d50f5726d2fbf6b993fb5f25b0737ab5be6a57cdace30132a3914bdede1f6256abded5ce9cbc84c2f9fb6b7dd89891d11ff69592e4269b6e7bc515da0199b1ad695c25feb1bb89d6a66662245215d5a9a43a97351e166919ff43a6f7a88e0afa04eda88d08b5b1a01fd32dfd95ff0a3d1b8be781f7a3ff77ee1fdd2fb95f76ba731aeafde6fbc17a7199bbda82f497264ff708b899beba79df77b6986de6b5d1fbd3f797ff65effeaa79bf777f79c9a03de5abf3e8c08274753f66ca73e6a9f52ba0e024ecd9f8ad83653cd3fe5f08748bd7f9a316dfaf6fe65531a3cbcd7f78ff75fcb30795e0912b8e8c8fbac9be5c9d5bcd77ce7bd2f785ff4bee47dd9fb8af755ef4575faa9ee7dc375f6de37ed156ed7deab21d5696edcee73f7be23f2f0be6be638a9dae78f5beaf19ef7fd5fe1812dfda52a1fba82c6a876a51765ec64d2f71fa5a4f713efa7decfbcff335babd41e519da8579fb59759c6f53d72d07af23e2225dcfba8f731ef35f309eff578cafbb4f7191f38bd137b1b8104fed16d1b3e087d30b76328ed1ce6e42f376cc7d4a4d4ca926bbe0a666c8b6d7cb0f0c1d2072b1fac45ae3ed818ed055b1fec28bd7db0fffd7bf0c1d107271f9c7d70f1c1d507371fdcbde63678fae0e50395fcf8e0eb83ac0f723ec8fba0e083a20f4a3e285b7af547e0838a0faabfbd473d52f341dd362a04b325e193899f5f81ea54b79b3e68d94efde86c74586821c12500e27ed243f84d803ffaa3c07f57eb604bc21063143bd1696a9230d879cb8bf7366524ad3a3b3f88e1cd9c71d578bb16072b196890722122b133e2b74b338035d4ff9e0ffa3e18f860687be59fcc7f2d4a5e8d523e9000d74f631f4c7c30f5c1cc077f3e88f820ea83980fe23e48f820c9d9fcab7d90327f1a0976a9d0d26c33a37f3d376d56ac24d06c67cc909ebef8d0592a49ad23f182cc8f08c3c087e1efbebdd41f1186e28899df6f8bffcfa53f23df9497dc17311c0e36dacbcc65c5a78f1617ff3959987067e0c3950fd73edcf870ebc435c39d0ff73e3cf8f0e8c3930fcf3e545774ffe6c3bb0f1f34d7ccf5e5e9c397853626ce36d7ff54024db7a4a228d388b065c970cc5ad332a635569d6f1f7ecc662d713d9d19cde84f3a5bb2e00e6afd4ba16d097f162a99f561ce87791f167c58f461c987651f567c58f561cd8775f5db870d1f362d6da9bdb0b5a0a60f050aeae689d2d66cb47da8325dbe9a2c119ede53d4877d1f6abc433382a5973e946c51318920cdf2c487531fce7cf8e7c3880fa396b6f9a3f231aa1a69fce7e4f9fd4784b65b1d4fb8848693b0653a5597923ed454a77d98f17367e93fc5fde6819f877e3ef7f3859f2f699cfc7ce5e76be9847ebe7195be5bd6fd7c6be7d585594b9d7eeadba67cca3238bbe2d1cf777ebef7f3836b6459e991e36f5749ba5fc7cf55d5d9cf2f5e7c737ef37a1d8230f3a79fbf8c08e76f3ffff8f9d7cfb37e9ef3f3bc9f17fcbce8e7253f2ffb79c56d3696d3fa99f0f3aa9fd7fc5cdd6888871b93226f6981aebf0855b3d6a8e74d3f6ff979db2c99b6277e35fb4702dba3fbe14053d524124584c1e13ff2136b98776c3118d51decd2778997b29ddf60c1342375b86bc4135f988e273561db71d1b19ff78c252534a8befbd4cd18d36f1ba98c0fb6d3d669fbf940ccd14e6b7aac4c964a3957cd0293c3bd74363f1ffaf9c8cf55cfc4cfa77e3ef3f33f3f8ff879d4cf637e1e67abb793708fad51bb00b0e92f2f33b6f57f82440c629ea4dbf7f394a1b6b970cae827ccfc8ff0fe77cd33fff7df1f11aad08f08ffeff57b32f87d91789c9b12299228b45c7fec17ceb6e913376fb9923faed393a434979c7a95d9cb2f02bf08fd62ee170bbf58fac5ca2fd67eb1f18bad5fecbcc86371f08ba3dbec8ca72e4ec630ba491b80a6fbf7e91767dbb8370b4de3ff4a4295f48b8b5f5cc578fc42addf6d674fd065fc9b8bad6a7e18a208db8e831d14b1ebfbc5d32fd4a5b75f7cfce2eb1759bfc8f985da2df845d12f4a54ef16aba5d723c5dde8f061f873517692038b8a1d13b3a84a47f78b9aeddf64ea7ea1abe1175a555d67a11baa591d6ebb68da2f3a7ed1f50bcd86da1df8c5d02f467e317612328b895f4ca5f4d34b9846741a1a29da097e11bf98998f8b00e4426de9dfa85fc4fc22ee1709bf48fa45ca2f5473c62f9d25e395b05c06c4fa7e199aff6a352fb51049fee5dc2f1756c372e997b63be797ba36d63d01e6e5ce2ff7a6b9151e7e79909ee69747bf3cf9e5d92f2f7e79f5cb9b5fdefd52bf3efdf2e5977aeae3975fbfccfa65ce2ff3feff61ebbd9655e7b9a6d18bd1b98a1c0ec9196c0c18386b134ccef9ea77b761adf5bcffb767b928a6b16549566be43148ed97152cab58d640982d1b5836cd8a275bf6c95ef1350db1ec60d9c5b2676235ed05e4f0296957427d3a05934860e9c8af253511ff29e3c458d56ae3dcecba02a16c7dec892b48107214a8f866afabdfbf7f3f43d9f4b0eccb158e3c1165c2d4d8d44e91487f540dd0fb5a71498325cfc8202193183b59517fdeeda8d60567831338c0927d1e45201c69fddc39753e96632c27584eb1e464c6b164b79358a6b04c6399c1328b650ecbbcd9d7f414c29b7b47676ffc9344d662eac7be151366522308258384f60bc27fc88a80f75facfd03e1dfe377e99736864627c987860128b7349e3289167ceec108e78abb2f928dbe29dfe3ee8370a184e44e15e11261887085708d7083708b7087708ff080f088f004327b1822bc20bc228c9c925209840a170239d51ccf3f7f2ce80f84110ea72f84af4841caf6df08f9c4825ecc692d486cd8721161c94cb208cb082bf66011562d37e9b086b08eb081b08990ffb61176c07719f6103a085d2997b965f646d2f190a48764f3d83d8fa393db44de903bd5761e0e10f2fc08a12feeb49c4138b61e6769a2e4e7d519c2a9dd1a84315b64b371841c54d2789cc014c2b4346f9129529f31f68db7b3ab6453395e9e71c8ec2be135f9dc55201cae6658cdb15a60b5c42ac46a85d51aab0d565bac7658eda3fc94351909d263ac0e267bc0ea88d509ab33561745c6381e5657d36a62a53240a636c1ea8ed503ab27562facde587db02a6055c4aa04826a55c1aa8a550dab3a560d91c415ef6d61d5c6aa835517ab1e56eca78b551f2bcf16f9d3c06e06580ded604e465701d931de3e9264954889fe903b5df964c5b11a4bbf45cc74d3029bdc535e11256c49294d69bc54926303b1f7e589bea8fb03bc1f0ebf3fe5da52edf85751d1d45860eb8db09a881b5ae64581e371ceb34ac252d2a31c1e5b48f99f65e7a7ee8d50f7959dc81e64da15846ed267b6391531ace25825b04a62c5f94f63c56e67b1ca6195c7da5aaf8db5912a98af9eacfd70a9fe6476580762d3d633a933c657ace7582fc8ac61bd34ae1f91c1c597aa118cff97dafd0784bf8b82ff01e13ac47a2510aed9a242f123a5df0ba463eb9d595cc99462bd57b59a49de1057eb03d647ac4fa658c0fa8cf5056bf6e986f51deb07d64fac79efdb9eb97f36b0fe60cdcb8affd8d1482c946e466261efc7914e1b5f1046e32c615dc6ba827515eb1ad675ac1bdcccb06edadbc4de5a58f368dbcfd21a5e4c82f9b49f1ed65dacf9e960ed62ddc7dac37a80357b3bc2dac77a6c36432538cdba2a2e174fda7d48224360603dc17a6a367d851d524c5bc7acc721c4b14e983887c9f949c9e854ca619dc69a2f268b75ce6e2939b3fd3ca5666c8ced4db181a26f131369e7db24f83bd2436c026c66129fd26373f4b199839cdb66894d080a9c9b35361b6cb620852729de1cb0398aa5dc9c281662c3b98d4909491c8672f8c2e682cd159b1b36776c2860f3ae27362fe951366f13ab9a3891bc35c921361f6c0a20ebb5296153c6a6824dd57aec6a0d9b3a360d6c9ad8b440c973d3c1a60bf2db1b071b3eba8f8d07426ec34646d8f82ac992e50563a5a58b676d65261a581faa330b8e3ae2f3f72bd3a540c566a7c6e5c99890369c89ab4c4c450937f1c87f384d386193907cf1c55eb0fff147fc4292288fdcd36f91383fd981f20836491524db730652f276769af2f7d20472d7638399e8a7acbc2c35e41c3679b97f5d9252d1665df92d19a36e6cad32fd6f613131ce01db00db996256288d6fe7d82eb05d624be9b469372713f6cc742df49262a78ad8aeb05dcbe9df1d986b19db0db65bae6a6c77da71223d0b71f897c18cfefd73fc03e11787c4db768fed410705c9ed115b3674c6f682ed5520eca6e5965a0fa445d846550a83bbe2ac7cde7857a2f87d85fdc6f681ed13db17b66f6c3fd816b065474bd8b27f156cabd8d6cc8afc43dcf4da367d8ee265ff8150acbf9b813d510ac5b6fe5f104accdd36b06d62dbc2b66d4a6cbc43b11edbaefc80d36cbc671e7cba83ad8b6d1fe448b7fc7588ed085b3f42204f8eb19d603bc5965d8d639b30dba8fc4618f0ad70efc496af2485ad62aeb9cb629b91d4317e2820bac45792c5964c0efb90c7ceaa40c30ee6f8c12ec06e86dd1cbb05764bec42437e6647a66883dd1a3bf943dadcc6bd8544a0561559d33dd1d3c16e8bdd0ebb3d7607ec8ed89db03b6377c1ee8add0dbb3b760fec9ed8bd14f0397a994e03bbb7ad6cb54dec3e4a0fd77e5b6e31bb027645ec4a119e9f51d834cf94b1ab48815477b1ab6257c3ae8e1d5b6862d7c2ae8d1d3bd0c5ae871d5be0357dec3cec06d80db11b61e76337c66e82dd14bb18761c5102bb247629ecd2b6b3d6aeb4cb609795c6ee9d2565b6cb8c7297c43ad2cac4cf36cb3ee4cc90cde61565429e9c78e3b1a1445307513a9b4b4db5ef0854dca452aef0d6ddfde3427924c63f71b1f1c41e12c8791705cbd159912e84d03eb05e06fb9962be9363f212dca0e5b93298c92c11e736ea623f37fe03fb051f81fd127bca846f2026c9705004370b6705d2d2fd06fbad39a9aeb379376daa2755cd7e07aec63db73f55b356490e51b909f647ec4f7229890cddd89fb1bf607fc5fea64e1ecbe654c2fe8efd43c4903baf984af3972ffd5f4af8f710e4fe82f01c6947674a3c51e3e09f26cbd1be2cdfdf36a93a84c151e90fd3c49ef21160ffb6174e04bb58c09ea3e2e3cbd857b0af625fc39e33dec0be897dcb9439a43638effbaed8faeefe6b20d2a47fb991c85822d2f71f1072a8d8f7b077b0e724f6b1f7b01f603fc47e843da7698c3d27658a7d0cfbb8e166bc4fd8173fd9e714f6699363c732d191c53e877d1ee4570f0607d8dd40d1bdf7b9a435423a1c48d3782aca1d872ffb54928969b6e256aaf4c9f5a225fd3c04b28cc7ead295cb42ddc16186c31c87050e4bfb6ce110e2b032ab339bc2612d8f446e2e899489030792b899c966a52accf2b2ad6c06871d0e7b1c0e381c7138e170c6e182c315871b0e771c1e383c4d318bc30b87370e1f592366574980ad9e6c80e5b4497f24b01d0a0a42f5f204210e451c4a3850e80d6c8fe3ade05035630f871a0ebcb2a13cdc053ea889033bdcc681a3e8e2d0c3c1c1c1c5812d78e2510f031c86388c70e0c0c7384c7098e210c3218e43c28e32c2a1a492839c78e2635bb4a2d2cb64c4e717258ea69238f048898d2c770542f2819989cd3d64c12337e126b9d3e19056fe5b771a8580257e5c6818f90c460a522d150a5a414f9514de2d6d645ec22646642695543abd91db892a75467c0d095d6a2ebe83c0a3109bf015c7b89d0b45870c0ed928f560516cadc389cad9dc1a87bc3d713b7671541a6773e5ee1c9382ff18e038c3718e23cf1c715c4ab8cd8f22d7d3ce4f629217f4cb90d108f2388638ae644bbc2f23ab6002c7f55f100a68c7cd5fc6f3ff8090f450383cff03213fd79befa158ace556d9b2770de53e24088f2f25145d8e557c5349e98b66ce2e7223d9e3b8c3919f071c79e684e319c70b8e571c6ff6bac4f18ee303c728a9cc1f958c269a501c46e1b3441d396c797597fe5929a60d1c5f38be41ca732ce058e466836349f561e663a5a923ab408ec82ca0f3151cabf6ce19a9e158c791f736716ce1d8c6b1836317c71e8e0e8ea47e7565b2126b1748944f4aa2209f633a24b66ca78fa327af00a2b43751ce81e2cc64f84a062068074bebb04b231328d3b1351b90b73c8e719ca8b0e133e472c171aad5902ae1185365c5655e378e76d1272f8ee3c897949406f298c2318d6306c72c8e391cf338599c8cac3ca700a799a22506d616d2865bfb692e954c30b7edb42d73b00993cbe1b4c0698953284dbd57c16905cacca78d14aaa7ad807ada816fe77430c3294e479c4e389d71bae074c5e986d31da7074e4f9c5e38bd71e2f5059c8ae08e7e2ae3c406ab38d570aae3d4b053dedbc4c2eae9a796629727979fb780f389fc780842ded8c6290aea8d4c3212c0b8f396b4b5891051424e2622c37ddc3eefe23bdcc82fe2bb297fd9d18cf70361252d73dfa92be673fc1484c64f7b79cb6476ea457e9725695f29072ae6e8a28046ce3cf7cafbca7a639c1cf9b8c5391cce4614ac97d86b53484c458af72bf95750e43e79380d701ae234c2c9c789374e709ac2b64d22a14c4ba1238f7fffa0554722519d445e252729f35345c953d7b2210f1f7ab28e1081a7d897129a7ce607c23fbce81f4af81f81f01f08c58b1e23101e708ae3c486e212d8c8e4cc3c3b75548685505c4f941bd73b9a65470527c297d98dec9b4b2789530aa7344e199cb2528d9eb83ef2385b9c8da99e65383c0726e8e13cfb49de7f70887946c779aeb1c98dbbf84f374362785ee0bcc439c47945861ee735ce14bc46d6dfda1cdbdcaa408d99e3bcc399bf1e703ee27cc2994fbce07cc5f986f31de707ce4f9c5f38bf71fec8177ea374e0da8691957a265d336e807381fb34f05675e1fa5876f97311e792ed25ec722dbd68c806cb640a601c93e7af159cabda3bcf3515c0785eedb18473dd165c9c1b3837716eb937f6ad8d73476634a720ab7a3a817317674e055be0957d9c3d9c07380f711ee1ece33cc6792203c6798a734cbed151941041689d9abdacace3db321f1417dbe9b4252c9c13fc827312e714cee9afb328ce195938567c0a9bcae19cc7c59a4359a5cd2ec025c06586cb1c97052e4b5c425c56d199352e1b5cb6b8ec70d9e372b0e52a2e475c4eb65cc3e56ccb65fb2eab6a3ab9af4649f125c3212e17f900f17b726bdb4d49ad972b2eb72862ee19b17f10d2f2399d145416a47214e1a426f50fb8dc25017e37e5eb4a8a9caf278dae89c448c24f5eda3d5c1e62442f4f293f8934c5c8b7c8b668b3e34b745684012e2fe31a891b330ee10d52c24b0124d1614ce474420ad6927b70ee06f07c0917ce49c5ba1c63d5044d5c38cc3a2e0d6397dc9771e19996b686e14ac01b94f84916345222464293df12c59a1771694b22bd7470e9fed49c1776d8f987b5e3e67fb5a3ff8712ca5e1ff2c5b842e0a5cf43d1e2ce4ea502eb65eb118155bd78ae8f8b87cb40153c2e3d5b49e1c27730c2c5c7658c0b7f9d4a2b7389e112c7854d257149199f2da7ad5e79e68f85905399e5a128321ed1904c9b5396fb07c22a79fa132e795c550a0978c917d6f5b821e10abdbc6b802b05833eae735c1720c9bd86e05bbcae71dde0bac57587eb1e575e79c4f584ebd99e78fe82eb553ef8a41e7c977c43a68deb4df9f0e65fafdf89492671bde3fac0f589eb0bd7b7f28ebc89070afd155c3fa61ac3b560023eb1886b09d7b2255370ad58b29dd72aae355cebb836706de2dac2b56dfa6f5c3b32ac5dbbb8f6707570757165cf3d5c07b80e711de1eae3caa14d709de21a93c34f8584ae2df7f10919dd87e4820a8713579eaee54b8e01ab9b89b1c184b88c15278add4ed9f40dd734ae192c392ef627876b1e947e6f065c78b700147db93bdc16b82d710b715be1b606a5d9db16b71d6e7bdc0e201b763be176c6ed82db15b79b7c366e77dc1e92098381ac70ec5b7ac44efe345ba7b67c8feb23dc9eb8bde4eb573b2960b5235724a946c98eda814018bf18ee1a4426814796e1f626f0be87ac0832057baaf04e8e911c8abc6dd8e18f40481ab89ae8adbd3bf223cfaea5daa104312846bad3b9943437be44440c6a0fb7226e6cbf8c5b45f17eb185e245bb918f7e9bfdac1aa765c2096e35dceab835603ddc9a2097f73e7183c0ad8d5bc78c39035d59ce536cad674a9c0447693bdd41c4af958497356f774d6f244a78ebffd85182f0bf04efe6fdec845f5de8bfdfd6ab9fe028bd285fcf80c45032a1c948df7a1baa0c77ca213b6a8a7cf6c8cecaca5d73c95bbf67f76ff796c4cd37738e768c1b07330529c32d8e1bbbc29f52b8a571cbe09655b5a64b4343da77c4694c8ad2f6de72b8e5e5ec73b74acbfd7561fb034255a1bc03f700f799dd5c9598a454c276451912f7b919a5ed73a682508f1aee0b5b5de0ce7642dc57864cda7d8dfb06f72dee3b51c2fb1ef703ee47dc4fa632c4fdac0ceae4216737e9b84b53dc2f72e68e711b1e496b37bae07ec5fd86fbddac4bb8932960e34ffbe2952fd3f2707fe3fec1bd600f495bbfe35e344ed3063bdc4bb61e3773f6b08c7b05f72aeeec5e9d421d4ccd90cbb837706fe2dec2bd8d7b07770eb387bb83bb8b7b1f77b63cc09d3d1ce1ee4bd2db9cb4d65b6b53ccd80a9b1d0b12f789acf3f7a95dbea595a104788fd9115b60fb09b389e19ec43d857bdab4eab86770cfe29e0379f5875561cf07f008f098e131c763111d4b3c423c5678acf1d8e0b1c56387c75e1a9dc7018f231e273ccea4b17844611994a31e573c6e78dca5280adaa48166c27f1fa6c9eb9f78c8db41d9222edf9ceb45a5722287969c0a060421f7e2fc486a9228fcf2cbafca5354a92e4b3269f082c94bde14e4568a9081a194a3d0a5efa99d791eb8a78bf22c5dd93c128a96c2bae0723779bc050672e68f8f7c4b1e053c8a7894ec31864759a88e45868a0e49d6cb92f57854f0a8e251b3f73b1e75e34df168c88af8200dcc48d81b35e4f58aa492c4e4e38a80cda6f168e1d1c6a383471780ea52a7b94d7431e34cf622231fe7d9f9ab98f96a657e94f007c2ff3f53211eeeff80b04b11766e26093cfa6444f1f0942777569318567ee331b0ed8575f732543c86788cf060bfc778f04a0e83038ee3c17b9302e12385479a4c20674153964de29189dce79bf2d5dc1f28ef8a23fdc78b46949014ef91c52387471e4f8ba701fb755dabd04770c333c073663f053ce7782ef05ce219e2b9c2738de706cf2d9e3b3cf7781ea4a4791ef13ce179c6f382e715cf9b5cf0a2b06885a827ee0ab44d8d7148990c9bbde3f9c0f389e70bcf379e1f4b22f3e4838a96e4e259c2934c29cf5734fc67154aadcd36eb7836f06ce2d9b2d30a9e6d3c3b7876f1ec99ed184fc72447d669e1e9e2d9c7d3c37380e750207c8ef0f4f1e435133ca778c6f08ce399c033a9dc84bd99cc0fcf94c9f5ed5bba163cd326cd2b3376ed594ab0cfac5dd779e099c3334f76416573492649c70e0dbc02bc6678cdf15ae0b5c42bc46b85d71aaf0d5e5bbc7678edf13ae075c4eb84d719af0b5e57bc6e78ddf17ae0f5b40db6f6c2eb1d295a0bb6de31a5339607a58de37e971ee1f551c555ae4bb9b0ae602a7815f02a9ad1f31bce2b75971f496ead8f9d6d05bc2fccbad2d6089084843bfa590ebf20b4be28a1a34c227666b1c9dab623e3d0d6c8c05b1f4419711dec7d524bd1580591e515b810905b89592f455ed4259d7f95f02ae3c52e556d7b636791803a9f0854226b6485fa92065f35bcea7871ba9a78b5f06adbca0a5b729b9ca50e5e51b40792726df57b913af489570f2f76e9216b829ccde278b902a1c90a845f4af8ea47de2fc17ffd61fec8845fabe0ff63249491919df6b0ddffb4a32414cbb5f2522f9f0aeef0462aff3da068f7b6e58f0dce86d4f635b0fecd3e47c69be0c5c1f095f8788df1e2bf53bc62aaaffc62ff127825edcdc52b1505a7afe4321ef0f5734769cafc22f16ffec76bf40f250cb8a4327865f1cae195d72be45b497c28b5a936f39be25b80f70cef39de0bbc97788778aff05ee3bdc17b6b6607bc7778eff1e69723de27bccf785ff0bee27d33b685f73daa461ccaf8765f9a215b78e0fd94dcd24de0fdc2fb8d371f57300b7e166d616e2973bf4ba0504412fdaed85e16ef2ade357b4869377dd7edae8d7703ef26de6c9fdf3bb65bc3bb8b774f510ec50cde0ede2ef8eedf1ede03bc87788ff0f6f11ee33dc17b8a371f11c79b1d48824b2d3d30d98b4dabb0bb4a14f7f2729a79a7f16653593b63677266c3aee6f1b1f8187c804f80cfcc24437ce6f82cf059e2c3ef2b43607fd6f86cf0d9e2b3c3678fcf019f233e277ccef85cf0b9e273c3e78ecf039f273e2f7cdef87cc09dee5394efdba7649747056770209ff237ee54b2eb9b4c6f42d566b9a3d5796f059faa496714bf1bda28764920943a7a5014bbf1a9fd1cd39a03018f50240bca2f04e43ee295be0e6b1426b9a9cc8e8a2f2771b3351c078ae50ff28a632ace941ccd8dc27ce37101e053c7a761b60b9244fee4721bfa34f169e1d3c6a7834f57fc9407424b987ff7f0e1e1e043f00cf0e9e3e3e1c32f437c46f8f8f88c4d2a2dfdfc6722102a2d4b4fead0701661ef2a348633410e6274f199460e8f33898814ee668b0884c15f88fdc763e67f69e09735fdc9919f183e71b5421e9758ff246cd041e82a616171692adc4bd8b3a4f58bf8a4ecf16dc3b37b1bdbd3cc861c641a9f8ca972d8597c72f8e451b02a5d5ea0f41ba03043618e429450d8cbc8f3c8297e7d29c48eba43d37e12847f70d8fd51c2e11a85250a210a2b14f87d6349ca0a5b147628ec5138980bbf1f5138a1c0f31714ae28dc50b8a3f0b024c585270a2f14de287c5028a05044a18442998c070a155bbc9af890c4d93a0f3be305553bdd2bff2c974bafcdf584424dab5cc16cbcab6eaf8e3df0b90d149ab6b476c966175a28b451e8a0d0551990c65844b8d0b314890b0e0aae3db0037d143c1406280ce5569ae3f7110a3e0a631426284c51603fe328245048a2904281cd6650c8a29003e52b721c45ab88f8f20d45a39c7721058f9948504c29f354879c67ea3d514baed86280e20cc5398a0b1497288628ae505ca3b841718be20ec53d8a07148f289e503ca37841f18a221bbfa3f840918dbc50e4433f2816502ca25892db40fb2cfffb6219c58a9dbaf208cf3a72c72956b13a28609a13d52e62bdb5b38b021d1bec494dc90446676521206f4f892ecc0a908ae15c09665f45e897f4291768e71f084918155ae1ffbc6446799928be94701f988d6382b252b1ec17e26f89ea6f60cd1a76bf9182ba98e74f28d6516c2824a2d844b185621bc58ebdb4cc40163ff2bd289275e4307b283a2812872314fb28f2fa018a4314f9af8fe2585117b9d0742f228352c970c626825f718a624c3089d851939d98f94822ebf624adca762f6415e3ff6544bf88fb0f08ffaa67fe80f01bf2448e546470bd11258c739613021b09e0b16a9777550be0922a262d97ac229af99248f753f69244310dee8ec52c8a3914f32859948c421a4a014a3394e6b8a4505ad8f4d6bdcdc5df478e4b282d85c0af88f80f7e5f134517a510a5154a6b9436286dede081d20ea53d4a07948e289d503a5bb267a50b4a57946ea0c45c7ad8f210a5274a2f94de287d502aa054849e5546a9825255b60ae2f0cc9f6a9698b9d4b16487eb0aa7f0cf8a964ab3b5064a4d05e6945ad619a1d446a98312bbd4b3073ec8513a6da4ccc4e776ab741b1e7f756d9e4feca3e4a1344089dde08d3e4a639426284d45344a3194e22825504aa2c409498b1296322865e5bfc64556caa36c51360a052907b69e41504379a61888c515e5b902a6ea4f39a315ab2ac1c7d5dfcbd9ca55f6c3e908e505ca4bc53a943d9443945728af51dea0bc457987f2de8e5b281f503e8a06964f289f51bea0cc966f28df517ea0fc44f90582bffc41b92087ef7211e512568e7240e77bb04394cb91c3ea4e894cfb1f1948eaae0a6f05aef258cb583f40b922e029df6157499cebf26243b92acd6457d2e0cf4afcc51ea919bfbfbbbf436722eed485949f974f741474240e669443b96697316509193d254f0e9732e22706caf0efbb510294b94cf3e53aca0d949b2873d47c7a07e52eca3d941d7be1a05c94fbd0440d64d67a739e3934cea18ff218e509ca53946328c7514ea09c4439a594d07e5bd9584809cb69adcf724607f9edc512e5ac1c03c8889acc1f85cbfa7ffcd2f8ef71fdcf4ef80f9dffe3407394a6f4d1d333f64f3bbd2b8c40aa304f1bcf8d3b9063671c554ed6b3cad5921494f3d6ddd9d31e156b9cbbac2b15235b4505c6af891dad04a8cc50999b4c1d9585d9e4a5daf2d212b839d755ff1b5b1dc5b0ac50592a11c0d77f8dcc77250459f3ca1a950d2a5b5476a8f04107548ea89c5039a372898e2b2a3754eea83c5079a2f242e58dca0795022a45544aa89451a9a05245a5860abbd140a5894a0b95b69df14bc7068e743326d29456ba66c236f9742772619fa0e2a2d247c54365107d0e5119a1e2a3a26d526e7461dae478865772f81c72dc3e3e96535a49a0924425858a52f598237fcaa0924525271056f2a85a548d82aeaa01aa3354e7a82e505da21adaec04d515aa6b5437a86e6dfb239f8105398b9a9422d5dd972a2ae148b845756fb8f1570fa81e8dbf35d702c89254cfa85e50bda27a033b597d18be88ea13d517aa6f543fa8f2b222aa2554cba85650ada25a4395ed34506da2da42b52d3f9b6a07d52eaa3d541d76c374d8a6ab7226c95e943abea66436a7655486fd2c6dedd147b52f10be9f727321717353824a14592f0218e378bdbf084475a0fcbf5f23e1378564e319c5af45ae1d2474d9b1b8cd4a526fe7c1ae0e511d9914d9e9baf431943089d5d0ca1ac4e534e58c8d4936519dd8194734b57b0fd518aa715413a8268d4f59913d4ca19ab61bd72c2e26efd844cb869400395119ede0d5aced705673a8e651b3a819b985d4805a404a689a6311407eaf05f26c79b4519ba136476d815be76f94d27f69e00f6821f994f84f31f373eec63fffee3fc84c118451a99687c2a172a14a5bef3bb6b830f3ac2a9b76ce8aa5584f4c3db0cbb39d519eb909846105b5256a216a2bd4d6a86d50dbfe644282b0b6436d8fda01b5236a271336e48ce2dfa3d41213057a0d67c2e137b1d21f108afa2763a89d51bba07645ed86da1db50748a86b2fd4dea87d502ba056b4871a6a25d4caa8b11b55d46ad6afa3c6a3815a13b5166a9ca60e6a5dd47aa839a8b9a8f551f3501ba036944bf7e9a5d23c1e574cc274b3a88d50f3511b7f835f509ba036b5a4e735f6278e5a02b5246a29d4d2d2e655d204216a19d478630eb53cea2ad3643f2928d0c7b7e307385db90dea330972f539ea0bd497a887a8af505fa3ce9fb6a8ef50dfa37e40fd88fa495e9af5b321135bbf281eba7e15dd6b531608c598f4d2849fde85aa3bdee4475127a3c81b6fa8df6d3d090a662498f517ea6f258c3a9e51ffa05e40bd887a09f532ea15d4aba8d750afa3de40bd897a0bf536ea1dd4f9e81eea0eea2eea7dd43d591aeb03d48782bde36a17d82641f9aa51c29e8fe02b9bcb2377b3b775cfc443eb847815c88b72c802527360736bd1b4e94e9baf52eff83fe6930721d789e8216920d1c8f924339298eab25349b75c0a62659779d24393cdcb2bedbe92a7f8baae4c4d5c4593974926501fdb29fb3f11bfda5d4b48ab4f518fa11e479dbf724ef846d8385f018793433d8f865596bacbdc74739c55d3eca361cca38e06a2c2d2453beaa211a03143638ec64276b59abcca84400a7e84090542ca6eb59982ad1a4b93b8ffbf5ea3ff0f25fc0bc2ff9718fe150bd97a23d4e749b60e9b3ec965645d32415771068d959208f82599ec1dd706a13de6adffb6242c8db59cd71a1b34b6623e1b3b34f6681cd038a27142e38cc6058dab6c748d9b3d4e94a3c58b12012b7bcf48dcffd743c2f5be0aa86f90351a77341e683cd178a1f146e383069f5204df7da38c46058daa0e92c4460d0dce1d7bd244a385461b8d0e1a9cc11e1a0e1a2e1a9c5f0f8d011a1cc8080d1f14e11a1363c925f2cc148d182e4f2c7d9b78098464bdb20713e34ec43e242ce9672389460a8d341a1934b268e4b0e25d79342d0e39534dd8fd164d83262c894f33407326b6b9394773619373fb6ea2295f393443345768aed1dca0c95b7668eed1e49547634f68f238cb6dba794193eddc14c9b15cd9e993af40bebb4460b8d267f040f32e0fb575d31ea7683eeca6a210c46664d86cf278a3f941b3806611cd129a65342b6856d1aca15947b38126bbd442b38d6607cd2e9a3d341d345d702d363d34d987a1c9b3ab23347d70fb6f4e94acddcbabb0179156c84aa7b58c9b75dcf6a6df04a70a76bbcfd0647f62ca76752d7e0308d18cdbe9d0ce42d137be77a5271b47aad183026dbdc94f388c3ea5f024ae06c52f0895baae928c92c100af5414397156f6a7744371fc946bc88e4e38d8049a493453ca18e88d15ffdd4ca3999136ae99453387661e2d6b2c99b89282d75ac6861b457ab702e97806fc3e5384fe716ca63c39476b215367328e7b09ad255aa1b6896bd9e4fd2f0875108444656b252fbcd65abec7a484f89fd8a57fd687f5fa97dee2ff5ef1e3455b9b1f2524a98940a89cb67cf15f10fa215adb282be34e85a3761d5581db8c9577f004fbeccb8dbbc57eec2516b60e681dd13a493dd33aa375917aa6758dd4215b839c22a4b8abe5db321971de2be91f37725dffd21c44f966d0baa1c5c962cb4fb37a5aca69ad175a6fb43e6815d02aa2551208f5bd8c5605adaa1da5d1aaa155478b1737d16aa1d546ab835617ad1e5a0e5a2e5a7db43cb406680dd11a192f81968fd6586184333e6b82d6543539f60b33be631d2873412b663ffc298e162f4ea295b2a4de0b0e2dad2c0c1d791ba395412b6b571f7be2971c5a79b4ad3dafd1366803ed00ed19d64bb4e72656447b8136bf8768afd05edbeb07dbbea50cd9dea0bd457b67d205b4f7681f6494eb9c5514e45420de70f88817ede5251ab40b91a9f084f956b1454460fb84f619019f7541fb8af60ded3bda0fb49f68bfd07ea3fd01ef6ab30325b4cb6857d0aea25d43bb8e7603ed26da2db4db6877d0eea2dd936d83ac5ddb453bca98c8872edf0a28697b4ace9fddc831e5c8a6062a6172aca83fc187940aeda17d96940fa2d396e3a5eafc0c44dc48e25453600ace9b317f022638525f3b3291c94fe22a319571229e14e52402f9f4f81e87c0bed9cfb1ea6a7a39ab04195d9159a7a85f2f1ce644b961a4d2df9b6cd2a6cf68f34131b4e36827dcdbc87a551bcfa2cdfea7949f2637536e8b765a0573378ac312bc676f42513eb197b2229e3649a5a5659ffb7cd71eda19b4d942ee87c3ed55a8692b7ecdac63324e12845f62189a7ff689affea56311e21f25fcc785768c5af97efe5cd87e876c352a18dab7eda39c363b044c151dde3253ca998183cedcec3d421127dfee070ab1eb2cd059a213a2b31201fce4d059a3b30139ecce0e5cac538bcec1746be81ce5e6a7dcace24914581d79d023b4826246c927d1399958d7be07201bdcb9d805cf5ca598e9dcd0b9db520b1d72a7053bcf098a9d273a2f19303a6f74f85940a7884e091df6bc820e2fe043ebe8346408ea34d16989cde8b4d1e92869805239b8e874d1e9a1c3f5fd52d507ae95e6d2e4b3e8f0a73e3a1e3a03f9e57486e88cac6370de90548aa30e38401f9d313a1374a6f6d04027864e5cac7227814ed28c92e8a470e046cef6d3e864d061b3390bb69cb7a52dba9644028ad709d09da13b97d5e134b34ea0ea71a23f594524f7e2eeeda5d460792b3d0df9c6dddb1428a5efd05da0cbbe8dd00dd15da1bbb6a4d8dd8d296cd065fbbc608fee01dd23ba2774cfe85ed0bda27b43f78eee03dd27ba2f74dfe87ed02da05b44b7846e19dd0aba55f09575ebe83630271b7626e6a567ea36d16d599fcdb63569c7311e57394fa7d95a4761d0f3bc38e67c2e4a7c16313e5f3320d99f564151ead1fe2b6bc4d744e126157d422920b6fe52426c6291aa2caf7897e3caf8ec61970236ba3d60253635a7244932f0fa671234741d74dd28e821896e1f730f5d1e03748778e6ec846f2d862ea7c81731981545ba6737b983cfde36c8dbdb04ddb1f25f7e1ddf724bd3e3bd13d16a0a26dda94c14e5cc5720140831332ea73dc6edd8040349a4df60c2088ae8c6ffd1b93f27ffc763465f3ae61748f10d658aa2287e9edcc735f72782d0867c49911beb84339e942e2e3b453765d8c54add3eb9ebb4d04da39b01a5a9223f73e8e6d1b3e819618f73d50bd09ba137476f61a767591aaf2f85cf165fca1cc36d2f2be598de84b2ca6fa480ca9de4d57d7aa3b7442f446f85de1abd0d7a6c7087dede966ee81dd03ba27742ef8cde05bd2b7a3c7947ef81de13bd177abcfd835e01bd227a25f4cae855d0aba25743af8e5e038b83b16cb0a9dc3303d84b606715cc9b91c3fa4cac489cb7b74ccfb74ed56e92e8b5d1eba0d7354f8eae1765bc5e6bf5e4b36678960f5dcf41cf45af8f9e87de4050ec0dd11ba1e7cbf5747f543cc1a8822b3b3686587d7e99da2b7b18432f6e5ebc3e811e1f94925eb497969fda7e638f7d59ea7bd928219f27c9bccec16650a8d95dd352501c656c9963cca297432f8f59d69e3b66598463c19dc2019c00ce0cce1cce02ce1294d99c159c359c0d9c2d9c1d9c3d9c039c239c139c339c0b9c2b9c1b9c3b9c079c279c179cb7ec009ba72df7e07cc40c670927b65330a7a75824a709a72832e894f0e2d3cb4a3a1adf8b5dfc06c5970a702a02db982d57a33cb45319e51351587d62fcc7585ff8baa18977255c4f0a41c226102af66b25f522149d1a9c3a9c06f601b81d3b4d45ba392d55267cb7c58bca2d6e659db729b3ff1d38e4e61ad6fb2887e0790ca767ec5bae60ed02c54293ca281fd4a90bc781e3ca37959476af3c5b126589739207a7af5cd55c99ee008e2710521a0c39b1fc77287ed81945209c8b061e39b7be1cb8ff52c23f46c12f3b8abf568a9f1cf8bf045062218f876362b09e6f979cd3b1f26dbe1d9585a9889a195b8233b137f6782a252f7717871ba30f270127092705272d469420743270b2707270f2704906db367d57bac8a0224548bea38cf1dcf9f69d28d9c148809449b4404111dc8d9e20a9801bc09dc19d83bb8ebb841b82efde5d0b84ee06ee16ee0eee1eee012ecf9ce09ee15e64be77af706f70efb222ba0fb84fb82fb8946329d35a7b2bc3fdc02d281eca2dc22dd932cf94550ce31dd5c4edee15ab91e3e32a70ab706b70eb701b709bca5e354a2a6fcaf0a06db258354ec792b7715bf6d085dbb60736d581cbef3db80e5cd7a42d5795f2c10405bbcbd829cff7edbc00d793630d5fad3b843b82cb818fb15a9b34cf4ce41116ba76c7e7463671975d8dcb6f5b859667ca2b434a3275ec9b8f4bc24dc14dc3cdc0e59539b879f42dfa46d9aafa01fa33f4e7e82fd05fa21fa2bf427f8dfe06fd2dfa3bf4f7e81fd03fa27f42ff8cfe05fd2bfa37f4efe83f2c99f0fe13fd17fa6fbbe6af1ff40be817d12fa15f3619365291c8ea87b261c6f9942ab8c7c77df94c931892acc92129f72fa63e15e9bddc2863d237aeb7f18c54a92302d5ce56247d1208b91ef2ca9a857e0dfd3afa0da5d871aae8374db7837e0bfdb6bdf5d1e7f7ae9924549670e46bb33b6d70f484e47e0f7d077d57858994b83e21d4d503cb2d2cb8da82cf852d1a3b50c0feb7f88a2a732e331262e3c99fb8246f674f082cddff8190d879b85feda82ca2d9a3dd73aafb82559fa0f045c9fea3f8fc3fa14c5f1cfecc12873fc7f91f313459580e6c80fe50891c898627848d8131c709fa23c315d3f7d11fa3cf7fa7e8c71452d48f1b8f2713e8b31f29f4d302619f4756f997fa39f4f30a6208f6365c2a62d24b8be72efd496837994a3cf06c24ac8fe119788017c09bc19bc35bc05bc20be1ad843d6f0d8f9f5b783b78bceb609a05784793a38061e19de09de15de05de1dde0dde13de03de1bde0bde17d9419cd2bd8b0285d995784578257865701c586a9b1d37da4b6dd47dc11dba9992abb51872704dad24275ed490c5560c4932f62467545e035e1b56c666bf3a4cc03786d6968bc0ebc2e94a12405cf91a58182931ee4c2ebc3f3e0f1ca21bc1138751a353b36851793e7b417c76e67b8d77a09d54baef74c2e0f2f1955953cca558d20f4d86c1a1e89e1075ed65c3336c5b9cac1cb6360313018008300831928370e16182c3108315861b0c66083c116831d067b0c0e181c31386170c6e082c115831b06770c1e183c317861f0c6e063538125f606050cf859c2a08c4105832a06350cea2a901e0f314b2bb8feaa7a638a173b49cf2935a6024a373f8e94d207b99effa6b4e8eebf2a53797bd7764a50efdf05da41e15be3da16d9f906064d0c5a18b431e860d0c5a0074a460317833e069ebc9dc605c51c112d948107030c86188c30f031182bf828c1c14e5449ea5253bccb608a410c83b8babacce9b8a85ecd4f94e59afcc655d51eaa59f28d604aec05428a85c408a53632f683041f8a41523e3d832871211118224a9bf60b62fa3f396622bfeddfe70f84a77fa1bddf837337beba37230d0ab9e1415a0aab6e86f41d838cdd7160590c7218e431b4182ad905860186330ce7182e305c62188272dd702dbe74b8c1702ba79957ddbd7924a42ad7ba899ce2f7bc662746a51b25992473e2c5144944340e0f181e313c81cc1e89def082e115c31b86770c1f183e317c097ec337861f0c0bcac49cf0312c6258c2b08c6105c32a86350ceb1836306cdacf0ec316866d0c3b187631ec61e860e8e2feb20591680cfb18727e27180ee4b01b8f160d97f27088e1084336ce8e4decb66a437666ca43ead35b4efd1fc64cf12e97eb61dc66161171e35312d2910e9318a66c65e7deae72311ba623c3da4176bf6106c32c86390cf318598c8c72298e02bb9cabb6567853764924309ad9906bd1b51b12a206050493a5c0b3356f573261a32cafce750ba339460b8c96188518ad305a63b4c1688bd10ea33d46078c8e189d303a6374c1e88ad10da33b460f8c9e18bd307a63f4c1a8805111a312466590791e5531aa6154c7a8815113233ea58d5107a32e463d7361830e462e467d8c3c53829d3eb14963343089848d62e795fa25c6ce0c5580957f9139fe97ecf06f0e8be1fe0742fe4ac695201f8d9489cb614f7c8cc6f2589c9d140d47beac90c6712436b81eca38349a6034c52826d7ea47477c1c05d6515c8e66130e81b397949b0be98bc949b7394a91b3c328ad50a6116fcf6294fb72a1220c7f692059e2dc1a8113d1eab8d64328139aa25e4b8fbf20949e669437b9913d96153b4a10fec27949e632ffafb5303411087dfb0769fbaf6226625553bfcf1f253ce801fbb3022928c1fae4340315211a3f2c67c19fc19f4bc5e22f4832e02fcdb4033f84bf82bf86bf81bf95e1dedf8122bb7f807f847f827f867f817fb5dc86fd9b5d776d7da0882f47e906beec2838e9d39b9d0ea0226d7df84f90fbf7dff03ff00bf08bf04bf0cbf02bf0abf06bf0eb20a3bfad99061b6f8a39f179b4e1b33f5df8ecaa03dfc59b37b2350f3e5b1ec21fc1f7cd23097f0c7f029f4388c18fc34f6039b0d39369b2b5a424bd381b49d9296f4cdbcf027e067e167e0e7e1e636b820a19278c8dac9d958d7df6a004b02953ba621c603cc3788ef102e325c6a1ad14315e996e15f73ac66b9585b994a5ff1c6f30de62bcc398df0f181f313e617c36dba55d7fac125bf4ed658af1c5540a66e1607cb573de72a35c80f1dd7a37333fd8424546aa6088f103e327c62f8cdf187fc0ed79cce796302e63ccae5631ae61cc0e34306e62dcc2b88d7107e32ec6ec3c1b7731ee63ec613cc0786853bc7784b18ff118e309c6ec460ce3b85d3d2cf9a27102e324c6298cd3186730ce629cc3388f89555df4d156096fb31be51acc6d30313a8e43952e6ede0cf9ed09791115bdd267671f2544c437ae2262daad12ce930070eb7fdc4c7582c90ce51426734c163c883779904d969884b213ec2f8a309aac440049acaa93e89650a46f435971a05fb7574cd69890fc66c89762b2b5410e931d267b4c0e981cad136d1f9728f33a67986cd7a32981b054525a96c70d9313b1679abe703ef9d12a4cd88dab21b598dc947cf75f96a7ff3104fef4a55fed28267701af1146397fff58edffa59cf993e482bf7a2b334d4b2bc5bd78f2c0e4a990e16d1df26608e5ea4d61f436c0e483490193a296e9a404ee3dfd843c626b1c5805932a26358170c21b1b9834316961d256fdb7d44ebeb9f98e927c92e0045121c5e952eb7ec34176cc898fe862d2c3c4c1c4c5a48f09cff089434c4698f898f05d4e6c8c333b15e33a89d937a73e8e49029324267c67694c32986431e174e7ed6588a9aa152a43ef34c07486e9dcde6fca5a3c5d808f9e8698aeb88c143f4651adea81a2b632fa6ea4a09a6e411963bac7f420a191fb776680e911c46d6661c9d04ecfb2354f2f985e31e5f5777b2e986302d38794019ba709d61286c7e45eb68ac320d198be30e5970fa6054c8b9896ccbd0daeb01b37cb3ca615342ff29758ce31ad625ac3b48e6903d3a659e4306dd9510ad3b66d6730ed982e1be982a2e6d4c1d4c5b40f6e1fdcd1a61cf508531fd331a6134ca798c6308d63ca8e2531650b6965e09eb2912ca66c368f9845cc2006c4021027b139620bc496888588adeca26d3e23c4d626b3476c83d816b19d7279909cce3e88f1e401b1a3bcacd65dc44e889d11bbc87210b4edec291bc03ca7106a5bb3391bc552178c814492776445dc2b47bd94abfb9efe25083933021b5bbbfeb77889a24ff7fb5fa83b9288dd7eb54cf8992aea3c3fbf792872516e98f149c5eaa64ba5c6b82c10f6152d1dbb636dccb38cd8c3aca44310bd498ca5b6cdf1d7a7821bd3679207c45e8a3baf3d85912f2f2a4f4ff6ea4d78dbe747f96f64eafb1f03c43f5be05f106a87f8a2b9f33f914e3faaf85726243bdabd60db41ec23f12f56105c5f7dc48a88a98663949ae9a8d87e92bbfdd5d8a1ddf0b595cd892fa0825815b11a6275c41a6890bf6f8a42c69a88b5106b1b2224d6b133321b6fd5d6503288a83a921f996e251ff2b5716c3dc41cc45cc4fa32cdc73cc4064ae514637fb8087c013236b6c5186213c5d7c6a688f17b1cb104627c440ab1346219c4b23263c67288e5edfc80b805998b38642a8c0788cf109fa3a62cf92ef714d5dc7b21be546c0771be6e6a652493a0a8135fd9530bf135e21b25d29b1455129d0c707ccb1b65b6e14a8def14568f9bac05f13de207737dd85b1b716eb473c44fd65f444950f6267d028719bf207e95df76fc86f81df107e24fc45f765e42fc8df8c76cd97ec16eca51df8a88f37c19f10ae255c46b88d7ed8e5f1a4a6691ef21de948131ce4ef2891d9092c779d231230ed345bc8fb887f8403edcf121e223c4d9f818f109e253c46388c7114f802b299e429c5dca209e356b5e9623c76b97ec6d5ee9367a2524c8231bc57c29f6b2a164e289408a503211b7a19214b7e752d2a85a4b1d8999dd5ca4700e5a2ac247a64306f7bd0d6a3c940d24de17cb2a4fd18612939239133e37d64b49b3fafe93eee48ba8c88cacf4d0bf235424eaf465c99524e6d629a9ca1a09e05f62187d8992a7bcccf82e10fa0751c2c442aee793ab2a9151a2deb9482c9108915821b1d60ecb4d39b4fa62fd5fc57977f0cbc0f4a5845fb0ecaf486c1471df8faa2a1023caa6fdff82f007b14bef5b8ba22df50eefecc675b5c9ff151cffa47e8a5a279d4d6ccda9a8e2ec5c1f8f9e3e29d472c5f3a06c9a38445f8ed17192474f338dc459314d890b12572130719386a63443e28ec44320ac1dcd24ab9c14e14ec5a88a718250b55793a928ae22d270389c8527122f65c87aa69078db621f890f1205248a4870059491e08c5791a821c1a7701134916821d146a2834417891e120e129cd6beddb0939e12222606480ced3383c408444e628c04bb3a458217c4914820a100113bedda0aa7352506a9f455277464502af2591c206fcf2291b32456893c929692026557bbf194d27eaa7aa3480686437bb966d7467266df6924e7482e8443ef63c9232497a6c2cf10c915926bd1c0e406c92d923b24f74ab99b3c207944f284e419c98ba5ec9dbc9ab26b33bcfe86e41dc907924f245f48be91fc20c9168a687b385454bb225942b28c6405c9aa59f591648375241b4836916c21c95e7590ec22d943d241d24592d7b0ffecd210c911923e92632427484e918c21c90613585ccd9b434b826f8afc4592474605faf63dd514e03aaeec54fed159c9b563fa30f1b5bc7c9615256bec4191008594cdf6cdd157f4bd9742326b1e75ebbb0a4a7a7760da3f533ef94f1e04e1a5a4045c2aeedb128a3605394e38ed3f6969ff78fce72e5f7d099c3e71f8adf6f52b2ef47f4148f164bb92a031e784e46c2e507cf09c9bbe4f69d0bd7162f3d65122533deb1aa53bbaae456f7ca5b740ca4a1abcf415f4f7f52de311bb2265282be2f65460506bf3d721f4c7664671bd11d00c521125d4ce41e6e70f08bf9184bf78c22f08ffb2a3a51b52814c2e83819e3ab34ae80d231937355392d2d41c2f47476a81d412248cf521523ccfeeae65a5486d90da22b5130e537b398ba50e481d8de7dadb40b5a6d6be0a2f6727a65dd074bb9117c5f8a942ca0bf6f864cdda9a255267a42e485d95e13375932898ba23f540ea89d40ba937f605fbe2651fa40ae07e395f584a86a9125265a42a485591aa21c5bb1a204d4eb5902243b547aa835417a99ec9f102c79c1248b972039a1eb962e40d4850910cca493f6e3f1c481f290f29f681c31c21c5f5c4278e919a88d14d4d918a211547976d26643e4d25914a91df428a5391412a6beb4d297c5339a4f2485ba4556507e900e919d273a417482f910e915e21bd36e50fd21b2517486f91ded95719e93dd207a48fe69c000969fa8cf4c51eeb485f91be21cd2b1f483f917e21fd469ab717902e225d429af75690ae225d0385ae7403e926d22da4db487790ee22dd43da41da45ba8fb487f400e921d223a50c4efb488f15a591cd223d419adc6ce8de46caaa36f454cc66b641338e74cca4c87f8e15e2988ee3d411b97b77146168e3123d54357d2fc8dd57ca7047dc3a916fca24462650fe3194be48babf05796ccdf48c8a1f2a537004c2c7ed57ade9b4f90f47aa24d132df4dcb020c211a8150d8fbb2a07f0e514802354d39652deb573a696f135559da1c55f2299e433a8545d73a5d4524c52282a920fa97d0388e2a174532a1d851729184c35f15663a8d7406e92c412845ce7f4118e5fffda600fe1f10aa4fd7b2007deb2b41e2c3fd6579fa5a087f4d1f04c24989ccba2a487113226229fef2d84661c5e99c52e53f485417a2ab938bd2696f8d24dd741e198b8c518d93e1129940e6fbc50b991932736416c8f06468fd35322b6959336bd5d3498ce4a02467a5a3ec72990d325b7b599bf081ccce781950fcc81c9039227342e68ccc0599ab1227676ec8dc91e1654f643865231bf0d737321f640ac8149129215346a6824c559a924ccd6e262abc3aea61b7325ecdece2c8d4916920d3b4f79dbd79b652c302d69f2be42c4a7b814c84a84c1b990e325d647ac838c8b8b6c2fef7b14ad87768034fc58e3c472675d2c08c074a8c195e3042c647668ccc0499293231253bccf0a10964d8662a3ad2c86414cc9ec92243792963979cae3cb21659a35231d900d919b2736417c82e910d6595cdae905d23bb41766b1f31eb9d90dd21bb47f680ec1159fe7b46f682ec15d91bb277641fc83e917d21fb46f6836c01d922b22564cbc85690ad225b43b68e6c03d926b22d64dbc87690ed22db437f88f35438cc3ac8ba4a3097eddbb68a7ecac216a7046554bced58b6e45f966cc1c3b2276eb3dbe4268b7e8b2034cf834ab86557f23b0bf276f63017c7bd5da2fac751da0b1751d46f545a30be5715ce74ed0f3b1ae18d04e7915186befd3eca91b932dd28e440206cabeef290e4744b6225f35d3a1bddabea263a991d68ad222ac94684933dbe8772b9b60d8c468aa0cf0ea1ba779c2b528bc839c6c4901dab9a37e12dd1f4ad33b3a8d4c49774898c25c001ca83a5a5a4e35fecfdcdf6ebf83fab3ddfd8a5f7d38e8a46676302212531de2010fe4976f80f847c797193d8492b934d98ea587025f0be01167f7222f28cf44e1ad852599b38926cd2eeeff692500ab66c4a392fb2696433e0cb2308b33964f35076b38a2d7454ae2c37437b22f54c7622b4fb774b1241ea4f613767b099c91f2007e402f0ca4705b939720be496665307b19a5b817b4f6e83dcd61ed2c8ed90db237740ee086e7bb933721750b6cedd90bb23f740ee690d7f7d21f746ee43102257300f5e5c44ae845c19b90a6a436356661318eb996928aea933054965ae861c1fda40ae895c0bb936c8576f92c87590eb9a4906b91e5a063976d845ae8f9c87dc00b9a17db00f2353e1137de4c6c84d90638331e4e2ca704104e612c8b11d7ee110d84e16b99c52d1e4f2c85be40dc87fe403e467c8cf915f20bf443e447e85fcdaec9ac86f84c9fc16f91df27be40fc81f913f217f46fe62fb03e4afc8df90bf23ff1031cc3f917f21ff46fe837c01f922f225e4cbc85790e7af35e4ebc8379067cb2de4dbc87790efaafe6e31677715db4f9a471361de06eda8105a5b79fe3cf275dcfe3a72ac3b4e709f62d9b1d9b45cff7c76a387bc638f71b30a28169a90ffbac8f7cda92a67a078a0fcd64d19f445f74e256926fde7b7c08b9c66fe8050a2208197f708c29fa37fe9fe5b905c9ce47d62370a84c80fc4313e7a3f4d29e5c3094737447ea4ec4cd3a854fb7067ba59254d2c06721c75deb24c6ce696cccefd2d3552852b7f245f82faf88b793d2e92097f055efe4a70e410f3bef2b3e4c766f4fa29668e1b1c43939f107842a0e087c881e66ba298cc7e7ced76fff317fd8bbdffb51312dca6f89262860f20e719815003fe8651f1bbbedcbfc450bd9c908ebd55dafb7eb39709d883fc14f918f27131a5ec408d2c7e1a794abafc29897c4a6eecf9b4dd9cac9796b3424c117d0a6ca110dce65ac9589f8b298b7c0ef97c60ad3d71ef3807d60416367c5bef68d71d1b280285afd7d82e2a8b4086d87960f96519793aadece013d8355cdeb809ec36b0bbc0ee037b08ecd14c173f10c609efb29967026e0161dfee6f76b9976a61df0a2c6fbc04f61ad85ba0b08547609f817d05f66d43cf7c8e940f75fddbb31b5ef621f50e6c21e0ebb5a5c096035b41836bb717d86a606b81adc3b09d46609b8165e3edc012c6b1c07603cb6b9cc0bab69e0e6cdfbe95683ab083c00e033b0aac1fd87160270159c0f4d696f82fef8a073611d86420233aef62ffb3818aa7e6391d81515af7c00481217b340fcc2230cbc08401d160c86671a7d9066617987d600e813906e6149833255aca43013b69ee5ca181790686fcedd406bce56d8fbcf86392477bac04a6600b3ede79d34e9af062fd92fc5a136b4578045bd3bf2b15f2de0f0c577a2930657905b6c7a6cb75c37bab0a86de2e546d2e4c7f4307bfd6b9c86de3a518c244e4f37d5dff2be4acf45f561e23dfca255c7bdf35f9e872e336c3a3a03809a3220e3979754ee6fa895c25b7f5c94cf588ba2752c5c0d46455aa5ced391b98ba4b718ebc778393d3407b1098a65d72b0ad80926a46c8170f4c7eb09c1170228de69ffcf60b8230305152e0f1ceb8f3af47da5f7af843200f92c428e54c4409a7e59f7f4c36ae7c163f5ef43f7947ffca84e4c54f9cbe2e42be869ee85e14effb3d282373a801b9902f477aebc97bab9f46832faf1f686fe46086811905c6478b2f758c2957c384fb1e6afc2916680f4c286d96d38c2a576d70792a6bf264aaf005cb0591b4e506f758f3e0c5e94021a2597bda734fb5959558972157558ec493630d606dfb10c04473137c6b52c16f0458981345db651453b90ac8e3ab2ec4365022fb3ddeb700bceb1828dbee39c045c14d8bb33d261474bfa9057209e535f740f9c99e2ac6b4ee900606caa0f40ef0c1a51a2881475172a37ac5464a01ca012ac6f0826ae4ea5b0ffebfb2beab3975aee9f27e7ec6ecfb2e93a1e68a9c4432c970b79740420281102288aaf9efb31afb79c337a728978f2d6fedd4ddabb3e6a50d6c87d318c23aa63286c6c253e0e7a0c1b553686cde37a83152077b73c005ec52169e641c6405bb863a1f3750d7ca97d850aa137372c4e36239421eb600d5e0a95f5201e0f4081e6aa6d503999352217784ff5c6007ec010ff081034857088123700222e00c5c8018b80209700352e06ebc17f0009ec04b9eb70fcefcb4cd73721ae2e779b2d8f142cfd29b25ba39cde4d4d7dedaa86b547478559fb01fcfd23a90016f791e24cfcbc6f7d6b5cbe7e5f05795f40347818619dfb4b176c36a12c6ec9f465dbf9e86f5441d66bbf29fdfefa3167df4a392ba2b266ba0f92b09793f95084981bfd534d72352a05a6ec8cbd002da725d021d3b2a48ffa0b9d1b521af22d005b8697db970aa03506e37eb1ac2c557731c2d2df3714bf0dfafc4fbc051c021118aeb9bd997ea841f538bfef6f5fd4b84faf53f89d01e131d8514d8c87d729f5c750f8eccbf83b9ff81a3bada23cf66646673254222527e7cf7f72b3006b4f29429b6545b5d34b51c955733f15b8506a66633d73a885f1d60067c0373dbe7fa170017bf32f10fc0fffe508e91080909ec75a865a05a6560639db56ce766b0316968823940f2fd92e742c22990e3a1aa12128dd5c11872c0bcb6cef4de66220a78621e640128022580a355645b05aadaf0f7cc6f6a20eb720dc86afa815e4bd785bb83bb87eb81fb987f483a93fadc48495c62811fb807b87c32847b847b821bc13ddbfdcc6cbfe15ee0c670af7013b837b8a96de5e1dee13ee03ee1bee06670df70eb701b70d50708b705b70db70397f8960ff7e0f6e10ee00ee1f2b7236da6eb5dece1a981699332dcb13c39b709dc29dc195cbe742e35d7387c7e01770977659e9cd2da1cbb7039db0ddc2ddc2fb839b81cbf60575c6c116e092e47abc0adc2ad6127d81968456f12a68bdd0ebb3d761e763e7607ec02ec42ec8ed89db08b4c30c4ee8cdd457a77ec62ecaea65815629fa34bcdca484eb9e77c2cee496b3d5d37a655b227ab27ebbe240a487e56de449e525f690251d2211f967c4373b2dc54b691263d14785e0d0def2e6f940209024392714fad62d159a51cbf52246e3ddbf8c22e21a5515d32db06763735557cddb43636353759fff6d8d37a45cdf4cfde4ee95aaad9f5439b1c365bd8a5d871210fec9ef2138acd517463f7c22ec3ee8d5d1d3b0edbc48e4fb60df9d4aea312381fabd4d975956a482c04a5fc8c732407ec38cf83b87b35ccbcbeff430c1654066aa5990f117efcf8bf4d42f7ffd53fede3a6fffb5e9df5f1af9cc4ae6f723c7e2d788cdde0d37674ad32f2438afc1e3b1e0c457f5de65f8a10e6078da25c27c6afd9e599b013bb117663ec26d84db19b61f78ddddc46dcc1855d71cca58917a654c16e45b5509329d7df96e4bae3c83f32e72dd98877c56e8bdd97d971b41c4523767933cf51edc4ae20184b749ca5449504719c70719676c986b12b913cec98978c6fe1f855ec6ad88b028fbdd16eb694047b177b32efae7204b335f9ae1daeece682fddef827eaab76f185bd87bd8ffd01fb00fb10fb23f627ec23eccf261c9a70803d9f8f6517cabd8dfd15fbc476e7d8df84b7799fdaf252623e73c7fe81fd13fb17f619f66f5be6c87545a4fb06f64dec5bd8f3cf3bd877b1efd947442c6a891ef77d0df0e795f59e9277a5b0c37e2029df32c4dec17e2414b6fb31f613eca7da5ae835931cff3b3327bee81b7bce6481fd12fb15f66bec7fb0df60bfc59e6fcf619fc7be807d11fb12f665ecb93355ec6bf0049e3a5c55527a2ebc1dbc3d3c0f9e0fef002f907957737ff2b13676cfee76faa3f1e8d3cc50f67aa1069acc8cd6d8254ef68ef04eda11355fb4b9a3acb5f8aa3a0c93ae64a9fa0f8385ccd726f635fa2c3fa1de002fb2c52f8d59d38cde3a15727867cd63a8f29b8b1605d62e316b25b376491b4b5e169662a0d9e35778b13ad98d55957279fafcd543aa21bcab5df4655584c79b39807783974a9cf0bec2bbcb7743be7bf01ef2eca99934773104ffde93b7ce5c1af68b6b7fc1cb40fe4ea4e0d5ed910f374878f09a7fbadb3b470809af25257e6d1baa5dc3f0df9d9828fafe2142a5c32020b9fd8b08f15f44f8afd2a3ffd9a4e91298fe135e479b2b54b66a935d2c6cab04afcb0f690f5ecf063e05a05a6f33ee42df74af665c90fa9772166fa00d2a22eec250efb1e710974abe008f9449fe4d76bb843796c641bb813fbb5229ca937bcd71a6f066a63584f70d6f6e0e11bc05954c787c7e056f6dcb7b69a4b26e6bc54859c865ab5948f599240ebc1fc966f036769a87b785c7abc0fdca4bc2f7f2c393e0fccb1471f07869aa066f78355b7d93f6f4e38bd914b552b8fb2d8d10bed14272ed8df69fe615a1f0f777f0f99867b5ae800fff003f90f6d69c6af0f9fcd138655ba9c23fc18fe09fe15fe0c7723ac22724cbc34fe0df6c9503a6f0efa0ea489cefbf542afa19fc37fc3afc86a9564c3f07bf295e4fab68567cc1689666943cf05b525cc16fc3e79f7465c76f7a94f3422dccef8397cc1fc277e08fe08fe14fe04fe1cfe07fc39fc35fc05fc2e79fafe1ffc0e734b6f0bfe0f35d9c5b017e117e097e197e053e5751c3417030385839de14ba1e5c1c76a6f3927ad7769794cf9a0aa481071d8d832d54ecf5a8ad5aa42bdb070e6478171c3cb3db6ac1e86c2697370ebeb9bd742d4fdf5c1c1c0ec6d9aaf1ef79572b6b618b43a0b598c781363c4b9aa422ed8b56d496075aa29624ea3c2c61617ea1950ac8859f6d0a4c1c42d9f6645ec4e188c3c9ac5ea6d1c121323f9906546cd6c4b4389c71e07c62d5f00ffc24ba4507ae2b353dea7b5b7bd8cc528d3517b7a11d38736b59dd71e0e7a1dd8736076b0a203ca9943e31a85fbf45097178a969334ad5a7ff5db097f96f99c37f19487fc9ef8f08b5c2fdf49f006e17f6ffab7af8711246ffce2a7c7f297a3e641abcb2bf6ad8de6386c3db6e121ceab642d8cd35344cb189033f2d6b1f5ae23e6ae3c00fb7a08b430f873e0e031c863870c747388c3526bbbdb73755bd709808a9f7c0833c1146da21cf9bafe0c2e6382c70588ab7374241ba30a71c0e2bc9be70581b0ad803b523a3aeb9e944a6792db4e1ec49932421a1983b68c7391c383e9fe71fe671e0f615257ce250c2a18c4305872a0e3504622bfc4ac4616db5096dfed8d0b2ff4e209e43666f3a15b3de237011eccc6eca0f82bd79ad11783617ea4a031fc10101f5cc27029e6501c111c149fc92dc8b721404118233820b82d88e5b08ae081204370429823b820702fee10b4186e08da08ea081a069272533094c61a6010364fc152d13c82ba2468e8083f0bd1d045d043d1bcf11f4110cc436e5c87186081ce937a93621182118239820e0b46708be41601f2c102c11ac1070a81f041b045b045f0872209b0838ff2282128232820a82aad439ff9af0863d5f08455c6d0d8ad0aa2f877b55ff56ef7c56d432c4d311b54bf3b69fae8c03e133bd97a45384ae62163e90f411ee280367e99ba4a8cd33822325a72568a93e0cd150f7649a5584fb59da41e821f4350d3731245a68e5db971683df5e0d15e0f020a7a1bde610060843f52af7a9db1e119e1046da74ed2336ccad85f08cf082303693a5ad7bbcef08af0813843784a98c1d847729d6655bd7cedb541abd3e5f67ab13840ff0c2845c75260ebf79137c22ac9bacfa97c7e47f9cea9505c286ea87a44a9bb3bbea9fbffd1f49f86f9df0d73ce39a3f49f88759ff9d52f8f90315a3ee5f52c547d41ae7d353eecc75363551400856c66631d1c238abb5f87b7132842d33db19592a5a200ba4523419206c23e43e7611f61072ebf99321422e78a45690708c908b9c2aea7b173eed54df3c42991410ce107e239c23a474e37e2d8df0e115c2b506a6d6db2a859212c21f69376daf268d8bd20ce549b8119797602b11dff8853067e7bedcb9413cb082cc5b32dd4916232caafc7cf042548cadcdd20da5a2ad4dadc7b3e7b06584158455c95f8c5fb0e3891a00c31a484bbc2de9b73c4786aba66c3f5ab56550fa1d5d1c7766cdaf7b6ab0387ae41d38fa381e700c70e403471c4f62f97d04827c4297638ce315c704142fc714c73b8e0f1c9f38bea4ef9addcd94be70ccb480677eab5dc4b72f35536b6cd755a313ab7cf28d631dc7068e4d1c5b365fc2b18d6307d40989978e7d1c07728d6c061c395507c7118e63a1a6779ce038c57186e3378e731c173872b62b1cd738fee0c8f96f71e4db7338e6712ce058c491839771acc84f40486c7eb89caac29363cdcc673889929366879655d6957c09df1f081d989c67c675adcd13180923334bccb8aba4a8e46a543f844392d086dbbb8ab88e9d73878f9aec97bcec74839331184935534dde3bf186d8f944056f3ac7c9aa15e9e4e2b4c369af852a8b659c3c9c7c9c0e5a90c29b29115a5f8d1a1acb95d73080fc174e81941a42d67c0a4178723ae114e174c689b7e825f34c0b992f78ed9694e1427c7e8ae5ab8ed315276d328d133fa93d066a10facde8edf38d77b5cdfa465b2976083eef7f26d30f4de1f4f8731292fc8a56ad30ff9684ffe413fecfd8d18f771f27ad81af54def63f9508b666ee990af7e5657677d5a34edc9aaadc33739a9972641430bf71aaababa7ba32d4b84e0d9c9a38b5706ae3d4c1897fd2c3a98fd34021dc69f8f938b20e0847b5de8e7b5754e372b5239cc6384d709a12c3e0349327392b47fb5635f2c40358482332b8925789e3cb7aa06ec927475b8ac30d5de1b456da38fde0c4096f71e28ee7b4b7f6706fa81c9ef23815d435dc87464ecd665a4f96a2f554c4a984135f51113f50bf563ad260bad597f12b369ee254d55480be31bb86f93adb88c75f4324bf15394c4822ef5029b2d3be89ba6a658c5cf099688fc843e4233a200a40701e1d119d4060199d115d10c588ae881244374429a8eb52e55e96cc7ca42fda0ed54f63be103dcd6368de9f58e7688ce865e315a20cd11b511ddc9fa8695a1cb0052291a863bd81ec06d099f410f5110d6cd995a327ef26a2212207d1083ace04d114d10cd1b734398139a205a225220ebe46f48368630c07df829a7c944394d79e84195754404464d1252ea51e6e372371f98725733fcbe49ba723e18d581a515952beae82a88aa866bf97388b16da9b182d10dcbde06cb4e7573ed5decf95aa9ad6bef61a2051dcaa5fb13e07c50141071116b5358da8fed1bef3b9bae6796e490f37b5e69e5d9c77367f36bc99e7bdc99f70f6ccb6a3d8783eb5dd1dcebe362d2b4e3926ce0750a89c439c8f38f3c908e733ce172951d21c0cc5d86321edb74602139e9c639caf32e63309ce374bbdfa9ce2ac698a6a922511126e9cee383f54e2b5c6a6c4f93cd59947f2d1eaa315b5dfa80fe3f3d1eed7c17f1866fe29fbfb5f05d788a9ce7c71f6f96f592da84d5f6dc6e36fd9deb86b38bfed74abd5f59e37d98654760deff4b9ae4d89934c8de7eb02ce0d4335667fc5b969880ccf2d9cdb76bd354fee7807676e53d3b666dacd62e19b4d7e96b69408a7d07a21090fa987731fe701ce439c1d932b09b8b091397097f3667a532e781eab75fb3c119ec079aad4e85202b7e5d2c49944ebe0fc8d33cf6f8133075c89d7d48200cdb746c32d3dad56408cb759dbefb5c97f7f8a0ef114d738738b37386fa9ede0fc2591ab2d2f73556d804ef0f3cf478fb6420dc1e09cb39522cedc8182c94e5a8fbdb2d18616bc73e7a2bcb893259ccb38f3bf559c6ba6d9e5cee142266bd50b6067b8b84a8a971d2e7b5c3c5c7c5c0ec27b3cff51a7c9a305aae597cd5f20f257ddaca79f22b99991272ea186ec05355c8eb89c7089e459905213174d7ec5e5a2ea2279df25c6e58a4b82cb0d172dd282cb1d97072e1ce1854b06ea69973a2e0d70f72e7c631b978e14425cba2659e0d2331175d1392e7de3f3b7035c8656e652727071ccec26a58ef16ae25fcd11129147f340a9db0f65fe71515ca9d10ca45821e637f52a2e23b57b934f9159e7de9a75b17785423eea7ff2691a1abfb2c9f444fa4fd5fa90a96fc3559647596756151509ea8bbecaf6aeb9451a354ab4bac4658ccbc4ac87b84cf98d5efd382fd14eb4d31b192bb77766767d2a7ab87ce3c2e57069fcab152e6b5c7e70d948dac7652bfe433fb9b25047b87ce1929319b7252f156e600197a2fa454a15ad67f15b97508bf44ea92b9208f55e9daaa4255b54f79eea8a76f749eadd7ff2ebff81a37fa94cbf44f83f24e16f601bc5e574f96928035c4ab89471a9f043b18e0b37b1a676ad75d3442dc9bf248c6d015a56c459698c8b36fa3c6bd12bf230778258101bc416316c7e257e13b18b78679efcba37db2962ef5712ced2b64a8fd8976d84f88038401c223e223e514747cc1f9e115f081d6d8d58857f18cbf4cbe4fa88af88132d98b5164d4d9c9710dfa4dd409c22be237e207e4af6a345693d8ef6e2717eac6a572ddefa187fa2f0c6267c23e6c1e7a8d9033993238ae60cdf9f722656eb08556666956a776e7eb3be528a6ae562674a62b3a4a8b84e8dd44c7dc40debeb0d43dc94b5238fbc90b0e316e236e20ee2aeb9bf10f710f7157cc6838f376e88d8413c423c46cc1d9b4abbccfd914a5d035196a7bfce399acca91f35d38f23d31ae96d8b6788bf11cf4d18235e205edae5504890f14ad6df8aa8a96be5e788d7b6ea21fe41bc41bc45fc256434710e711e710171113137ad8cb882b88ab8461986abe20c75135e5d5c77b8ee71f570f5713de01ae01a1a73c3f588eb09d78fc3368bb5163b714dbb2ed9923f548dee7ac6f5228143e420cf9c9121aeb1c96bdd7be5e9a527ae573ba79ad0d4e0a4d540cffd9a68fbebfc5afbbddc3d638b1af891b842ddecca37a6a206c931ae774d92f87cd4f3ce6f226e0b01e15ddb57ae969a6d68b4eb1eaedce7b721eeb83ea5fee68610615ab78eeb0bd70cd737aefcbe816b13d79619ade4f925e04adb661fe3da116f8d6b57c8b5bff94c0fd7be75125c07eaa2fcb969296e0ac3df02144a69ae56e2a8918f789a66e47303b9e44f085b97db38c295331fdbdafaafeee85fccdbff6f95f9ad06c57135e4475d9cb84e3462a85651386ab9aa29ae33adf51df284be4d2927adb6015732d7b63b5bef53af5f3469c5e1692d34428d30e6ca8359e1ca25fde0ba31d3d46c2ab6c5f3dee2aafe7a095f54b934b6b0148a7b01b5ed6b1ed782958b19a4b816712d496ccd28674e2349be712d6ba13759e0ca295535959e42fefca199325f574322d2dc2131482c12eabaae754a76e2f1668bb3b4e93712f7df114284f5c94e998ef6cacc907848fc597a40729060a5f5643f7d98f543b8922f114d5146911a910486749e84488ee2f35dda5f0d49a4ad0e4717533922392bfe492e4862247c3e41724392826a0338b1876a86c9d3def8cc0b4966786992b72d16b4be50e9a1da38b14679f3a9fb9068307dfaa901f1c96db39515923a920692a686ef242d246dd9c5860c3ee9688a5023d4e882a4ab6106494f9d28491fc940aa6324437b282071908c90f0bf1390b52533706f9339280009499295925fb246f2836423d4e012aeee4b0a0da1d4dd89b43b1480487248f212f04f0ae293fcfab3b4ae9a9b7fb04ff518695bdfb5f3813ca1d620cc3f8daced8c7b72307e0f92980197504452b28b3292b26c1d6950dd1a684fec964365d296dfa0024f6df9bbac452bc2f7bf42d83edf38eacc5870902a65117522c5a85e17a66b7eb88a1aa52e6e84530637aba1073717b79df10eb8ed71f370f3713b98dd0eb700b7504647dc8e421ca18da833dcc80747b845b89d71bbe016ab5de746097cc52df9cb69e22d523ba6abe227e69837edfdbe1ee396fe15c3272efd4d57e2e763a1f910e15f94b7f923423ec17fbf495044a1b7960d5c3375643bc0ed4ea4ab591407ad7d88db03b7a73d37405d9c5ac76465b75c46a6408242667a50b0445991f4a5c449bf4de8c9fd8e5bdd341e6ac638b7716bc085d653aa7cc9f6aca8156b2d75e176b47f757856bfd0f644f0ad657366d68e8a52afe1d6149f9369c9fda42ae58de374e452163f549f6f78d1363d54d89e94a81d33d8c8b4885b17b71e6e7ddc061a1a4f85c1799b6588db1037e71370a81f0d7c7532db2aabc98b0ab7cfd98eec6e86db187bb1a54f8fd8e34ecbfe14679664739bd8695d9b54868125abbb4d65c1a325066e936d9bda50e5677ea5197123c7eec89bf6da7ea4f6c6ed1bb7396edcc3256e2bdcd6b8fde0b6c16d8bdb176e39dcf2b81524e457ae45d41cbafc2dc1f229bc395b513f5447f66c63beb88a92958179709e65dc2ab85571ab2115a164d37224d0ac059f3fb1484125ccc413bb9f89792275c54e91ee90ee917a487d90dda401d210e911e9c9500ca611d233d20bd218e9156982f48634457ab7133efc40fa343b474c43fc582e75bba898f05b3395d29c7edc87362d73888d3dedcab86e4a3bd598fe7343bd08dba53ae2d29756646a1cc8a3c5b8e075c79452483bb19f964a8ac9de34baa6dc52849f1fa8ef010da5e175f8f1c5ff1711daf5c32c8f14b39493da4e30fd56ff999afa32a45ac81469036953721ba1b468e690b690b641719d7691f690f6cdb34e0d82248794e0792b017f3b94558f70c6983b5207e908e918e944f21c6aaae12ef94823be063f1a3a96cec09726a95685252b49e7b6f3a345e56b390d737b4d71d08033a40bb5b9fce613fe194cff5113358c9b04edbb7a17d7230d5147a4c17bbf9d03969eeedaa9aa614ac5c92c9d6885532d78ce635bced2ab4c88f778962b2ad012f108d7487f885270fad678cb8027bd91d149ee751e1bd22dd2af8f17ee048dad59a82474a9dc93f11fed46909223641a383be2b2f3a66354d72a5a59776cb52dd1d3d8a5d915259caaad92d826efaa7dbc37517f7d91d7ab8094179192f32c396e7759638e79a7db255bf9d1b6c61b57c32cb847bf1f9e22f10cbf19f3c255a01d4bc79496ea59ea9eb409494eed31925f6889fbefa106732fe7dafe2a1d5173435ad36e301868725ad0936ca081ffa59e465de5dee2d5d4687e17fb13e36e34387bb606a5e01db8bbb8ef70df83e8e5f5c4ddc7fd807b807b88fb516f1589f0b285a9126ba904ae395a8aaeffd4aa53db25eebc7f09ee11ee67dc2fb8c76a20bd5f4df6ad5417ae8ce1af12137671bfe19e82acf0fec0fd69cb631015df33dcdfb8d7716fe0dec4bd05cac97b07773edf531e77ef4b58c17d80fb107707f711eefcc389740cee53139f66696ec61d08266ae47c76d5e73eee0ae6dac4aabeb2df7d6b7e34867b231a22e39fa93dced299296a5d40f316e59bce532adf928c2414dc67b2f370ffc69d6c6b344bcf1a541d36149a0aa7b450cfa1a355ed1497716fb593ecf8efebb68ffbd246a29b169d944325dcd895091fda8e7643d589778fa7b096cad81c121971f93fb86f70dfe2fe857b4e7eb8ffd4fae612eca5b494c55efbac808f15ecf16ec8a4ee45dcb9bd65dc2b9a8a3dd3e2bf9acdf4ce5121c4bd6a6a1bf37034dc7c52d1362da78576a421ffedf17c6b387ff2f11f8e7e6ac57f1ac2fc996b76bff5baf1100dc023656f5f263adbddf4aff4607eabc13e2d42a08a2a6ced40ae05717b7818932f6880ee03da5582347059683c914f1d9a32732bebb98a4def93324b29ff70f1d8e1b137d1cae069d75fe25ff85b3c3c3c7c1221754275517821775ff3803046d09262198f031efcf3508bab5f323c8e861a4ed9c7e3844784c779960ef1b8a8657cb137e59d19d4295d6d7d6f8b7c269644f0b82a3204c594667c7e4a24685592bf2c4f0d2974342a9f287fb6c023519877d77a028a28c880160d2dab1c2eb4e890368bdf6b8a6aa122dab30e4a1b1438f992f5eb5299a88f2bbfd1feeced925a350b791337345b349be171c323c5e38ec7038f271e2f3cb89c371e753c1a7834f168e1d1c6a3a38e352f35b340abad50126ed65a829a2ffa3aa81beac13de99a1157cd53e8e331d0a00582a5077fe2e031c2638cc7048fa9b609a97cdb780d4acb87e673e0c1cf0297d4eebff158e2b1c283bfe5296cf0d8e2f18547cefa793cf238f5444f8d475cc4a3047d690597b939f31cab7810ed13f91394363556e929b27d4b7daab6b14b43c30c8f7b49ec8c9293c073fad62acfb522b50db95478f8a04888438d26eb3fad47f47bb455b1668ea76b4e25e21dc9f33244da3c2f57d4e8ed9fa7163e939c240d2d73be7da9fd73dbff57df2e7e83e74e6bfce4cfb66bd570bd39e0b997e7dd6c56aa6b3c3d4425794e8dd395295fc49f1c0c05e033c033c4f388e749ce053c23637c3911918d659a4ab569762d699d25e0335ce645167d3c63992c0d4ff3f9a952bfb9e299e079339b0ed0d44eddde4bf22dad129a6ed48e4d61b87d6992031f7eccf453c9d9e0f01b3b7afd8b1dade4d48cf34b84b3bd2936b4a2fba6f169a1f8a942b74e3fedc57f0433623fed8f313dd9ee8a128fd702cfd48e7f280a54f7bd702feeaa1e50152148bb24b238d80997fac0f389e70bcf0ccfb79210457fb8c1b3816713cf966af60d5f6341b3224f14c475a308cf0e9e5d258099767855461873909e4e204ad47af9ecdb37071f187fa2b268fd8db3afd68874a5d1301293621515b7b52b889a407f830949815f75edab5a5b68d2e71f2b7da9f05f8fb473ba99999c1a693e2653adcfafc9fedd58ed014f4dc01127337b8b43a65df2b6be067610036f0353de9a550587cb6f0680c63d5290668e1df2da39788ef0e408131cb90f533c67787ee3c90bb1c0936b59e1b9c6f307cf8df4f8e4566d8073ff93b013e9f7e5851635ed3e38cf4fe59badb91dd4165779e3f96588729f393cf3aa13526f791670b9697d83dd56ee67d3b9da80b79f7b5bc2b3ac9e9227ff5bc5b3066ae22f839725d3c7cbc56b87d71e2f0f2fdf905a5e07bc02bc42bc8e789df08af03ac36f8b3f2512c1eba2b521d2ad26a1539b700e26b7c42bc6ebfa0b47b51a1a7a083960224fdf06c448e4865dd3bd6873dc7c8443913ba9fade2e8fd70daf14afbbac29397bb660350cf5eeab093aa4be0d3b9e8077806c6eb7c1ebf101a2ff264285366372ea832a444b4d2e5765c174f07af2f69308a5b2553d3670b4ab21d956bf2fc504af175e195e6f731e99f2d944df5a2fe75451d774ae89571d2fe2e72fa083fd14afa6311d090ed26bc893bc3ed57cdfe643b3db5f534d32f49f9f1e893b355862a8a9799b979a8e785dd572935767c6aba571a77f7ec24f37194dc9e5145f6dbc3a36e611ee7e2fa5dece0fd4962d3937f41612914e8cc915f0224cff328bd0acb9809e46b88f031b4f4dbb60665ffa565ed360a367e08412f3ad7dbc06780df172f01ae135c66b62a2a6bdbfa5c15d986a2f8720d2b0cfc3941c5483660a55bc66787dab2f78c67b30c76b81170f78a50db11e251bf0a4d778fde0c5f3d8aa5b490b9e6bfe005e5f543648cf7811e23635b594ec93b444c639db6b0a2515bf38af11f72a0ffdbf12095c35eff764a8d0b7b2d108acf50e075fa64f1574abb24ab93994f65e7909b970a214a3fd6293965a4a232b79cea768268eb6688f9b5ae2d61dab9aea521af4f02ae155c6ab821797564326c88c3daf9059cd28c85c35d5643b647b649e59d5c5196ad803b9c6b6a350795646e62b425e0f7f9b8a696edbdc27efa78ea0a900c499d90159802c44764476421621a35848905d6c953f8c915d0d45689f43f18737647ce31d19fff089ec852c43f6465647d640d644d642d646d641d6257d21eb992ba7da4736403644e618ff21ee8fb68ecdc6ea192a4e55187a91d4179f3da7843cc9b46ba1c1d9a67b33b344bdf9fe45ea3ff6ea231b697a55712ce9d05279e1203cd31bb7622249916a8bbdfc907b229b6a628db65b3bab32d970052364334d56f89496f9a795a5435183d34d81dbb666bea86471aaa4a8a23e90f1d1bcf824efb77da9ff06d99c44886c816c898ca7b036f51fb5df263369ecb41d72be2f754a0231be95b42fbab73fc836921999b4255c6804ec6ea47d11673f2445645bde280d6d1ddfc4b7d68eb4764ef6e9c1588db51c14c5a0eb910ef5c9cabf9cf5f9583f9bbd7ea55808fcdfdc5ccd8ed732015a1440990d457ff6a5d504761b335a28d05ae5c9e3952c7ff3be7239e53d545a2a336a6e1a5195e5781791f17dfcad56f8913c1750445642c61b5011c737f399cd527be2daaac86a52ff261c55228cda5275a50a2b73bcc58c36d2aa9bbd48fb4ed5126f83b7c59bdfb866bec07b67fa0da330f8a165cedc29de7b824fbc3dbc7d4973781fd441aff5bc0a4a81c9b7a6ba900293ef4f06dafd53fea0a100f54a4cee6bb9cbef9c5d468aacb62dbc03bc43cda5e0abe393b40f828ab4db4282890fda838e0a7071a61cfa7dd45c016ad1858040510b1f45d64e07da663d38e24d41d1c63bc2fb8cf705ef18da953ab321bf4ff0bee19de27dc7fb81f7d33e67a64d3814a8e6f35b1ca5aac9cd78bf34433c7c2b4e264dbe336591f38d4632bddf9a05f2aee3ddc0bb89774bdd866fbeb163e2a3313bbcbb78f7ec9e3bd6c77b80f7106f07ef11de1c9693e7becdf0fec69b7bce679678aff05edb2a67f583f706ef2dde5f78733ff37817f0a6f8e2bb4a78973938d5018d8f499edc138d310a9bda2989e0b9efe25d91eac976b42880243be58cef2a35055842ca3ddea416dfec21d9197541dda06eed35b65f65d4a137a47c205326dc55add23bca74803aaff3da843310ec6c5f9ffbd9f85455ebab39835c35a97c8e7b21d5d47656a8efd4657f6ba1be37fb9dacf98ed0d41fd28f65c7df7a66df41dd47fd60ea05714ef65896e888eb59bc58bb71e62f429d3c3ca0cee3708c1c510f513f9aa345fda4cad4f5cb0ed71271f29186707edd4826e2fa944c5a779c6269f9f1846dae66b9e3c7067b9d27efe12f1c45fdacd4592aa9bdde596b3c9a16d2f0f5766e9bca802931ae3bcd1ca95ecdada9aeed3c5150deeeaa6ab0a14664678a577354e1265a429b8b2fb7d415fef3325a0ba067a8dd6ac5c82feea6b634b9e6cc51340489e49dbf918f4a38d4b638d945333b236a774f8d75225b7df651bf98dc16de48ad67bd1fd4637256d4af52e595aaa29e6880a8bb95765f9d1ff59be637d453c3fb5dbfa3fe40fd89fa0bf58c5cc0fa25d4b5e6a4f21aae9f444888ff6a7f0a576afd1f9b54255793624d2b3d2f384e5d7893361b509fac370c06382d4c78d7349c76d1066b59e5b0a346dea75826b25282f127a837b50cc75ab443d0a26ec617cd97c9e535a9eaf2adf6e13aaf421b759e7a17f51eea5c236fd5107507f511ea63d439c814f519eaa4e7aed9d6354b73dbf9cd435536bf6d7efcd1632a0b84a9fa51089daa452e4ab97cd4e7a82f50e786ac6cc6b7ac51e7d66d1409d7b7a87f2928ade750cfa35e40bd883a7786379e3fe496d6d01070b90d2b8f040da0e1a2b143638f8667db5fd2ecc9bd23bb071a3e1a0743ae147524a9e87dcdaed47fd4d012eed58590ab6b1b892ffe616082adacbe6deb938f1fded475e16e416da57144e3a40523a289dcdf8600a111c9916f24dcdda91181d0b47151cbc2f4619ebc1b776d2f511a287fccc66670d7ca85eb4f96a0b2d18e7a98b4bbfbcd7a3334287cde685c158b6e35d1018d44dca5e44ad4d6ccac2fa3261a371094ce6af25d47e38ec643cb6461a298ae52b366aca1978793041df17ca99e655f93eb44fa9cf653725f68bccc702fd52f85b8cf54fb91ac876864b84cad03f1aa66f856ead042dd47d5efd69a92af616ec7cb27e2fa2309d5c84bfeb12bab0427cdf846c10fb59d5a058db73a85338dde31e3923afd924c533c6acec7531c6a74e8dd37da77dab7a8f1b6e90110014b178dbacd1fd580e112905cd1506d4a1105f98acfadec9af9a7f3b673c16969af7c5eab9a9bc30569417b74f0b134670f5b1c79842d34dad4da3f31f55af76e965ed0e8a81dd29f98b86ddf5313cc85536df6d51491b4d1e0803d538c2901a8314a453b1f70fd7a421b5eacfe1f111278bfda7fb5b76e81caf92f57c3c11e7ce340a31cf3bc01dc53c7deeb6adad52ee75334c6684cd098dae59726e050ff7679fb032d05908dc4bba92e3a9f6a4e1d99d18708b9277a63a2011abc19df68ccd158c830436389c60a8d351a3f686cd0e0bde4b9e6d0c8a35140a3482d48274c184d60a225c6de663b20f9693024951f9ed438c179afdf771f5a19bdc6e995d028a3c1e3e385ae8118b269d0b4ea9368024d17cd1d9a7b343d347d340f68066886681ed13ca119a17946f382668ce615cd04cd1b9a299a77341f687284179a199a04277cb86e6f2dc9785e5c75aa59c4d7a1c66cbc7b8a1b2b7562257bb3d25bcb946f216557ac37579df0de0395db3d15f58d9c39c3169e56cdaa2dbebdad7a8a7bb26ea876efd8088168a6ce0f2d5b9a7f736fb544edae22f5b5aca7a650d41ad86b8db2d00c7a7ea8e7fb9f969de348ee5c66c7863934bbaaf957f84d4f2a3941a84d353dfeb62f8131c8a339407388a683e6481d9ecd319a133373cdaa866b6a9098f5404e1df5198e3c9c0f32e28aa666709069519e4b294d2906b47455146a7c4f6d6d662b3467327da9fa166a058d4f9f694fefd8235549f89aff07116a51b3ad295bb5bfe73e5ea9cb5613026b5534bfd593d69c9be25171663bd4109984c2caa2b9d00afe54159a4b8db7b49fdce1b1d66cd47ad2c4ca4551f162a089d5971fd3bd6aefeb5a55c359a8815c0be21f29c1d57a966fa2b9b21b1eaaf6e29086af6a40820f2e5da2f983e606cd2d9abfbe10de959c99f1dee4cddcdaf64691a73566ced916ec99d892132ea25942b38c6605cdaaf85d346b9a74ef5d3e9d6dc636ce7d9040f82b0c29e7fffc1033ab52cb5688924c99a0ab2a514381dca0411d5e224dea57333a9e1a18d0326601c59fd452221f2d6b6a43dbc85b39ab4008af1ad2119655275c3f282405c759fad61cee968bd60ead3d5a1e5abe71bb681dd00a64fc8d5688d611ad13781d5b67b42ea6542305aa24a47e8e9a293695ad0e56a6f954e9476d70d330b232b9549b05e46a68c58a1dca315a57b412b46e68a5881a4280ba4ff1e204ee683dd07aa2f5422b438b3fa9a3c50536d16aa1d546ab8316a7d443ab8fd600ad215a0e5a23b4c6684dd09aa235438b539da3b590e70f5a4b899e72699a812e59f299196d35fa97f7e4b256401439b2be08918edbe32648c0fbd0d40fa6b2ee695e283558f958c8ef6bb973e62bfbe034d668fdc89a8394d0da68f429d5192d5b6ad1daaa1b3958a0f5a55982bf86196a5cff94335421b34934c221694af1db14f3863a05c62a095b3913954de6a9a1ee9a97a08917d79e9705175540ab28a78be4db76cc7595d4bd54c86ba99456595096e9b7e96472ea499b5bc70b5095d65baa4d8d9de64c9c507bdd3a13123f5a35f588c85c63366b8ec6eb90115262ff63c8fd0bf87eccfee0a8d6aea15e478e726be0bef8346a6c7e8a91eeb47cc4f2aa1734d46654aa10fb9e501b0b8b2a58c679715eba9bb9a2297ce15252225c9f15952d0e26592b32761ef02692fe6885b2414e92baf6033d2db5f20aae5a7e66353686d85ad0d652d45605e04a8521994af8307e156d8b368c9cecb94686cacf2c3d98b26bdb63ed3ed78dcd66681f45b45db477663b477b8fb687b68ff60154e8db21da47b44f246cd3e15091a65f118ec6793567fd1221850c97ccc94f7507247314c1b6cfeae06e5f4cc41162d976a5c2b3efa8e948634d72685fd517a4e1da0fd552da891ef9ba8936c79973102a4594a81abeb44a0956c50f290ad04ed1bea3fd40fb099e653b43fb8d761ded06da4d19f26b0bed36da1db4bb68f740987de003834f8db04861db3252bbe82f05ce7d9b7e8aace452333f48e98af69097156d07ed11da6344990923b427667a417b8af60ced6fb4b94b0bb49768afd05ea3fd03f2b2f616ed2fb4b9ae3cda05b4b9a525b4cb6873ce3c851a3a0ab5ec60076e6307e8b8e8ecc8fbecbda1ea3d91733e907b2893cac7ee1248c3a35a314b632d74102cb56f697d28979749a62454553708711fb134c8a1f2c65f4ab294d3099dbd2697763cb3e68b7c843fc86a26dc6af3bcfd4db12842cb2b475dc06bcbd6d1de417f6efaf16f855fdbd86a31fccad60c1e0633cd6c4c3eb5b10764f1551c4274a8d28726dc0969665593a82ef9093a81390ec5ae4d32966422d4053aa1a64a395a401d1d22c9ae19ce88b5ec2daf60381d49ab2f93a5dd735b4e1a3e9da520089f1b9b72af222dc6b1551f9c86ce047d358dea31ddff9c619fc6da9fba189f006e8d8621c5c71a89a7a4b209d49d6f3e3d3a6f0d8d8f2146cd3fd139ab659cf8ea71d76c0379718bd1b9684bba9f3cf506746274ae1ad380bb9699589336c65259cad6d30ceb37e7cac5dccc5c0b31a1938abbd2086ccd8fe6af1ee8909be6a95ff108358dbd9e28296af04d179d9766d6e6c93e37ca47938e1a5deaae990dd53e730f540bea64e8bcd1a9a3c39934d169a1d346a7830effbc874e1f9d816cdfb6b34067a8b5393e62f08f0ef9a1aeb51b98d5151d6796f2128c28e1c589ccb3a9e91a076adefc2f7f3e56a87ce15bc859c8ea72e84ced9093e7997dcfd2a3a6471df6e8ccb56e1f315867a1456c73396d0f263375ab7696e8acd059a3f383ce069dadb17cec0b9d9cb15c421e9d023a45f35342879fb275ad0266ad22f9b0d41b3b15b5886a5fae4fcb94cd56cb01fa639dfc6ea3bfdd0e34cba65355adb853537f7d571497760dba560ba6745d7477e8eed1f5d0f5d13da01ba01ba27b44f7846e84ee19dd0bba31ba577413746fe8a6e8ded17dc8d935f104dda70ac0ee0bdd0cdd37956474eb9ac6c9dbbc6e69ddf1b0aa3479fde17a55130ea6ba6927c76e49961d12a176a7889f9acf795a48cc97125e96a4319049434e4d7429343a1adb992dc4e7345a64b5bfac59e38af7372d5b45ea721365eeb3c25fd9a5df2696d5ab16a1de101069702fba6d6453b3f4291bb577d2575d2a6ff19ff6dad2c689d1505042b743c8334b0792cb343c2ddb52b6a3db45b7876e1fdd01ba43741dcdc6ca0fa4b7407704efaa668bfac2e47b52f28c098cad493afd94b41adbd0aa8b428b71dc6da7a251569b2dba63bbbe6b2c272759cb291ca530d400b7df26a13c5152d7d7419cf9a743754b5d19a592bdf3c026520ab52079dc541b2b1f5b25d452cc68a83178dda9a5ac785d6da0edf8d09d996cac98b8bcd58035320902bfe8a05d1672555c071a71d280f10b26e6f17fdb5a01ddb97dcfc52da2cbd1b8f215ba6b8dcfeef28c375a010c81d4c7b0bc195b652d9785ba8902abc58a0ec45462ab9ce417ba3974f36aeeeb724c8ec69d2da35b419797af869ed8b641cf889f2882d590a2d19f0c540acca9bb823af0e4c7540ae8590dce984fcd26c75ba2d1d8948487b29154fb0a3f729ab3bf7d2b4b4ee7669f98a8ade51b8a270a4fada0eef6d183b42ff6e2696f039ef434b3fb0b7aee2ce5af76eaa2e8edd1f3d0f3d13ba017a017a27744ef845e84de193d3e1ca3773535634623ad9755f54d236f1e23f4344540c9afd9d438d2df0e9beac99cdb7dac9ddb3e81351a24905ba277c3b36c1e1c3045ef8ede03bd277a2ff432f4dee8d5d16ba0d744af855e1bbd0e7a5df47ae8719203f486e839e8f18d63f426e84dd19ba1f78dde1cbc853d0ebe426f6dc2bcb9b87a6b7b3f100aff0d7a9f20c16b4def2859556f6b7916bd2f3ba0cecc97e634ab2369290ff5f412a39727bda157305ba2fdb3bdecc42d0935e75e51fbb190a4ab4d71c79a9691cd348b02333b1d6aa3d8fc9e8aa56ae0d49e761b437db457d2ba989f8c411b3d55245aca8086a6cff7ca766dd1aba057d56c8c80e3d7acf3a3f51a932f33afa12f66362020039167dfa20ff45df477e8efd1f7d0f7d13fa01fa01fa27f34bc0bdb892991d154b5d8b14b30989969d5f612a953c31ca17f423f327d578d4fde0bfdb3a2cbd1489318c934cb272d56987eca25d6d67fe52d342cc69be913c439641256d0bfa86a7bdda9ef21d40c6ec5545fa16ad8494341c9c4a821b41d5005521539df55077d6768662593fb56ab20ffe5eeca90c8a8e63fe8c7527a7128f4af0accd6dfbf161ac4173157f4131b544ce8687a78ff26d7b67df5d04fd555382603bf62ef9b5d0efdbb3d839b88fe437308fb3ff6b4d77a87fd27fa5c6a86fe1bfd3afa0df49be8b7d06fa3cf01bbe873b43efa03b38f643d578df7182b224fbed534ca6fd2ef3f22e4a5f756ea538e0f04c394d5e80f253f36035f8c28ab763704545af6264ee025e83b46557c5e47229fb34ac2f54d5a04570989502da5a7bd6a8cf322fa94ff654d0c7739ff31fa13f4a7e8cfd0ff467f8e3e7fb8449faf5ba3cfeddaa0bf5517ebaaf2a921ddd44ee84453b3bd4abc8f18d4f3224fd4665d9f8e88d54489506b01466a00a31ed5ff429f9b9647bf606e1c9973e00fcbe857d0afa25fc3400d5b1858ad213670419c39d863e061e06370c020c020c4e088c1098308833306170c62eacb0886e658c7e08a4182c10d83d4382519f101622d57fc068803a33621a8c915e17190a7f01227a11a3c062ff10a6a238d73ea832d4ea5fd947c4b815ce46090a9a9468521c77f63502706917a5ee1ab46d2071af516dec91c67143b6193b75c31bfc6d052a5d75de2e50615f84113ef17062d1b166c52b56ed14cd66ab3392a1736e6ac8ea5af58de5c721b830e065d0c7a18f431186030c48033196130c680a34d3198d9f912836f2151954823332a50849a6ae6f542ea20c44176c8ed9aab4dceed6b11f10af5a6ae16b09a7efa194667f5676e530dbda224cc8db4ce22b131afdfaf24c46041e4a947b86da8ecd2bc434fd5b9e69dd2cc501d1f2cd5e351ea6affc0675fa185ef2939f9050d164db88095d991ed556c65a5e151de5a9bc811bd63a23daee63fda4da5bc519ff5bca0e917bc4f832b753c1b9c6d91bbf083c106832d065f18e430c86350108aa3db0b213428bc41195b3019a5e854bc2f2343bbe6c345dbf8560a3c722b4b189431e0c8550c6a186a45590c2d86c0d0c57087e11e430f431fc383196dc12dd3b0758feb0509fbe168e82d8950a3f3366a6b6907669bc7f088e109c308c3338617c490c659d3148731d55a75373b8146053d3b5c8ef1fb6ab80a1b521f68d5a912d5dcb7895f9fda8d7d1b194978fc3be5bb8314c32b86098637eb7c63c8ffde317cc877c904130c9f18be30cc30fcd402ac8618d6b596cfb0a1e1bff923f5404d3e2016e5e9fca46ac1bb7dba555245dcf635d8b542f53e8721e7d9d28c8a611bc30e865d0c7b18f6311c6038c4d0d14a6ac31186630cf9d22986330c399939860b0c9718ae305c63f883e10643eed817861c937b52c0b08821e753d660a66105c3aaa570f343ad6d35ac690121474c48057e64471738c6546744e2702c35408de126f37520d91864227187aaa34657921f8453382e9c9d6df3e1bd668a3a1e1c82ff92168f228c0aa876fe98c5749616715a8bdfb68d229c830da875e7e104d403e1849f30c3b5f62f52abe908e902ced1ae0b7ab74b6a6b9168af89a9772bd47b9d139c08ce190ea71a0bf502e72a54049cc47c1b3837d81a9c14ce1dce03ce93a728fe4d9df8ed8bccb34f79d2b5ce2ddd8857b7d79c4c3600197a20fe5eb5b691d29bf669dbb6fe95f0f1d761dba9aac4fe6d120ae7a5d6ed5bf05b035fbd1c4e06e7ad4e9efcf6b706ab7a1b077752d72c5553b7e4eb66fac9f7bb4d3499508b0b4ed0a89bdc0e4e5d9e5dd50077035b5b6a639da4a53499b5759c6e6cf26b0da05971c71b709a705a70da703acaad1d7525e3b1b4d7b6f867eaeec42d52ddc3e9c1e9ab306c5c34c7392bc019c0e13c1d2d27d1e819322d6704875f277038f20cbcdc6f6e28cf9b67b084c3c1d7707ecc39b21de2ea179c0d3fbcd36afba518fc25c2fc5614652db4073a2a70b652220e1df2b4cc2eaf3d439d1fb5bf6f7baa939478785f64f9da5788a45c7f7decc62fa972f09c5d5466e90da6fd09981858c93e353b3632e74c7863b88422eadf7078de65387c57154e0d9497238391c5081851aab48587571b72f7b4448aa66f57d534ba74290f3565aeb6d27cc2c7e7689ba9aa1cab543d99e30a463b335d63b4c7c8b3c50e463e46078c028c428c8e189d2c7566521125184965146374c528c1e886518ad11da307464f8c5eb6c95f6562f9576f8ceaf255c2a8815113a396bdd531e2201cbc8b510fa33e46038c8696c07be468d1adebcfc78e3594351f1b21abd9bee036b58439fd3b3914ce062791fadc5e9aa61bda32573d56ae3a9a6034d540ffd949c3182f0db9d6344235ea1be1dc66187d63cf89cd315a6804c5aea845e8aa21f70767cf9a9c9a22e7569342474b15865f37b5c655b44e9216531cade45a96c65366dc0aeed20f461b903b8fbecc998bca6194c7a880519122d19cb9f09239df8dc7379631aa5005d2a8a0e0471bdafc2c25225af4b5dd4dfb440ad7343a22271e841a50b47592e6e28eaa4a84b7e66f0970ed83d67c680e9dc5fffebfffe77ffd3ffb7f3fe2 + + + image2 + 789c6dd2c10ac2300c00d07bbf2234b7229d1ddec44f503c0ae2a154410f53d0ed20e2bf6bdb656dd6861dd23d9a66591b0587fd1654235ebded6f0edcd53e419d87ae7b1f4f9b8f906d0bfe012317426a70b07bdc2f3ec77f8ed6b89559061a0343d06a124cc105596482585094bc0ae599b04646c9018926491b2205e140c485cace25755c175d0a967b622ff900b8cc9c7d29af594ea722d589167f813aa852ba07d94b9dce296e883fe7bb163f23896753 + + + + PushButtonConnect + LineEditServerAddr + +
diff --git a/src/llconserverdlg.cpp b/src/llconserverdlg.cpp new file mode 100755 index 00000000..b6d8e4cc --- /dev/null +++ b/src/llconserverdlg.cpp @@ -0,0 +1,177 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "llconserverdlg.h" + + +/* Implementation *************************************************************/ +CLlconServerDlg::CLlconServerDlg(QWidget* parent, const char* name, bool modal, + WFlags f) : CLlconServerDlgBase(parent, name, modal, f) +{ + /* set text for version and application name */ + TextLabelNameVersion-> + setText(QString(APP_NAME) + tr(" server ") + QString(VERSION) + + " (" + QString().setNum(BLOCK_DURATION_MS) + " ms)"); + + /* Create bitmaps */ + /* Define size of the bitmaps */ + const int iXSize = 13; + const int iYSize = 13; + BitmCubeGreen.resize(iXSize, iYSize); + BitmCubeGreen.fill(QColor(0, 255, 0)); + BitmCubeRed.resize(iXSize, iYSize); + BitmCubeRed.fill(QColor(255, 0, 0)); + BitmCubeYellow.resize(iXSize, iYSize); + BitmCubeYellow.fill(QColor(255, 255, 0)); + + /* set up list view for connected clients (We assume that one column is + already there) */ + ListViewClients->setColumnText(0, tr("Client IP : Port")); + ListViewClients->setColumnWidth(0, 170); + ListViewClients->addColumn(tr("Put")); + ListViewClients->setColumnAlignment(1, Qt::AlignCenter); + ListViewClients->addColumn(tr("Get")); + ListViewClients->setColumnAlignment(2, Qt::AlignCenter); + ListViewClients->addColumn(tr("Sample-rate offset [Hz]")); + ListViewClients->clear(); + + /* insert items in reverse order because in Windows all of them are + always visible -> put first item on the top */ + vecpListViewItems.Init(MAX_NUM_CHANNELS); + for (int i = MAX_NUM_CHANNELS - 1; i >= 0; i--) + { + vecpListViewItems[i] = new CServerListViewItem(ListViewClients); +#ifndef _WIN32 + vecpListViewItems[i]->setVisible(false); +#endif + } + + /* Init slider control */ + SliderNetBuf->setRange(1, MAX_NET_BUF_SIZE_NUM_BL); + const int iCurNumNetBuf = Server.GetChannelSet()->GetSockBufSize(); + SliderNetBuf->setValue(iCurNumNetBuf); + TextNetBuf->setText("Size: " + QString().setNum(iCurNumNetBuf)); + + /* start the server */ + Server.Start(); + + /* Init timing jitter text label */ + TextLabelResponseTime->setText(""); + + + /* Main menu bar -------------------------------------------------------- */ + pMenu = new QMenuBar(this); + CHECK_PTR(pMenu); + pMenu->insertItem(tr("&?"), new CLlconHelpMenu(this)); + pMenu->setSeparator(QMenuBar::InWindowsStyle); + + /* Now tell the layout about the menu */ + CLlconServerDlgBaseLayout->setMenuBar(pMenu); + + + /* connections ---------------------------------------------------------- */ + /* timers */ + QObject::connect(&Timer, SIGNAL(timeout()), this, SLOT(OnTimer())); + + /* sliders */ + QObject::connect(SliderNetBuf, SIGNAL(valueChanged(int)), + this, SLOT(OnSliderNetBuf(int))); + + + /* timers --------------------------------------------------------------- */ + /* start timer for GUI controls */ + Timer.start(GUI_CONTRL_UPDATE_TIME); +} + +void CLlconServerDlg::OnTimer() +{ + CVector vecHostAddresses; + CVector vecdSamOffs; + + ListViewMutex.lock(); + + Server.GetConCliParam(vecHostAddresses, vecdSamOffs); + + /* fill list with connected clients */ + for (int i = 0; i < MAX_NUM_CHANNELS; i++) + { + if (!(vecHostAddresses[i].InetAddr == QHostAddress((Q_UINT32) 0))) + { + /* main text (IP, port number) */ + vecpListViewItems[i]->setText(0, QString().sprintf("%s : %d", + vecHostAddresses[i].InetAddr.toString().latin1(), + vecHostAddresses[i].iPort) /* IP, port */); + + /* sample rate offset */ +// FIXME disable sample rate estimation result label since estimation does not work +// vecpListViewItems[i]->setText(3, +// QString().sprintf("%5.2f", vecdSamOffs[i])); + +#ifndef _WIN32 + vecpListViewItems[i]->setVisible(true); +#endif + } +#ifndef _WIN32 + else + vecpListViewItems[i]->setVisible(false); +#endif + } + + ListViewMutex.unlock(); + + /* response time */ + TextLabelResponseTime->setText(QString(). + setNum(Server.GetTimingStdDev(), 'f', 2) + " ms"); +} + +void CLlconServerDlg::OnSliderNetBuf(int value) +{ + Server.GetChannelSet()->SetSockBufSize( BLOCK_SIZE_SAMPLES, value ); + TextNetBuf->setText("Size: " + QString().setNum(value)); +} + +void CLlconServerDlg::customEvent(QCustomEvent* Event) +{ + if (Event->type() == QEvent::User + 11) + { + ListViewMutex.lock(); + + const int iMessType = ((CLlconEvent*) Event)->iMessType; + const int iStatus = ((CLlconEvent*) Event)->iStatus; + const int iChanNum = ((CLlconEvent*) Event)->iChanNum; + + switch(iMessType) + { + case MS_JIT_BUF_PUT: + vecpListViewItems[iChanNum]->SetLight(0, iStatus); + break; + + case MS_JIT_BUF_GET: + vecpListViewItems[iChanNum]->SetLight(1, iStatus); + break; + } + + ListViewMutex.unlock(); + } +} diff --git a/src/llconserverdlg.h b/src/llconserverdlg.h new file mode 100755 index 00000000..37dbb55b --- /dev/null +++ b/src/llconserverdlg.h @@ -0,0 +1,78 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "global.h" +#include "server.h" +#include "multicolorled.h" +#ifdef _WIN32 +# include "../windows/moc/llconserverdlgbase.h" +#else +# include "moc/llconserverdlgbase.h" +#endif + + +/* Definitions ****************************************************************/ +/* update time for GUI controls */ +#define GUI_CONTRL_UPDATE_TIME 1000 /* ms */ + + +/* Classes ********************************************************************/ +class CLlconServerDlg : public CLlconServerDlgBase +{ + Q_OBJECT + +public: + CLlconServerDlg(QWidget* parent = 0, const char* name = 0, + bool modal = FALSE, WFlags f = 0); + + virtual ~CLlconServerDlg() {} + +protected: + QTimer Timer; + CServer Server; + + QPixmap BitmCubeGreen; + QPixmap BitmCubeYellow; + QPixmap BitmCubeRed; + + CVector vecpListViewItems; + QMutex ListViewMutex; + + QMenuBar* pMenu; + + virtual void customEvent(QCustomEvent* Event); + void UpdateSliderNetBuf(); + +public slots: + void OnTimer(); + void OnSliderNetBuf(int value); +}; diff --git a/src/llconserverdlgbase.ui b/src/llconserverdlgbase.ui new file mode 100755 index 00000000..3f373e24 --- /dev/null +++ b/src/llconserverdlgbase.ui @@ -0,0 +1,272 @@ + +CLlconServerDlgBase + + QDialog + + name + CLlconServerDlgBase + + + geometry + + 0 + 0 + 633 + 240 + + + + caption + llcon + + + icon + image0 + + + sizeGripEnabled + true + + + + margin + 11 + + + spacing + 6 + + + QLayoutWidget + + name + Layout4 + + + + margin + 0 + + + spacing + 6 + + + QListView + + + text + Column 1 + + + clickable + true + + + resizeable + true + + + + + text + New Item + + + pixmap + + + + + name + ListViewClients + + + + QGroupBox + + name + GroupBox3 + + + title + Jitter Buffer + + + + margin + 11 + + + spacing + 6 + + + QLabel + + name + TextNetBuf + + + text + Size + + + alignment + AlignCenter + + + hAlign + + + + QSlider + + name + SliderNetBuf + + + pageStep + 1 + + + orientation + Vertical + + + tickmarks + Both + + + + + + + + QLayoutWidget + + name + Layout3 + + + + margin + 0 + + + spacing + 6 + + + QLabel + + name + TextLabelNameVersion + + + text + TextLabelNameVersion + + + + + name + Horizontal Spacing2_2 + + + orientation + Horizontal + + + sizeType + Fixed + + + sizeHint + + 72 + 20 + + + + + QLabel + + name + TextLabel1 + + + text + Timing Standard Deviation: + + + + QLabel + + name + TextLabelResponseTime + + + text + TextLabelResponseTime + + + + + name + Horizontal Spacing2 + + + orientation + Horizontal + + + sizeType + Expanding + + + sizeHint + + 20 + 20 + + + + + QPushButton + + name + buttonOk + + + text + &OK + + + autoDefault + true + + + default + true + + + + + + + + + image0 + 789cd3d7528808f055d0d2e72a2e492cc94c5648ce482c52d04a29cdcdad8c8eb5ade6523234530022130543251d2ea5248564056503300071f5205c0b2004719541dcb434986c22840b0260c56800454c9918b1c444e54454b1c4c4a424e5a4c4442431a0085008081231c4949511621021656565b042843a908032bade24a832547b21c6a1ba0f08d0fda18ccd6fd8c2009f58ad351700407358e1 + + + + + buttonOk + clicked() + CLlconServerDlgBase + accept() + + + diff --git a/src/main.cpp b/src/main.cpp new file mode 100755 index 00000000..1b0fee49 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,106 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include +#include "global.h" +#include "llconclientdlg.h" +#include "llconserverdlg.h" +#include "settings.h" + + +/* Implementation *************************************************************/ +/* This pointer is only used for the post-event routine */ +QApplication* pApp = NULL; + + +int main(int argc, char** argv) +{ + /* Application object */ + QApplication app(argc, argv); + + /* check if server or client application shall be started */ + bool bIsClient = true; + + /* QT docu: argv()[0] is the program name, argv()[1] is the first + argument and argv()[argc()-1] is the last argument */ + if (argc > 1) + { + /* only "-s" is supported right now */ + std::string strShortOpt = "-s"; + if (!strShortOpt.compare(argv[1])) + bIsClient = false; + } + + if (bIsClient) + { + // actual client object + CClient Client; + + // load settings from init-file + CSettings Settings ( &Client ); + Settings.Load (); + + /* client */ + CLlconClientDlg ClientDlg ( &Client, 0, 0, FALSE, Qt::WStyle_MinMax ); + + /* Set main window */ + app.setMainWidget ( &ClientDlg ); + pApp = &app; /* Needed for post-event routine */ + + /* Show dialog */ + ClientDlg.show (); + app.exec (); + + /* Save settings to init-file */ + Settings.Save (); + } + else + { + /* server */ + CLlconServerDlg ServerDlg ( 0, 0, FALSE, Qt::WStyle_MinMax ); + + /* Set main window */ + app.setMainWidget ( &ServerDlg ); + pApp = &app; /* Needed for post-event routine */ + + /* Show dialog */ + ServerDlg.show (); + app.exec (); + } + + return 0; +} + +void PostWinMessage ( const _MESSAGE_IDENT MessID, const int iMessageParam, + const int iChanNum ) +{ + /* In case of simulation no events should be generated */ + if ( pApp != NULL ) + { + CLlconEvent* LlconEv = new CLlconEvent ( MessID, iMessageParam, iChanNum ); + + /* Qt will delete the event object when done */ + QThread::postEvent ( pApp->mainWidget (), LlconEv ); + } +} diff --git a/src/multicolorled.cpp b/src/multicolorled.cpp new file mode 100755 index 00000000..1757c78e --- /dev/null +++ b/src/multicolorled.cpp @@ -0,0 +1,182 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + * Description: + * Implements a multi-color LED + * + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "multicolorled.h" + + +/* Implementation *************************************************************/ +CMultiColorLEDbase::CMultiColorLEDbase() +{ + /* Define size of the bitmaps */ + const int iXSize = 13; + const int iYSize = 13; + + /* Create bitmaps */ + BitmCubeGreen.resize(iXSize, iYSize); + BitmCubeGreen.fill(QColor(0, 255, 0)); + BitmCubeRed.resize(iXSize, iYSize); + BitmCubeRed.fill(QColor(255, 0, 0)); + BitmCubeGrey.resize(iXSize, iYSize); + BitmCubeGrey.fill(QColor(192, 192, 192)); + BitmCubeYellow.resize(iXSize, iYSize); + BitmCubeYellow.fill(QColor(255, 255, 0)); + + /* Init color flags */ + Reset(); + + /* Set init-bitmap */ + SetPixmap(BitmCubeGrey); + eColorFlag = RL_GREY; + + /* Init update time */ + iUpdateTime = DEFAULT_UPDATE_TIME; + + /* Connect timer events to the desired slots */ + connect(&TimerRedLight, SIGNAL(timeout()), + this, SLOT(OnTimerRedLight())); + connect(&TimerGreenLight, SIGNAL(timeout()), + this, SLOT(OnTimerGreenLight())); + connect(&TimerYellowLight, SIGNAL(timeout()), + this, SLOT(OnTimerYellowLight())); +} + +void CMultiColorLEDbase::Reset() +{ + /* Reset color flags */ + bFlagRedLi = false; + bFlagGreenLi = false; + bFlagYellowLi = false; + + UpdateColor(); +} + +void CMultiColorLEDbase::OnTimerRedLight() +{ + bFlagRedLi = false; + UpdateColor(); +} + +void CMultiColorLEDbase::OnTimerGreenLight() +{ + bFlagGreenLi = false; + UpdateColor(); +} + +void CMultiColorLEDbase::OnTimerYellowLight() +{ + bFlagYellowLi = false; + UpdateColor(); +} + +void CMultiColorLEDbase::UpdateColor() +{ + /* Red light has highest priority, then comes yellow and at the end, we + decide to set green light. Allways check the current color of the + control before setting the color to prevent flicking */ + if (bFlagRedLi) + { + if (eColorFlag != RL_RED) + { + SetPixmap(BitmCubeRed); + eColorFlag = RL_RED; + } + return; + } + + if (bFlagYellowLi) + { + if (eColorFlag != RL_YELLOW) + { + SetPixmap(BitmCubeYellow); + eColorFlag = RL_YELLOW; + } + return; + } + + if (bFlagGreenLi) + { + if (eColorFlag != RL_GREEN) + { + SetPixmap(BitmCubeGreen); + eColorFlag = RL_GREEN; + } + return; + } + + /* If no color is active, set control to grey light */ + if (eColorFlag != RL_GREY) + { + SetPixmap(BitmCubeGrey); + eColorFlag = RL_GREY; + } +} + +void CMultiColorLEDbase::SetLight(int iNewStatus) +{ + switch (iNewStatus) + { + case MUL_COL_LED_GREEN: + /* Green light */ + bFlagGreenLi = true; + TimerGreenLight.changeInterval(iUpdateTime); + break; + + case MUL_COL_LED_YELLOW: + /* Yellow light */ + bFlagYellowLi = true; + TimerYellowLight.changeInterval(iUpdateTime); + break; + + case MUL_COL_LED_RED: + /* Red light */ + bFlagRedLi = true; + TimerRedLight.changeInterval(iUpdateTime); + break; + } + + UpdateColor(); +} + +void CMultiColorLEDbase::SetUpdateTime(int iNUTi) +{ + /* Avoid too short intervals */ + if (iNUTi < MIN_TIME_FOR_RED_LIGHT) + iUpdateTime = MIN_TIME_FOR_RED_LIGHT; + else + iUpdateTime = iNUTi; +} + + +CMultiColorLED::CMultiColorLED(QWidget* parent, const char* name, WFlags f) : + QLabel(parent, name, f) +{ + /* Set modified style */ + setFrameShape(QFrame::Panel); + setFrameShadow(QFrame::Sunken); + setIndent(0); +} diff --git a/src/multicolorled.h b/src/multicolorled.h new file mode 100755 index 00000000..d03b70d0 --- /dev/null +++ b/src/multicolorled.h @@ -0,0 +1,139 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + * Description: + * + * SetLight(): + * 0: Green + * 1: Yellow + * 2: Red + * + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(AFX_MULTCOLORLED_H__FD6B49B5_87DF_48DD_A873_804E1606C2AC__INCLUDED_) +#define AFX_MULTCOLORLED_H__FD6B49B5_87DF_48DD_A873_804E1606C2AC__INCLUDED_ + +#include +#include +#include +#include +#include "global.h" + + +/* Definitions ****************************************************************/ +#define DEFAULT_UPDATE_TIME 300 + +/* The red and yellow light should be on at least this interval */ +#define MIN_TIME_FOR_RED_LIGHT 100 + + +/* Classes ********************************************************************/ +class CMultiColorLEDbase : public QObject +{ + Q_OBJECT + +public: + CMultiColorLEDbase(); + + void Reset(); + void SetUpdateTime(int iNUTi); + void SetLight(int iNewStatus); + +protected: + enum ELightColor {RL_GREY, RL_RED, RL_GREEN, RL_YELLOW}; + ELightColor eColorFlag; + + virtual void SetPixmap(QPixmap& NewBitmap) {} /* must be implemented in derived class! */ + void UpdateColor(); + + QPixmap BitmCubeGreen; + QPixmap BitmCubeYellow; + QPixmap BitmCubeRed; + QPixmap BitmCubeGrey; + + QTimer TimerRedLight; + QTimer TimerGreenLight; + QTimer TimerYellowLight; + + int iUpdateTime; + + bool bFlagRedLi; + bool bFlagGreenLi; + bool bFlagYellowLi; + +protected slots: + void OnTimerRedLight(); + void OnTimerGreenLight(); + void OnTimerYellowLight(); +}; + + +class CMultiColorLED : public QLabel, public CMultiColorLEDbase +{ +public: + CMultiColorLED(QWidget* parent, const char* name = 0, WFlags f = 0); + +protected: + virtual void SetPixmap(QPixmap& NewBitmap) {setPixmap(NewBitmap);} +}; + + +class CMultColLEDListViewItem : public CMultiColorLEDbase +{ +public: + CMultColLEDListViewItem(const int iNewCol) : iColumn(iNewCol), + pListViewItem(NULL) {} + + void SetListViewItemPointer(QListViewItem* pNewListViewItem) + {pListViewItem = pNewListViewItem;} + +protected: + virtual void SetPixmap(QPixmap& NewBitmap) + {if (pListViewItem != NULL) pListViewItem->setPixmap(iColumn, NewBitmap);} + + QListViewItem* pListViewItem; + int iColumn; +}; + + +class CServerListViewItem : public QListViewItem +{ +public: + CServerListViewItem(QListView* parent) : LED0(1), LED1(2), + QListViewItem(parent) {LED0.SetListViewItemPointer(this); + LED1.SetListViewItemPointer(this);} + + void SetLight(int iWhichLED, int iNewStatus) + { + switch (iWhichLED) { + case 0: LED0.SetLight(iNewStatus); break; + case 1: LED1.SetLight(iNewStatus); break; + } + } + +protected: + CMultColLEDListViewItem LED0, LED1; +}; + + +#endif // AFX_MULTCOLORLED_H__FD6B49B5_87DF_48DD_A873_804E1606C2AC__INCLUDED_ diff --git a/src/resample.cpp b/src/resample.cpp new file mode 100755 index 00000000..04d99b71 --- /dev/null +++ b/src/resample.cpp @@ -0,0 +1,184 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + * Description: + * Resample routine for arbitrary sample-rate conversions in a low range (for + * frequency offset correction). + * The algorithm is based on a polyphase structure. We upsample the input + * signal with a factor INTERP_DECIM_I_D and calculate two successive samples + * whereby we perform a linear interpolation between these two samples to get + * an arbitraty sample grid. + * The polyphase filter is calculated with Matlab(TM), the associated file + * is ResampleFilter.m. + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "resample.h" + + +/* Implementation *************************************************************/ +int CResample::Resample(CVector& vecdInput, CVector& vecdOutput, + const double dRation) +{ + int i; + + /* move old data from the end to the history part of the buffer and + add new data (shift register) */ + /* Shift old values */ + int iMovLen = iInputBlockSize; + for (i = 0; i < iHistorySize; i++) + { + vecdIntBuff[i] = vecdIntBuff[iMovLen++]; + } + + /* Add new block of data */ + int iBlockEnd = iHistorySize; + for (i = 0; i < iInputBlockSize; i++) + { + vecdIntBuff[iBlockEnd++] = vecdInput[i]; + } + + /* sample-interval of new sample frequency in relation to interpolated + sample-interval */ + dTStep = (double) INTERP_DECIM_I_D / dRation; + + /* init output counter */ + int im = 0; + + /* main loop */ + do + { + /* quantize output-time to interpolated time-index */ + const int ik = (int) dtOut; + + + /* calculate convolutions for the two interpolation-taps ------------ */ + /* phase for the linear interpolation-taps */ + const int ip1 = ik % INTERP_DECIM_I_D; + const int ip2 = (ik + 1) % INTERP_DECIM_I_D; + + /* sample positions in input vector */ + const int in1 = (int) (ik / INTERP_DECIM_I_D); + const int in2 = (int) ((ik + 1) / INTERP_DECIM_I_D); + + /* convolution */ + double dy1 = 0.0; + double dy2 = 0.0; + for (int i = 0; i < NUM_TAPS_PER_PHASE; i++) + { + dy1 += fResTaps1To1[ip1][i] * vecdIntBuff[in1 - i]; + dy2 += fResTaps1To1[ip2][i] * vecdIntBuff[in2 - i]; + } + + + /* linear interpolation --------------------------------------------- */ + /* get numbers after the comma */ + const double dxInt = dtOut - (int) dtOut; + vecdOutput[im] = (dy2 - dy1) * dxInt + dy1; + + + /* increase output counter */ + im++; + + /* increase output-time and index one step */ + dtOut = dtOut + dTStep; + } + while (dtOut < dBlockDuration); + + /* set rtOut back */ + dtOut -= iInputBlockSize * INTERP_DECIM_I_D; + + return im; +} + +void CResample::Init(const int iNewInputBlockSize) +{ + iInputBlockSize = iNewInputBlockSize; + + /* history size must be one sample larger, because we use always TWO + convolutions */ + iHistorySize = NUM_TAPS_PER_PHASE + 1; + + /* calculate block duration */ + dBlockDuration = (iInputBlockSize + iHistorySize - 1) * INTERP_DECIM_I_D; + + /* allocate memory for internal buffer, clear sample history */ + vecdIntBuff.Init(iInputBlockSize + iHistorySize, 0.0); + + /* init absolute time for output stream (at the end of the history part */ + dtOut = (double) (iHistorySize - 1) * INTERP_DECIM_I_D; +} + +void CAudioResample::Resample(CVector& vecdInput, + CVector& vecdOutput) +{ + int j; + + if (dRation == 1.0) + { + /* if ratio is 1, no resampling is needed, just copy vector */ + for (j = 0; j < iOutputBlockSize; j++) + vecdOutput[j] = vecdInput[j]; + } + else + { + /* move old data from the end to the history part of the buffer and + add new data (shift register) */ + /* Shift old values */ + int iMovLen = iInputBlockSize; + for (j = 0; j < NUM_TAPS_PER_PHASE; j++) + vecdIntBuff[j] = vecdIntBuff[iMovLen++]; + + /* Add new block of data */ + int iBlockEnd = NUM_TAPS_PER_PHASE; + for (j = 0; j < iInputBlockSize; j++) + vecdIntBuff[iBlockEnd++] = vecdInput[j]; + + /* main loop */ + for (j = 0; j < iOutputBlockSize; j++) + { + /* phase for the linear interpolation-taps */ + const int ip = + (int) (j * INTERP_DECIM_I_D / dRation) % INTERP_DECIM_I_D; + + /* sample position in input vector */ + const int in = (int) (j / dRation) + NUM_TAPS_PER_PHASE; + + /* convolution */ + double dy = 0.0; + for (int i = 0; i < NUM_TAPS_PER_PHASE; i++) + dy += fResTaps1To1[ip][i] * vecdIntBuff[in - i]; + + vecdOutput[j] = dy; + } + } +} + +void CAudioResample::Init(const int iNewInputBlockSize, const double dNewRation) +{ + dRation = dNewRation; + iInputBlockSize = iNewInputBlockSize; + iOutputBlockSize = (int) (iInputBlockSize * dNewRation); + + /* allocate memory for internal buffer, clear sample history */ + vecdIntBuff.Init(iInputBlockSize + NUM_TAPS_PER_PHASE, 0.0); +} diff --git a/src/resample.h b/src/resample.h new file mode 100755 index 00000000..e6a6803e --- /dev/null +++ b/src/resample.h @@ -0,0 +1,75 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(RESAMPLE_H__3B0FEUFE7876F_FE8FE_CA63_4344_1912__INCLUDED_) +#define RESAMPLE_H__3B0FEUFE7876F_FE8FE_CA63_4344_1912__INCLUDED_ + +#include "util.h" +#include "resamplefilter.h" +#include "global.h" + + +/* Classes ********************************************************************/ +class CResample +{ +public: + CResample() {} + virtual ~CResample() {} + + void Init(const int iNewInputBlockSize); + int Resample(CVector& vecdInput, CVector& vecdOutput, + const double dRation); + +protected: + double dTStep; + double dtOut; + double dBlockDuration; + + CVector vecdIntBuff; + int iHistorySize; + + int iInputBlockSize; +}; + +class CAudioResample +{ +public: + CAudioResample() {} + virtual ~CAudioResample() {} + + void Init(const int iNewInputBlockSize, const double dNewRation); + void Resample(CVector& vecdInput, CVector& vecdOutput); + +protected: + double dRation; + + CVector vecdIntBuff; + int iHistorySize; + + int iInputBlockSize; + int iOutputBlockSize; +}; + + +#endif // !defined(RESAMPLE_H__3B0FEUFE7876F_FE8FE_CA63_4344_1912__INCLUDED_) diff --git a/src/resamplefilter.h b/src/resamplefilter.h new file mode 100644 index 00000000..c383777e --- /dev/null +++ b/src/resamplefilter.h @@ -0,0 +1,157 @@ +/* Automatically generated file with MATLAB */ +/* File name: "ResampleFilter.m" */ +/* Filter taps in time-domain */ + +#ifndef _RESAMPLEFILTER_H_ +#define _RESAMPLEFILTER_H_ + +#define NUM_TAPS_PER_PHASE 12 +#define INTERP_DECIM_I_D 10 + + +/* Filter for ratios close to 1 */ +static float fResTaps1To1[INTERP_DECIM_I_D][NUM_TAPS_PER_PHASE] = { +{ + -0.00129181992672801360f, + 0.00561586829442904840f, + -0.01349857823816511800f, + 0.02541150940858524100f, + -0.04267869501534898200f, + 0.07724474282951483700f, + 0.96609875058711103000f, + -0.01641812005088002400f, + -0.00427135103965109450f, + 0.00726225824406205160f, + -0.00544188094946287510f, + 0.00266742068076876060f +}, +{ + -0.00207886551285772290f, + 0.00866090598717600930f, + -0.02161960909069559500f, + 0.04383507935997314800f, + -0.08302470868585065700f, + 0.18738870090358245000f, + 0.93524350914423104000f, + -0.09031872116141286000f, + 0.02909509423931267600f, + -0.00897188476756275060f, + 0.00178311012364952820f, + 0.00010586149691723067f +}, +{ + -0.00287519800425638110f, + 0.01143197533872717000f, + -0.02889142869399521600f, + 0.06060641890050100900f, + -0.12152802242786863000f, + 0.30933747340895279000f, + 0.87539536840978205000f, + -0.14271415809850990000f, + 0.05516985095031713000f, + -0.02205265100214613000f, + 0.00761119378345958850f, + -0.00187713739944610450f +}, +{ + -0.00354120720771153910f, + 0.01351098086300389300f, + -0.03433664370844288100f, + 0.07367662235517660800f, + -0.15398027155782226000f, + 0.43728178746780866000f, + 0.79013921003423337000f, + -0.17341770937821352000f, + 0.07263788052016696700f, + -0.03120859084480779800f, + 0.01170664402374247200f, + -0.00319259334815649940f +}, +{ + -0.00391755659664638590f, + 0.01447751287549226700f, + -0.03701682481313090000f, + 0.08107302414568577600f, + -0.17606165300033697000f, + 0.56464344237183917000f, + 0.68451472884717957000f, + -0.18369620562420094000f, + 0.08111657494320076400f, + -0.03614676421513295800f, + 0.01396276906259418800f, + -0.00384568128202934270f +}, +{ + -0.00384568128202934270f, + 0.01396276906259418800f, + -0.03614676421513295800f, + 0.08111657494320076400f, + -0.18369620562420094000f, + 0.68451472884717957000f, + 0.56464344237183917000f, + -0.17606165300033697000f, + 0.08107302414568577600f, + -0.03701682481313090000f, + 0.01447751287549226700f, + -0.00391755659664638590f +}, +{ + -0.00319259334815649940f, + 0.01170664402374247200f, + -0.03120859084480779800f, + 0.07263788052016696700f, + -0.17341770937821352000f, + 0.79013921003423337000f, + 0.43728178746780866000f, + -0.15398027155782226000f, + 0.07367662235517660800f, + -0.03433664370844288100f, + 0.01351098086300389300f, + -0.00354120720771153910f +}, +{ + -0.00187713739944610450f, + 0.00761119378345958850f, + -0.02205265100214613000f, + 0.05516985095031713000f, + -0.14271415809850990000f, + 0.87539536840978205000f, + 0.30933747340895279000f, + -0.12152802242786863000f, + 0.06060641890050100900f, + -0.02889142869399521600f, + 0.01143197533872717000f, + -0.00287519800425638110f +}, +{ + 0.00010586149691723067f, + 0.00178311012364952820f, + -0.00897188476756275060f, + 0.02909509423931267600f, + -0.09031872116141286000f, + 0.93524350914423104000f, + 0.18738870090358245000f, + -0.08302470868585065700f, + 0.04383507935997314800f, + -0.02161960909069559500f, + 0.00866090598717600930f, + -0.00207886551285772290f +}, +{ + 0.00266742068076876060f, + -0.00544188094946287510f, + 0.00726225824406205160f, + -0.00427135103965109450f, + -0.01641812005088002400f, + 0.96609875058711103000f, + 0.07724474282951483700f, + -0.04267869501534898200f, + 0.02541150940858524100f, + -0.01349857823816511800f, + 0.00561586829442904840f, + -0.00129181992672801360f +}, +}; + + +#endif /* _RESAMPLEFILTER_H_ */ diff --git a/src/resamplefilter.m b/src/resamplefilter.m new file mode 100755 index 00000000..c281d082 --- /dev/null +++ b/src/resamplefilter.m @@ -0,0 +1,50 @@ +%/******************************************************************************\ +% * Copyright (c) 2004-2006 +% * +% * Author(s): +% * Volker Fischer +% * +%\******************************************************************************/ + +% Filter for ratios close to 1 ------------------------------------------------- +% Fixed for sample-rate conversiones of R ~ 1 +I = 10; % D = I +% Number of taps per poly-phase +NoTapsP = 12; +% Cut-off frequency +fc = 0.97 / I; +% MMSE filter-design and windowing +h = I * firls(I * NoTapsP - 1, [0 fc fc 1], [1 1 0 0]) .* kaiser(I * NoTapsP, 5)'; + + +% Export coefficiants to file **************************************** +fid = fopen('resamplefilter.h', 'w'); + +fprintf(fid, '/* Automatically generated file with MATLAB */\n'); +fprintf(fid, '/* File name: "ResampleFilter.m" */\n'); +fprintf(fid, '/* Filter taps in time-domain */\n\n'); + +fprintf(fid, '#ifndef _RESAMPLEFILTER_H_\n'); +fprintf(fid, '#define _RESAMPLEFILTER_H_\n\n'); + +fprintf(fid, '#define NUM_TAPS_PER_PHASE '); +fprintf(fid, int2str(NoTapsP)); +fprintf(fid, '\n'); +fprintf(fid, '#define INTERP_DECIM_I_D '); +fprintf(fid, int2str(I)); +fprintf(fid, '\n\n\n'); + +% Write filter taps +fprintf(fid, '/* Filter for ratios close to 1 */\n'); +fprintf(fid, 'static float fResTaps1To1[INTERP_DECIM_I_D][NUM_TAPS_PER_PHASE] = {\n'); +for i = 1:I + hTemp = h(i:I:end) ; + fprintf(fid, '{\n'); + fprintf(fid, ' %.20ff,\n', hTemp(1:end - 1)); + fprintf(fid, ' %.20ff\n', hTemp(end)); + fprintf(fid, '},\n'); +end +fprintf(fid, '};\n\n\n'); + +fprintf(fid, '#endif /* _RESAMPLEFILTER_H_ */\n'); +fclose(fid); diff --git a/src/server.cpp b/src/server.cpp new file mode 100755 index 00000000..afce0053 --- /dev/null +++ b/src/server.cpp @@ -0,0 +1,113 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "server.h" + + +/* Implementation *************************************************************/ +CServer::CServer() : Socket(&ChannelSet) +{ + vecsSendData.Init(BLOCK_SIZE_SAMPLES); + + /* init moving average buffer for response time evaluation */ + RespTimeMoAvBuf.Init(LEN_MOV_AV_RESPONSE); + + /* connect timer timeout signal */ + QObject::connect(&Timer, SIGNAL(timeout()), this, SLOT(OnTimer())); +} + +void CServer::Start() +{ + /* start main timer */ + Timer.start(BLOCK_DURATION_MS); + + /* init time for response time evaluation */ + TimeLastBlock = QTime::currentTime(); +} + +void CServer::OnTimer() +{ + CVector vecChanID; + CVector > vecvecdData(BLOCK_SIZE_SAMPLES); + + /* get data from all connected clients */ + ChannelSet.GetBlockAllConC(vecChanID, vecvecdData); + + /* actual processing of audio data -> mix */ + vecsSendData = ProcessData(vecvecdData); + + /* get number of connected clients from vector size */ + const int iNumClients = vecvecdData.Size(); + + /* send the same data to all connected clients */ + for (int i = 0; i < iNumClients; i++) + { + Socket.SendPacket(ChannelSet.PrepSendPacket(vecChanID[i], vecsSendData), + ChannelSet.GetAddress(vecChanID[i]), + ChannelSet.GetTimeStampIdx(vecChanID[i])); + } + + + /* update response time measurement ------------------------------------- */ + /* add time difference */ + const QTime CurTime = QTime::currentTime(); + + /* we want to calculate the standard deviation (we assume that the mean is + correct at the block period time) */ + const double dCurAddVal = + ((double) TimeLastBlock.msecsTo(CurTime) - BLOCK_DURATION_MS); + + RespTimeMoAvBuf.Add(dCurAddVal * dCurAddVal); /* add squared value */ + + /* store old time value */ + TimeLastBlock = CurTime; +} + +CVector CServer::ProcessData(CVector >& vecvecdData) +{ + CVector vecsOutData; + vecsOutData.Init(BLOCK_SIZE_SAMPLES); + + const int iNumClients = vecvecdData.Size(); + + /* we normalize with sqrt() of N to avoid that the level drops too much + in case that a new client connects */ + const double dNorm = sqrt((double) iNumClients); + + /* mix all audio data from all clients together */ + for (int i = 0; i < BLOCK_SIZE_SAMPLES; i++) + { + double dMixedData = 0.0; + + for (int j = 0; j < iNumClients; j++) + { + dMixedData += vecvecdData[j][i]; + } + + /* normalization and truncating to short */ + vecsOutData[i] = Double2Short(dMixedData / dNorm); + } + + return vecsOutData; +} diff --git a/src/server.h b/src/server.h new file mode 100755 index 00000000..31f7b346 --- /dev/null +++ b/src/server.h @@ -0,0 +1,78 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(SERVER_HOIHGE7LOKIH83JH8_3_43445KJIUHF1912__INCLUDED_) +#define SERVER_HOIHGE7LOKIH83JH8_3_43445KJIUHF1912__INCLUDED_ + +#include +#include +#include +#include +#include +#include "global.h" +#include "socket.h" +#include "channel.h" +#include "util.h" + + +/* Classes ********************************************************************/ +class CServer : public QObject +{ + Q_OBJECT + +public: + CServer(); + virtual ~CServer() {} + + void Start(); + void GetConCliParam(CVector& vecHostAddresses, + CVector& vecdSamOffs) + {ChannelSet.GetConCliParam(vecHostAddresses, vecdSamOffs);} + + /* we want to return the standard deviation. For that we need to calculate + the sqaure root */ + double GetTimingStdDev() {return sqrt(RespTimeMoAvBuf.GetAverage());} + + CChannelSet* GetChannelSet() {return &ChannelSet;} + +protected: + CVector ProcessData(CVector >& vecvecdData); + + QTimer Timer; + CVector vecsSendData; + + /* actual working objects */ + CChannelSet ChannelSet; + CSocket Socket; + + /* debugging, evaluating */ + CMovingAv RespTimeMoAvBuf; + QTime TimeLastBlock; + +public slots: + void OnTimer(); +}; + + +#endif /* !defined(SERVER_HOIHGE7LOKIH83JH8_3_43445KJIUHF1912__INCLUDED_) */ diff --git a/src/settings.cpp b/src/settings.cpp new file mode 100755 index 00000000..60bfdc76 --- /dev/null +++ b/src/settings.cpp @@ -0,0 +1,378 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer, Robert Kesterson + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "settings.h" + + +/* Implementation *************************************************************/ +void CSettings::Load () +{ + /* load settings from init-file */ + ReadIniFile (); +} + +void CSettings::Save() +{ + /* write settings in init-file */ + WriteIniFile (); +} + + +/* Read and write init-file ***************************************************/ +void CSettings::ReadIniFile() +{ + int iValue; + bool bValue; + + /* Load data from init-file */ + INIFile ini = LoadIni ( LLCON_INIT_FILE_NAME ); + + + // IP address + pClient->strIPAddress = GetIniSetting ( ini, "Client", "ipaddress" ); + + // audio fader + if ( GetNumericIniSet(ini, "Client", "audfad", 0, AUD_FADER_IN_MAX, iValue ) == TRUE ) { + pClient->SetAudioInFader ( iValue ); + } + + // reverberation level + if ( GetNumericIniSet(ini, "Client", "revlev", 0, AUD_REVERB_MAX, iValue ) == TRUE ) { + pClient->SetReverbLevel ( iValue ); + } + + // reverberation channel assignment + if ( GetFlagIniSet(ini, "Client", "reverblchan", bValue ) == TRUE ) { + pClient->SetReverbOnLeftChan ( bValue ); + } + + // sound card in number of buffers + if ( GetNumericIniSet(ini, "Client", "audinbuf", 0, AUD_SLIDER_LENGTH, iValue ) == TRUE ) { + pClient->GetSndInterface()->SetInNumBuf( iValue ); + } + + // sound card out number of buffers + if ( GetNumericIniSet(ini, "Client", "audoutbuf", 0, AUD_SLIDER_LENGTH, iValue ) == TRUE ) { + pClient->GetSndInterface()->SetOutNumBuf ( iValue ); + } + + // network jitter buffer size + if ( GetNumericIniSet(ini, "Client", "jitbuf", 0, MAX_NET_BUF_SIZE_NUM_BL, iValue ) == TRUE ) { + pClient->GetChannel()->SetSockBufSize ( MIN_BLOCK_SIZE_SAMPLES, iValue ); + } +} + +void CSettings::WriteIniFile() +{ + INIFile ini; + + // IP address + PutIniSetting ( ini, "Client", "ipaddress", pClient->strIPAddress.c_str() ); + + // audio fader + SetNumericIniSet ( ini, "Client", "audfad", pClient->GetAudioInFader () ); + + // reverberation level + SetNumericIniSet ( ini, "Client", "revlev", pClient->GetReverbLevel () ); + + // reverberation channel assignment + SetFlagIniSet ( ini, "Client", "reverblchan", pClient->IsReverbOnLeftChan () ); + + // sound card in number of buffers + SetNumericIniSet ( ini, "Client", "audinbuf", pClient->GetSndInterface()->GetInNumBuf () ); + + // sound card out number of buffers + SetNumericIniSet ( ini, "Client", "audoutbuf", pClient->GetSndInterface()->GetOutNumBuf () ); + + // network jitter buffer size + SetNumericIniSet ( ini, "Client", "jitbuf", pClient->GetChannel()->GetSockBufSize () ); + + + /* Save settings in init-file */ + SaveIni ( ini, LLCON_INIT_FILE_NAME ); +} + +bool CSettings::GetNumericIniSet ( INIFile& theINI, string strSection, + string strKey, int iRangeStart, + int iRangeStop, int& iValue ) +{ + /* Init return value */ + bool bReturn = FALSE; + + const string strGetIni = + GetIniSetting ( theINI, strSection.c_str (), strKey.c_str () ); + + /* Check if it is a valid parameter */ + if ( !strGetIni.empty () ) + { + iValue = atoi( strGetIni.c_str () ); + + /* Check range */ + if ( ( iValue >= iRangeStart ) && ( iValue <= iRangeStop ) ) + { + bReturn = TRUE; + } + } + + return bReturn; +} + +void CSettings::SetNumericIniSet ( INIFile& theINI, string strSection, + string strKey, int iValue ) +{ + char cString[256]; + + sprintf ( cString, "%d", iValue ); + PutIniSetting ( theINI, strSection.c_str (), strKey.c_str (), cString ); +} + +bool CSettings::GetFlagIniSet ( INIFile& theINI, string strSection, + string strKey, bool& bValue ) +{ + /* Init return value */ + bool bReturn = FALSE; + + const string strGetIni = + GetIniSetting ( theINI, strSection.c_str (), strKey.c_str () ); + + if ( !strGetIni.empty () ) + { + if ( atoi ( strGetIni.c_str () ) ) + { + bValue = TRUE; + } + else + { + bValue = FALSE; + } + + bReturn = TRUE; + } + + return bReturn; +} + +void CSettings::SetFlagIniSet ( INIFile& theINI, string strSection, string strKey, + bool bValue ) +{ + if ( bValue == TRUE ) + { + PutIniSetting ( theINI, strSection.c_str (), strKey.c_str (), "1" ); + } + else + { + PutIniSetting ( theINI, strSection.c_str (), strKey.c_str (), "0" ); + } +} + + +/* INI File routines using the STL ********************************************/ +/* The following code was taken from "INI File Tools (STLINI)" written by + Robert Kesterson in 1999. The original files are stlini.cpp and stlini.h. + The homepage is http://robertk.com/source + + Copyright August 18, 1999 by Robert Kesterson */ + +#ifdef _MSC_VER +/* These pragmas are to quiet VC++ about the expanded template identifiers + exceeding 255 chars. You won't be able to see those variables in a debug + session, but the code will run normally */ +#pragma warning (push) +#pragma warning (disable : 4786 4503) +#endif + +string CSettings::GetIniSetting(CSettings::INIFile& theINI, const char* section, + const char* key, const char* defaultval) +{ + string result(defaultval); + INIFile::iterator iSection = theINI.find(string(section)); + + if (iSection != theINI.end()) + { + INISection::iterator apair = iSection->second.find(string(key)); + + if (apair != iSection->second.end()) + result = apair->second; + } + + return result; +} + +void CSettings::PutIniSetting(CSettings::INIFile &theINI, const char *section, + const char *key, const char *value) +{ + INIFile::iterator iniSection; + INISection::iterator apair; + + if ((iniSection = theINI.find(string(section))) == theINI.end()) + { + /* No such section? Then add one */ + INISection newsection; + if (key) + { + newsection.insert( + std::pair(string(key), string(value))); + } + + theINI.insert( + std::pair(string(section), newsection)); + } + else if (key) + { + /* Found section, make sure key isn't in there already, + if it is, just drop and re-add */ + apair = iniSection->second.find(string(key)); + if (apair != iniSection->second.end()) + iniSection->second.erase(apair); + + iniSection->second.insert( + std::pair(string(key), string(value))); + } +} + +CSettings::INIFile CSettings::LoadIni(const char* filename) +{ + INIFile theINI; + char *value, *temp; + string section; + char buffer[MAX_INI_LINE]; + std::fstream file(filename, std::ios::in); + + while (file.good()) + { + memset(buffer, 0, sizeof(buffer)); + file.getline(buffer, sizeof(buffer)); + + if ((temp = strchr(buffer, '\n'))) + *temp = '\0'; /* Cut off at newline */ + + if ((temp = strchr(buffer, '\r'))) + *temp = '\0'; /* Cut off at linefeeds */ + + if ((buffer[0] == '[') && (temp = strrchr(buffer, ']'))) + { /* if line is like --> [section name] */ + *temp = '\0'; /* Chop off the trailing ']' */ + section = &buffer[1]; + PutIniSetting(theINI, &buffer[1]); /* Start new section */ + } + else if (buffer[0] && (value = strchr(buffer, '='))) + { + /* Assign whatever follows = sign to value, chop at "=" */ + *value++ = '\0'; + + /* And add both sides to INISection */ + PutIniSetting(theINI, section.c_str(), buffer, value); + } + else if (buffer[0]) + { + /* Must be a comment or something */ + PutIniSetting(theINI, section.c_str(), buffer, ""); + } + } + return theINI; +} + +void CSettings::SaveIni(CSettings::INIFile &theINI, const char* filename) +{ + bool bFirstSection = TRUE; /* Init flag */ + + std::fstream file(filename, std::ios::out); + if(!file.good()) + return; + + /* Just iterate the hashes and values and dump them to a file */ + INIFile::iterator section = theINI.begin(); + while (section != theINI.end()) + { + if (section->first > "") + { + if (bFirstSection == TRUE) + { + /* Don't put a newline at the beginning of the first section */ + file << "[" << section->first << "]" << std::endl; + + /* Reset flag */ + bFirstSection = FALSE; + } + else + file << std::endl << "[" << section->first << "]" << std::endl; + } + + INISection::iterator pair = section->second.begin(); + + while (pair != section->second.end()) + { + if (pair->second > "") + file << pair->first << "=" << pair->second << std::endl; + else + file << pair->first << "=" << std::endl; + pair++; + } + section++; + } + file.close(); +} + +/* Return true or false depending on whether the first string is less than the + second */ +bool CSettings::StlIniCompareStringNoCase::operator()(const string& x, + const string& y) const +{ +#ifdef WIN32 + return (stricmp(x.c_str(), y.c_str()) < 0) ? true : false; +#else +#ifdef strcasecmp + return (strcasecmp(x.c_str(), y.c_str()) < 0) ? true : false; +#else + unsigned nCount = 0; + int nResult = 0; + const char *p1 = x.c_str(); + const char *p2 = y.c_str(); + + while (*p1 && *p2) + { + nResult = toupper(*p1) - toupper(*p2); + if (nResult != 0) + break; + p1++; + p2++; + nCount++; + } + if (nResult == 0) + { + if (*p1 && !*p2) + nResult = -1; + if (!*p1 && *p2) + nResult = 1; + } + if (nResult < 0) + return true; + return false; +#endif /* strcasecmp */ +#endif +} + +#ifdef _MSC_VER +#pragma warning(pop) +#endif diff --git a/src/settings.h b/src/settings.h new file mode 100755 index 00000000..d51a87bf --- /dev/null +++ b/src/settings.h @@ -0,0 +1,88 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer, Robert Kesterson + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(SETTINGS_H__3B0BA660_DGEG56G456G9876D31912__INCLUDED_) +#define SETTINGS_H__3B0BA660_DGEG56G456G9876D31912__INCLUDED_ + +#include "global.h" +#include "client.h" +#include +#include +#include + + +/* Definitions ****************************************************************/ +// name of the init-file +#define LLCON_INIT_FILE_NAME "llcon.ini" + +/* change this if you expect to have huge lines in your INI files. Note that + this is the max size of a single line, NOT the max number of lines */ +#define MAX_INI_LINE 500 + + +/* Classes ********************************************************************/ +class CSettings +{ +public: + CSettings ( CClient* pNCliP ) : pClient ( pNCliP ) {} + + void Load (); + void Save (); + +protected: + void ReadIniFile (); + void WriteIniFile (); + + /* Function declarations for stlini code written by Robert Kesterson */ + struct StlIniCompareStringNoCase + { + bool operator () ( const std::string& x, const std::string& y ) const; + }; + + /* These typedefs just make the code a bit more readable */ + typedef std::map INISection; + typedef std::map INIFile; + + string GetIniSetting( INIFile& theINI, const char* pszSection, + const char* pszKey, const char* pszDefaultVal = "" ); + void PutIniSetting ( INIFile &theINI, const char *pszSection, + const char* pszKey = NULL, const char* pszValue = "" ); + void SaveIni ( INIFile& theINI, const char* pszFilename ); + INIFile LoadIni ( const char* pszFilename ); + + + void SetNumericIniSet ( INIFile& theINI, string strSection, string strKey, + int iValue ); + bool GetNumericIniSet ( INIFile& theINI, string strSection, string strKey, + int iRangeStart, int iRangeStop, int& iValue ); + void SetFlagIniSet ( INIFile& theINI, string strSection, string strKey, + bool bValue ); + bool GetFlagIniSet ( INIFile& theINI, string strSection, string strKey, + bool& bValue ); + + /* Pointer to the client object needed for the various settings */ + CClient* pClient; +}; + +#endif // !defined(SETTINGS_H__3B0BA660_DGEG56G456G9876D31912__INCLUDED_) diff --git a/src/socket.cpp b/src/socket.cpp new file mode 100755 index 00000000..89ebb139 --- /dev/null +++ b/src/socket.cpp @@ -0,0 +1,121 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "socket.h" + + +/* Implementation *************************************************************/ +void CSocket::Init() +{ + /* allocate memory for network receive and send buffer in samples */ + vecbyRecBuf.Init(MAX_SIZE_BYTES_NETW_BUF); + + /* initialize the listening socket */ + bool bSuccess = SocketDevice.bind( + QHostAddress((Q_UINT32) 0) /* INADDR_ANY */, LLCON_PORT_NUMBER); + + if (bIsClient) + { + /* if no success, try if server is on same machine (only for client) */ + if (!bSuccess) + { + /* if server and client is on same machine, decrease port number by + one by definition */ + bSuccess = + SocketDevice.bind(QHostAddress((Q_UINT32) 0) /* INADDR_ANY */, + LLCON_PORT_NUMBER - 1); + } + } + + if (!bSuccess) + { + /* show error message */ + QMessageBox::critical(0, "Network Error", "Cannot bind the socket.", + QMessageBox::Ok, QMessageBox::NoButton); + + /* exit application */ + exit(1); + } + + QSocketNotifier* pSocketNotivRead = + new QSocketNotifier(SocketDevice.socket(), QSocketNotifier::Read); + + /* connect the "activated" signal */ + QObject::connect(pSocketNotivRead, SIGNAL(activated(int)), + this, SLOT(OnDataReceived())); +} + +void CSocket::SendPacket(const CVector& vecbySendBuf, + const CHostAddress& HostAddr, const int iTimeStampIdx) +{ + const int iVecSizeOut = vecbySendBuf.Size(); + + if ( iVecSizeOut != 0 ) + { + /* send packet through network */ + SocketDevice.writeBlock ((const char*) &((CVector) vecbySendBuf)[0], + iVecSizeOut, HostAddr.InetAddr, HostAddr.iPort); + } + + /* sent time stamp if required */ + if (iTimeStampIdx != INVALID_TIME_STAMP_IDX) + { + /* Always one byte long */ + SocketDevice.writeBlock((const char*) &iTimeStampIdx, 1, + HostAddr.InetAddr, HostAddr.iPort); + } +} + +void CSocket::OnDataReceived() +{ + /* read block from network interface */ + const int iNumBytesRead = SocketDevice.readBlock((char*) &vecbyRecBuf[0], + MAX_SIZE_BYTES_NETW_BUF); + + /* check if an error occurred */ + if (iNumBytesRead < 0) + return; + + /* get host address of client */ + CHostAddress RecHostAddr(SocketDevice.peerAddress(), + SocketDevice.peerPort()); + + if (bIsClient) + { + /* client */ + /* check if packet comes from the server we want to connect */ + if (!(pChannel->GetAddress() == RecHostAddr)) + return; + + if (pChannel->PutData(vecbyRecBuf, iNumBytesRead)) + PostWinMessage(MS_JIT_BUF_PUT, MUL_COL_LED_GREEN); + else + PostWinMessage(MS_JIT_BUF_PUT, MUL_COL_LED_RED); + } + else + { + /* server */ + pChannelSet->PutData(vecbyRecBuf, iNumBytesRead, RecHostAddr); + } +} diff --git a/src/socket.h b/src/socket.h new file mode 100755 index 00000000..a215922f --- /dev/null +++ b/src/socket.h @@ -0,0 +1,78 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(SOCKET_HOIHGE76GEKJH98_3_4344_BB23945IUHF1912__INCLUDED_) +#define SOCKET_HOIHGE76GEKJH98_3_4344_BB23945IUHF1912__INCLUDED_ + +#include +#include +#include +#include +#include +#include +#include "global.h" +#include "channel.h" +#include "util.h" + + +/* Definitions ****************************************************************/ +/* maximum block size for network input buffer. Consider two bytes per sample */ +#define MAX_SIZE_BYTES_NETW_BUF (BLOCK_SIZE_SAMPLES * 2) + + +/* Classes ********************************************************************/ +class CSocket : public QObject +{ + Q_OBJECT + +public: + CSocket::CSocket(CChannel* pNewChannel) : pChannel(pNewChannel), + SocketDevice(QSocketDevice::Datagram /* UDP */), bIsClient(true) + {Init();} + CSocket::CSocket(CChannelSet* pNewChannelSet) : pChannelSet(pNewChannelSet), + SocketDevice(QSocketDevice::Datagram /* UDP */), bIsClient(false) + {Init();} + virtual ~CSocket() {} + + void SendPacket(const CVector& vecbySendBuf, + const CHostAddress& HostAddr, const int iTimeStampIdx); + +protected: + void Init(); + + QSocketDevice SocketDevice; + + CVector vecbyRecBuf; + CHostAddress RecHostAddr; + + CChannel* pChannel; /* for client */ + CChannelSet* pChannelSet; /* for server */ + bool bIsClient; + +public slots: + void OnDataReceived(); +}; + + +#endif /* !defined(SOCKET_HOIHGE76GEKJH98_3_4344_BB23945IUHF1912__INCLUDED_) */ diff --git a/src/util.cpp b/src/util.cpp new file mode 100755 index 00000000..40e07c9e --- /dev/null +++ b/src/util.cpp @@ -0,0 +1,276 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "util.h" + + +/* Implementation *************************************************************/ +/* Input level meter implementation ------------------------------------------ */ +void CSignalLevelMeter::Update(CVector& vecdAudio) +{ + /* Do the update for entire vector, convert to floating-point */ + const int iVecSize = vecdAudio.Size(); + + for (int i = 0; i < iVecSize; i++) + { + /* norm of current audio sample */ + const double dCurSig = fabs(vecdAudio[i]); + + /* search for maximum. Decrease this max with time */ + /* decrease max with time */ + if (dCurLevel >= METER_FLY_BACK) + dCurLevel -= METER_FLY_BACK; + else + { + if ((dCurLevel <= METER_FLY_BACK) && (dCurLevel > 1.0)) + dCurLevel -= 2.0; + } + + /* search for max */ + if (dCurSig > dCurLevel) + dCurLevel = dCurSig; + } +} + +double CSignalLevelMeter::MicLevel() +{ + const double dNormMicLevel = dCurLevel / _MAXSHORT; + + /* logarithmic measure */ + if (dNormMicLevel > 0) + return 20.0 * log10(dNormMicLevel); + else + return -100000.0; /* large negative value */ +} + + +/* Global functions implementation ********************************************/ +void DebugError(const char* pchErDescr, const char* pchPar1Descr, + const double dPar1, const char* pchPar2Descr, + const double dPar2) +{ + FILE* pFile = fopen("DebugError.dat", "a"); + fprintf(pFile, pchErDescr); fprintf(pFile, " ### "); + fprintf(pFile, pchPar1Descr); fprintf(pFile, ": "); + fprintf(pFile, "%e ### ", dPar1); + fprintf(pFile, pchPar2Descr); fprintf(pFile, ": "); + fprintf(pFile, "%e\n", dPar2); + fclose(pFile); + printf("\nDebug error! For more information see test/DebugError.dat\n"); + exit(1); +} + + +/******************************************************************************\ +* Audio Reverberation * +\******************************************************************************/ +/* + The following code is based on "JCRev: John Chowning's reverberator class" + by Perry R. Cook and Gary P. Scavone, 1995 - 2004 + which is in "The Synthesis ToolKit in C++ (STK)" + http://ccrma.stanford.edu/software/stk + + Original description: + This class is derived from the CLM JCRev function, which is based on the use + of networks of simple allpass and comb delay filters. This class implements + three series allpass units, followed by four parallel comb filters, and two + decorrelation delay lines in parallel at the output. +*/ +CAudioReverb::CAudioReverb(const double rT60) +{ + /* Delay lengths for 44100 Hz sample rate */ + int lengths[9] = {1777, 1847, 1993, 2137, 389, 127, 43, 211, 179}; + const double scaler = (double) SAMPLE_RATE / 44100.0; + + int delay, i; + if (scaler != 1.0) + { + for (i = 0; i < 9; i++) + { + delay = (int) floor(scaler * lengths[i]); + + if ((delay & 1) == 0) + delay++; + + while (!isPrime(delay)) + delay += 2; + + lengths[i] = delay; + } + } + + for (i = 0; i < 3; i++) + allpassDelays_[i].Init(lengths[i + 4]); + + for (i = 0; i < 4; i++) + combDelays_[i].Init(lengths[i]); + + setT60(rT60); + allpassCoefficient_ = (double) 0.7; + Clear(); +} + +bool CAudioReverb::isPrime(const int number) +{ +/* + Returns true if argument value is prime. Taken from "class Effect" in + "STK abstract effects parent class". +*/ + if (number == 2) + return true; + + if (number & 1) + { + for (int i = 3; i < (int) sqrt((double) number) + 1; i += 2) + { + if ((number % i) == 0) + return false; + } + + return true; /* prime */ + } + else + return false; /* even */ +} + +void CAudioReverb::Clear() +{ + /* Reset and clear all internal state */ + allpassDelays_[0].Reset(0); + allpassDelays_[1].Reset(0); + allpassDelays_[2].Reset(0); + combDelays_[0].Reset(0); + combDelays_[1].Reset(0); + combDelays_[2].Reset(0); + combDelays_[3].Reset(0); +} + +void CAudioReverb::setT60(const double rT60) +{ + /* Set the reverberation T60 decay time */ + for (int i = 0; i < 4; i++) + { + combCoefficient_[i] = pow((double) 10.0, (double) (-3.0 * + combDelays_[i].Size() / (rT60 * SAMPLE_RATE))); + } +} + +double CAudioReverb::ProcessSample(const double input) +{ + /* Compute one output sample */ + double temp, temp0, temp1, temp2; + + temp = allpassDelays_[0].Get(); + temp0 = allpassCoefficient_ * temp; + temp0 += input; + allpassDelays_[0].Add((int) temp0); + temp0 = -(allpassCoefficient_ * temp0) + temp; + + temp = allpassDelays_[1].Get(); + temp1 = allpassCoefficient_ * temp; + temp1 += temp0; + allpassDelays_[1].Add((int) temp1); + temp1 = -(allpassCoefficient_ * temp1) + temp; + + temp = allpassDelays_[2].Get(); + temp2 = allpassCoefficient_ * temp; + temp2 += temp1; + allpassDelays_[2].Add((int) temp2); + temp2 = -(allpassCoefficient_ * temp2) + temp; + + const double temp3 = temp2 + (combCoefficient_[0] * combDelays_[0].Get()); + const double temp4 = temp2 + (combCoefficient_[1] * combDelays_[1].Get()); + const double temp5 = temp2 + (combCoefficient_[2] * combDelays_[2].Get()); + const double temp6 = temp2 + (combCoefficient_[3] * combDelays_[3].Get()); + + combDelays_[0].Add((int) temp3); + combDelays_[1].Add((int) temp4); + combDelays_[2].Add((int) temp5); + combDelays_[3].Add((int) temp6); + + return (temp3 + temp4 + temp5 + temp6) * (double) 0.5; +} + + +/******************************************************************************\ +* GUI utilities * +\******************************************************************************/ +/* About dialog ------------------------------------------------------------- */ +CAboutDlg::CAboutDlg(QWidget* parent, const char* name, bool modal, WFlags f) + : CAboutDlgBase(parent, name, modal, f) +{ + /* Set the text for the about dialog html text control */ + TextViewCredits->setText( + "

" /* General description of llcon software */ + "llcon " + tr("Client/Server communication tool to enable " + "musician to play together through a conventional broadband internet " + "connection (like DSL).") + "" + "


" + "

" /* GPL header text */ + "This program is free software; you can redistribute it and/or modify " + "it under the terms of the GNU General Public License as published by " + "the Free Software Foundation; either version 2 of the License, or " + "(at your option) any later version.
This program is distributed in " + "the hope that it will be useful, but WITHOUT ANY WARRANTY; without " + "even the implied warranty of MERCHANTABILITY or FITNESS FOR A " + "PARTICULAR PURPOSE. See the GNU General Public License for more " + "details.
You should have received a copy of the GNU General Public " + "License along with his program; if not, write to the Free Software " + "Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 " + "USA" + "


" + "

" /* Libraries used by this compilation of Dream */ + "" + tr("llcon uses the following libraries or code snippets:") + + "

" + "
    " + "
  • audio reverberation code: by Perry R. Cook and Gary P. Scavone, " + "1995 - 2004 (taken from \"The Synthesis ToolKit in C++ (STK)\")
  • " + "
  • IMA-ADPCM: by Erik de Castro Lopo
  • " + "
  • INI File Tools (STLINI): Robert Kesterson in 1999
  • " + "
  • Parts from Dream DRM Receiver by Volker Fischer and Alexander " + "Kurpiers
  • " + "
" + "
"); + + /* Set version number in about dialog */ + QString strVersionText; + strVersionText = "
" + tr("llcon, Version "); + strVersionText += VERSION; + strVersionText += "
" + + tr("llcon, Low-Latency (Internet) Connection") + "
"; + strVersionText += tr("Under the GNU General Public License (GPL)") + + "
"; + TextLabelVersion->setText(strVersionText); +} + + +/* Help menu ---------------------------------------------------------------- */ +CLlconHelpMenu::CLlconHelpMenu ( QWidget* parent ) : QPopupMenu ( parent ) +{ + /* Standard help menu consists of about and what's this help */ + insertItem ( tr ( "What's &This" ), this , + SLOT ( OnHelpWhatsThis () ), SHIFT+Key_F1 ); + insertSeparator(); + insertItem ( tr ( "&About..." ), this, SLOT ( OnHelpAbout () ) ); +} diff --git a/src/util.h b/src/util.h new file mode 100755 index 00000000..1c17c130 --- /dev/null +++ b/src/util.h @@ -0,0 +1,381 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(UTIL_HOIH934256GEKJH98_3_43445KJIUHF1912__INCLUDED_) +#define UTIL_HOIH934256GEKJH98_3_43445KJIUHF1912__INCLUDED_ + +#include +#include +#include +#include +#include +#include +#include "global.h" +using namespace std; /* Because of the library: "vector" */ +#ifdef _WIN32 +# include "../windows/moc/aboutdlgbase.h" +#else +# include "moc/aboutdlgbase.h" +#endif + + +/* Definitions ****************************************************************/ +#define METER_FLY_BACK 2 + + +/* Global functions ***********************************************************/ +/* Converting double to short */ +inline short Double2Short(const double dInput) +{ + /* Lower bound */ + if (dInput < _MINSHORT) + return _MINSHORT; + + /* Upper bound */ + if (dInput > _MAXSHORT) + return _MAXSHORT; + + return (short) dInput; +} + +/* Debug error handling */ +void DebugError(const char* pchErDescr, const char* pchPar1Descr, + const double dPar1, const char* pchPar2Descr, + const double dPar2); + + +/******************************************************************************\ +* CVector base class * +\******************************************************************************/ +template class CVector : public std::vector +{ +public: + CVector() : iVectorSize(0) {pData = this->begin();} + CVector(const int iNeSi) {Init(iNeSi);} + CVector(const int iNeSi, const TData tInVa) {Init(iNeSi, tInVa);} + virtual ~CVector() {} + + /* Copy constructor: The order of the initialization list must not be + changed. First, the base class must be initialized, then the pData + pointer must be set to the new data source. The bit access is, by + default, reset */ + CVector(const CVector& vecI) : + std::vector(static_cast&>(vecI)), + iVectorSize(vecI.Size()), pData(this->begin()) {} + + void Init(const int iNewSize); + + /* Use this init to give all elements a defined value */ + void Init(const int iNewSize, const TData tIniVal); + void Reset(const TData tResetVal); + + void Enlarge(const int iAddedSize); + void Add(const TData& tI) {Enlarge(1); pData[iVectorSize - 1] = tI;} + + inline int Size() const {return iVectorSize;} + + /* This operator allows for a l-value assignment of this object: + CVector[x] = y is possible */ + inline TData& operator[](const int iPos) { +#ifdef _DEBUG_ + if ((iPos < 0) || (iPos > iVectorSize - 1)) + { + DebugError("Writing vector out of bounds", "Vector size", + iVectorSize, "New parameter", iPos); + } +#endif + return pData[iPos];} + + inline TData operator[](const int iPos) const { +#ifdef _DEBUG_ + if ((iPos < 0) || (iPos > iVectorSize - 1)) + { + DebugError("Reading vector out of bounds", "Vector size", + iVectorSize, "New parameter", iPos); + } +#endif + return pData[iPos];} + + inline CVector& operator=(const CVector& vecI) { +#ifdef _DEBUG_ + /* Vectors which shall be copied MUST have same size! (If this is + satisfied, the parameter "iVectorSize" must not be adjusted as + a side effect) */ + if (vecI.Size() != iVectorSize) + { + DebugError("Vector operator=() different size", "Vector size", + iVectorSize, "New parameter", vecI.Size()); + } +#endif + vector::operator=(vecI); + + /* Reset my data pointer in case, the operator=() of the base class + did change the actual memory */ + pData = this->begin(); + + return *this; + } + +protected: + typename std::vector::iterator pData; + int iVectorSize; +}; + + +/* Implementation *************************************************************/ +template void CVector::Init(const int iNewSize) +{ + iVectorSize = iNewSize; + + /* Clear old buffer and reserve memory for new buffer, get iterator + for pointer operations */ + this->clear(); + this->resize(iNewSize); + pData = this->begin(); +} + +template void CVector::Init(const int iNewSize, + const TData tIniVal) +{ + /* Call actual init routine */ + Init(iNewSize); + + /* Set values */ + Reset(tIniVal); +} + +template void CVector::Enlarge(const int iAddedSize) +{ + iVectorSize += iAddedSize; + this->resize(iVectorSize); + + /* We have to reset the pointer since it could be that the vector size was + zero before enlarging the vector */ + pData = this->begin(); +} + +template void CVector::Reset(const TData tResetVal) +{ + /* Set all values to reset value */ + for (int i = 0; i < iVectorSize; i++) + pData[i] = tResetVal; +} + + +/******************************************************************************\ +* CFIFO class (first in, first out) * +\******************************************************************************/ +template class CFIFO : public CVector +{ +public: + CFIFO() : CVector(), iCurIdx(0) {} + CFIFO(const int iNeSi) : CVector(iNeSi), iCurIdx(0) {} + CFIFO(const int iNeSi, const TData tInVa) : + CVector(iNeSi, tInVa), iCurIdx(0) {} + + void Add(const TData tNewD); + inline TData Get() {return this->pData[iCurIdx];} + + virtual void Init(const int iNewSize); + virtual void Init(const int iNewSize, const TData tIniVal); + +protected: + int iCurIdx; +}; + +template void CFIFO::Init(const int iNewSize) +{ + iCurIdx = 0; + CVector::Init(iNewSize); +} + +template void CFIFO::Init(const int iNewSize, + const TData tIniVal) +{ + iCurIdx = 0; + CVector::Init(iNewSize, tIniVal); +} + +template void CFIFO::Add(const TData tNewD) +{ + this->pData[iCurIdx] = tNewD; + + /* Increment index */ + iCurIdx++; + if (iCurIdx >= this->iVectorSize) + iCurIdx = 0; +} + + +/******************************************************************************\ +* CMovingAv class (moving average) * +\******************************************************************************/ +template class CMovingAv : public CVector +{ +public: + CMovingAv() : CVector(), iCurIdx(0), iNorm(0), + tCurAvResult(TData(0)) {} + CMovingAv(const int iNeSi) : CVector(iNeSi), iCurIdx(0), iNorm(0), + tCurAvResult(TData(0)) {} + CMovingAv(const int iNeSi, const TData tInVa) : + CVector(iNeSi, tInVa), iCurIdx(0), iNorm(0), + tCurAvResult(TData(0)) {} + + void Add(const TData tNewD); + inline TData GetAverage() + { + if (this->iNorm == 0) return TData(0); + else return tCurAvResult / this->iNorm; + } + + virtual void Init(const int iNewSize); + void InitVec(const int iNewSize, const int iNewVecSize); + +protected: + int iCurIdx; + int iNorm; + TData tCurAvResult; +}; + +template void CMovingAv::Init(const int iNewSize) +{ + iNorm = 0; + iCurIdx = 0; + tCurAvResult = TData(0); /* Only for scalars! */ + CVector::Init(iNewSize); +} + +template void CMovingAv::Add(const TData tNewD) +{ +/* + Optimized calculation of the moving average. We only add a new value and + subtract the old value from the result. We only need one addition and a + history buffer +*/ + /* Subtract oldest value */ + tCurAvResult -= this->pData[iCurIdx]; + + /* Add new value and write in memory */ + tCurAvResult += tNewD; + this->pData[iCurIdx] = tNewD; + + /* Increase position pointer and test if wrap */ + iCurIdx++; + if (iCurIdx >= this->iVectorSize) + iCurIdx = 0; + + /* take care of norm */ + if (this->iNorm < this->iVectorSize) + this->iNorm++; +} + + +/******************************************************************************\ +* GUI utilities * +\******************************************************************************/ +/* About dialog ------------------------------------------------------------- */ +class CAboutDlg : public CAboutDlgBase +{ + Q_OBJECT + +public: + CAboutDlg(QWidget* parent = 0, const char* name = 0, bool modal = FALSE, + WFlags f = 0); +}; + + +/* Help menu ---------------------------------------------------------------- */ +class CLlconHelpMenu : public QPopupMenu +{ + Q_OBJECT + +public: + CLlconHelpMenu(QWidget* parent = 0); + +protected: + CAboutDlg AboutDlg; + +public slots: + void OnHelpWhatsThis () { QWhatsThis::enterWhatsThisMode (); } + void OnHelpAbout () { AboutDlg.exec(); } +}; + + +/* Other Classes **************************************************************/ +/* Signal Level Meter ------------------------------------------------------- */ +class CSignalLevelMeter +{ +public: + CSignalLevelMeter() : dCurLevel(0.0) {} + virtual ~CSignalLevelMeter() {} + + void Update(CVector& vecdAudio); + double MicLevel(); + void Reset() {dCurLevel = 0.0;} + +protected: + double dCurLevel; +}; + +class CHostAddress +{ +public: + CHostAddress() : InetAddr((Q_UINT32) 0), iPort(0) {} + CHostAddress(const QHostAddress NInetAddr, const Q_UINT16 iNPort) : + InetAddr(NInetAddr), iPort(iNPort) {} + CHostAddress(const CHostAddress& NHAddr) : + InetAddr(NHAddr.InetAddr), iPort(NHAddr.iPort) {} + + /* copy and compare operators */ + CHostAddress& operator=(const CHostAddress& NHAddr) + {InetAddr = NHAddr.InetAddr; iPort = NHAddr.iPort; return *this;} + bool operator==(const CHostAddress& CompAddr) /* oompare operator */ + {return ((CompAddr.InetAddr == InetAddr) && (CompAddr.iPort == iPort));} + + QHostAddress InetAddr; + Q_UINT16 iPort; +}; + + +/* Audio Reverbration ------------------------------------------------------- */ +class CAudioReverb +{ +public: + CAudioReverb(const double rT60 = (double) 5.0); + + void Clear(); + double ProcessSample(const double input); + +protected: + void setT60(const double rT60); + bool isPrime(const int number); + + CFIFO allpassDelays_[3]; + CFIFO combDelays_[4]; + double allpassCoefficient_; + double combCoefficient_[4]; +}; + + +#endif /* !defined(UTIL_HOIH934256GEKJH98_3_43445KJIUHF1912__INCLUDED_) */ diff --git a/windows/MocQT.bat b/windows/MocQT.bat new file mode 100755 index 00000000..1ab82b26 --- /dev/null +++ b/windows/MocQT.bat @@ -0,0 +1,48 @@ +rem/******************************************************************************\ +rem * Copyright (c) 2004-2006 +rem * +rem * Author(s): +rem * Volker Fischer +rem * +rem * Description: +rem * Script for compiling the QT resources under Windows (MOCing and UICing) +rem ****************************************************************************** +rem * +rem * This program is free software; you can redistribute it and/or modify it under +rem * the terms of the GNU General Public License as published by the Free Software +rem * Foundation; either version 2 of the License, or (at your option) any later +rem * version. +rem * +rem * This program is distributed in the hope that it will be useful, but WITHOUT +rem * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +rem * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 1111 +rem * details. +rem * +rem * You should have received a copy of the GNU General Public License along with +rem * this program; if not, write to the Free Software Foundation, Inc., +rem * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +rem * +rem\******************************************************************************/ + + +rem .h -------------- +%qtdir%\bin\moc.exe ..\src\util.h -o moc\moc_util.cpp +%qtdir%\bin\moc.exe ..\src\multicolorled.h -o moc\moc_multicolorled.cpp +%qtdir%\bin\moc.exe ..\src\llconclientdlg.h -o moc\moc_llconclientdlg.cpp +%qtdir%\bin\moc.exe ..\src\llconserverdlg.h -o moc\moc_llconserverdlg.cpp +%qtdir%\bin\moc.exe ..\src\server.h -o moc\moc_server.cpp +%qtdir%\bin\moc.exe ..\src\socket.h -o moc\moc_socket.cpp + + +rem .ui ------------- +%qtdir%\bin\uic.exe ..\src\aboutdlgbase.ui -o moc\aboutdlgbase.h +%qtdir%\bin\uic.exe ..\src\aboutdlgbase.ui -i aboutdlgbase.h -o moc\aboutdlgbase.cpp +%qtdir%\bin\moc.exe moc\aboutdlgbase.h -o moc\moc_aboutdlgbase.cpp + +%qtdir%\bin\uic.exe ..\src\llconclientdlgbase.ui -o moc\llconclientdlgbase.h +%qtdir%\bin\uic.exe ..\src\llconclientdlgbase.ui -i llconclientdlgbase.h -o moc\llconclientdlgbase.cpp +%qtdir%\bin\moc.exe moc\llconclientdlgbase.h -o moc\moc_llconclientdlgbase.cpp + +%qtdir%\bin\uic.exe ..\src\llconserverdlgbase.ui -o moc\llconserverdlgbase.h +%qtdir%\bin\uic.exe ..\src\llconserverdlgbase.ui -i llconserverdlgbase.h -o moc\llconserverdlgbase.cpp +%qtdir%\bin\moc.exe moc\llconserverdlgbase.h -o moc\moc_llconserverdlgbase.cpp diff --git a/windows/llcon.dsp b/windows/llcon.dsp new file mode 100755 index 00000000..52588c97 --- /dev/null +++ b/windows/llcon.dsp @@ -0,0 +1,313 @@ +# Microsoft Developer Studio Project File - Name="llcon" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Application" 0x0101 + +CFG=llcon - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "llcon.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "llcon.mak" CFG="llcon - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "llcon - Win32 Release" (based on "Win32 (x86) Application") +!MESSAGE "llcon - Win32 Debug" (based on "Win32 (x86) Application") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +MTL=midl.exe +RSC=rc.exe + +!IF "$(CFG)" == "llcon - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "Release" +# PROP BASE Intermediate_Dir "Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "Release" +# PROP Intermediate_Dir "Release" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /Yu"stdafx.h" /FD /c +# ADD CPP /nologo /W3 /GX /O2 /I "$(QTDIR)\include" /I "../src" /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "QT_DLL" /D "QT_THREAD_SUPPORT" /FD /c +# SUBTRACT CPP /YX /Yc /Yu +# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:windows /machine:I386 +# ADD LINK32 winmm.lib $(QTDIR)\lib\qt-mt230nc.lib $(QTDIR)\lib\qtmain.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:windows /machine:I386 + +!ELSEIF "$(CFG)" == "llcon - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "Debug" +# PROP BASE Intermediate_Dir "Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "Debug" +# PROP Intermediate_Dir "Debug" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /Yu"stdafx.h" /FD /GZ /c +# ADD CPP /nologo /W3 /Gm /GX /ZI /Od /I "$(QTDIR)\include" /I "../src" /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "QT_DLL" /D "QT_THREAD_SUPPORT" /FD /GZ /c +# SUBTRACT CPP /YX /Yc /Yu +# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:windows /debug /machine:I386 /pdbtype:sept +# ADD LINK32 winmm.lib $(QTDIR)\lib\qt-mt230nc.lib $(QTDIR)\lib\qtmain.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:windows /debug /machine:I386 /pdbtype:sept + +!ENDIF + +# Begin Target + +# Name "llcon - Win32 Release" +# Name "llcon - Win32 Debug" +# Begin Group "Source Files" + +# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" +# Begin Group "moc Files" + +# PROP Default_Filter "" +# Begin Source File + +SOURCE=.\moc\aboutdlgbase.cpp +# End Source File +# Begin Source File + +SOURCE=.\moc\llconclientdlgbase.cpp +# End Source File +# Begin Source File + +SOURCE=.\moc\llconserverdlgbase.cpp +# End Source File +# Begin Source File + +SOURCE=.\moc\moc_aboutdlgbase.cpp +# End Source File +# Begin Source File + +SOURCE=.\moc\moc_llconclientdlg.cpp +# End Source File +# Begin Source File + +SOURCE=.\moc\moc_llconclientdlgbase.cpp +# End Source File +# Begin Source File + +SOURCE=.\moc\moc_llconserverdlg.cpp +# End Source File +# Begin Source File + +SOURCE=.\moc\moc_llconserverdlgbase.cpp +# End Source File +# Begin Source File + +SOURCE=.\moc\moc_multicolorled.cpp +# End Source File +# Begin Source File + +SOURCE=.\moc\moc_server.cpp +# End Source File +# Begin Source File + +SOURCE=.\moc\moc_socket.cpp +# End Source File +# Begin Source File + +SOURCE=.\moc\moc_util.cpp +# End Source File +# End Group +# Begin Source File + +SOURCE=..\src\audiocompr.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\buffer.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\channel.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\client.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\llconclientdlg.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\llconserverdlg.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\main.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\multicolorled.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\resample.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\server.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\settings.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\socket.cpp +# End Source File +# Begin Source File + +SOURCE=.\sound.cpp +# End Source File +# Begin Source File + +SOURCE=..\src\util.cpp +# End Source File +# End Group +# Begin Group "Header Files" + +# PROP Default_Filter "h;hpp;hxx;hm;inl" +# Begin Source File + +SOURCE=..\src\audiocompr.h +# End Source File +# Begin Source File + +SOURCE=..\src\buffer.h +# End Source File +# Begin Source File + +SOURCE=..\src\channel.h +# End Source File +# Begin Source File + +SOURCE=..\src\client.h +# End Source File +# Begin Source File + +SOURCE=..\src\global.h +# End Source File +# Begin Source File + +SOURCE=..\src\llconclientdlg.h +# End Source File +# Begin Source File + +SOURCE=..\src\llconserverdlg.h +# End Source File +# Begin Source File + +SOURCE=..\src\multicolorled.h +# End Source File +# Begin Source File + +SOURCE=..\src\resample.h +# End Source File +# Begin Source File + +SOURCE=..\src\resamplefilter.h +# End Source File +# Begin Source File + +SOURCE=..\src\server.h +# End Source File +# Begin Source File + +SOURCE=..\src\settings.h +# End Source File +# Begin Source File + +SOURCE=..\src\socket.h +# End Source File +# Begin Source File + +SOURCE=.\sound.h +# End Source File +# Begin Source File + +SOURCE=..\src\util.h +# End Source File +# End Group +# Begin Group "Resource Files" + +# PROP Default_Filter "ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe" +# Begin Source File + +SOURCE=.\llcon.rc +# End Source File +# Begin Source File + +SOURCE=.\mainicon.ico +# End Source File +# End Group +# Begin Source File + +SOURCE=..\AUTHORS +# End Source File +# Begin Source File + +SOURCE=..\ChangeLog +# End Source File +# Begin Source File + +SOURCE=..\COPYING +# End Source File +# Begin Source File + +SOURCE=..\INSTALL +# End Source File +# Begin Source File + +SOURCE=.\MocQT.bat +# End Source File +# Begin Source File + +SOURCE=..\NEWS +# End Source File +# Begin Source File + +SOURCE=..\README +# End Source File +# Begin Source File + +SOURCE=..\TODO +# End Source File +# End Target +# End Project diff --git a/windows/llcon.dsw b/windows/llcon.dsw new file mode 100755 index 00000000..9fb43f96 --- /dev/null +++ b/windows/llcon.dsw @@ -0,0 +1,29 @@ +Microsoft Developer Studio Workspace File, Format Version 6.00 +# WARNING: DO NOT EDIT OR DELETE THIS WORKSPACE FILE! + +############################################################################### + +Project: "llcon"=.\llcon.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ +}}} + +############################################################################### + +Global: + +Package=<5> +{{{ +}}} + +Package=<3> +{{{ +}}} + +############################################################################### + diff --git a/windows/llcon.rc b/windows/llcon.rc new file mode 100755 index 00000000..8f0b1527 --- /dev/null +++ b/windows/llcon.rc @@ -0,0 +1,72 @@ +//Microsoft Developer Studio generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "afxres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_MAINICON ICON DISCARDABLE "mainicon.ico" + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE DISCARDABLE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE DISCARDABLE +BEGIN + "#include ""afxres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE DISCARDABLE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/windows/mainicon.ico b/windows/mainicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..def8ea54d6f128c72f9b40cf57fcd14deed34093 GIT binary patch literal 1078 zcmb`GO-{ow5QSeTQsoWF4af@h7UgEm5wi8FM`>=r5$KW~S%OqCeB+##{TmOmAmJU8K*c^gg@%r6{p2Z!CJ)XYO)( zRm=hB>E+>vr{#o{B_W5=m9k>rG(sr3peHbG=TJE73(&%FtpB{aqEcXCwN@2ttuw!V z;F|whpjKLXdf>`-W?zq>1uM{Ge}oTfI;OD`d+hj-b`|iggZLrk9jRf|vMo4(J$MB# z;Jv`i`@91^sC_;$zik6OPK2ILj*;n~`@C$O`-9RH`XBFW&i*H literal 0 HcmV?d00001 diff --git a/windows/resource.h b/windows/resource.h new file mode 100755 index 00000000..cdb434d9 --- /dev/null +++ b/windows/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Developer Studio generated include file. +// Used by llcon.rc +// +#define IDI_MAINICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 105 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1000 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/sound.cpp b/windows/sound.cpp new file mode 100755 index 00000000..f460c778 --- /dev/null +++ b/windows/sound.cpp @@ -0,0 +1,601 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + * Description: + * Sound card interface for Windows operating systems + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#include "Sound.h" + + +/* Implementation *************************************************************/ +/******************************************************************************\ +* Wave in * +\******************************************************************************/ +bool CSound::Read(CVector& psData) +{ + int i; + bool bError; + + /* Check if device must be opened or reinitialized */ + if (bChangParamIn == TRUE) + { + OpenInDevice(); + + /* Reinit sound interface */ + InitRecording(iBufferSizeIn, bBlockingRec); + + /* Reset flag */ + bChangParamIn = FALSE; + } + + /* Wait until data is available */ + if (!(m_WaveInHeader[iWhichBufferIn].dwFlags & WHDR_DONE)) + { + if (bBlockingRec == TRUE) + WaitForSingleObject(m_WaveInEvent, INFINITE); + else + return FALSE; + } + + /* Check if buffers got lost */ + int iNumInBufDone = 0; + for (i = 0; i < iCurNumSndBufIn; i++) + { + if (m_WaveInHeader[i].dwFlags & WHDR_DONE) + iNumInBufDone++; + } + + /* If the number of done buffers equals the total number of buffers, it is + very likely that a buffer got lost -> set error flag */ + if (iNumInBufDone == iCurNumSndBufIn) + bError = TRUE; + else + bError = FALSE; + + /* Copy data from sound card in output buffer */ + for (i = 0; i < iBufferSizeIn; i++) + psData[i] = psSoundcardBuffer[iWhichBufferIn][i]; + + /* Add the buffer so that it can be filled with new samples */ + AddInBuffer(); + + /* In case more than one buffer was ready, reset event */ + ResetEvent(m_WaveInEvent); + + return bError; +} + +void CSound::AddInBuffer() +{ + /* Unprepare old wave-header */ + waveInUnprepareHeader( + m_WaveIn, &m_WaveInHeader[iWhichBufferIn], sizeof(WAVEHDR)); + + /* Prepare buffers for sending to sound interface */ + PrepareInBuffer(iWhichBufferIn); + + /* Send buffer to driver for filling with new data */ + waveInAddBuffer(m_WaveIn, &m_WaveInHeader[iWhichBufferIn], sizeof(WAVEHDR)); + + /* Toggle buffers */ + iWhichBufferIn++; + if (iWhichBufferIn == iCurNumSndBufIn) + iWhichBufferIn = 0; +} + +void CSound::PrepareInBuffer(int iBufNum) +{ + /* Set struct entries */ + m_WaveInHeader[iBufNum].lpData = (LPSTR) &psSoundcardBuffer[iBufNum][0]; + m_WaveInHeader[iBufNum].dwBufferLength = iBufferSizeIn * BYTES_PER_SAMPLE; + m_WaveInHeader[iBufNum].dwFlags = 0; + + /* Prepare wave-header */ + waveInPrepareHeader(m_WaveIn, &m_WaveInHeader[iBufNum], sizeof(WAVEHDR)); +} + +void CSound::InitRecording(int iNewBufferSize, bool bNewBlocking) +{ + /* Check if device must be opened or reinitialized */ + if (bChangParamIn == TRUE) + { + OpenInDevice(); + + /* Reset flag */ + bChangParamIn = FALSE; + } + + /* Set internal parameter */ + iBufferSizeIn = iNewBufferSize; + bBlockingRec = bNewBlocking; + + /* Reset interface so that all buffers are returned from the interface */ + waveInReset(m_WaveIn); + waveInStop(m_WaveIn); + + /* Reset current buffer ID (it is important to do this BEFORE calling + "AddInBuffer()" */ + iWhichBufferIn = 0; + + /* Create memory for sound card buffer */ + for (int i = 0; i < iCurNumSndBufIn; i++) + { + /* Unprepare old wave-header in case that we "re-initialized" this + module. Calling "waveInUnprepareHeader()" with an unprepared + buffer (when the module is initialized for the first time) has + simply no effect */ + waveInUnprepareHeader(m_WaveIn, &m_WaveInHeader[i], sizeof(WAVEHDR)); + + if (psSoundcardBuffer[i] != NULL) + delete[] psSoundcardBuffer[i]; + + psSoundcardBuffer[i] = new short[iBufferSizeIn]; + + + /* Send all buffers to driver for filling the queue ----------------- */ + /* Prepare buffers before sending them to the sound interface */ + PrepareInBuffer(i); + + AddInBuffer(); + } + + /* Notify that sound capturing can start now */ + waveInStart(m_WaveIn); + + /* This reset event is very important for initialization, otherwise we will + get errors! */ + ResetEvent(m_WaveInEvent); +} + +void CSound::OpenInDevice() +{ + /* Open wave-input and set call-back mechanism to event handle */ + if (m_WaveIn != NULL) + { + waveInReset(m_WaveIn); + waveInClose(m_WaveIn); + } + + MMRESULT result = waveInOpen(&m_WaveIn, iCurInDev, &sWaveFormatEx, + (DWORD) m_WaveInEvent, NULL, CALLBACK_EVENT); + + if (result != MMSYSERR_NOERROR) + { + throw CGenErr("Sound Interface Start, waveInOpen() failed. This error " + "usually occurs if another application blocks the sound in."); + } +} + +void CSound::SetInDev(int iNewDev) +{ + /* Set device to wave mapper if iNewDev is invalid */ + if ((iNewDev >= iNumDevs) || (iNewDev < 0)) + iNewDev = WAVE_MAPPER; + + /* Change only in case new device id is not already active */ + if (iNewDev != iCurInDev) + { + iCurInDev = iNewDev; + bChangParamIn = TRUE; + } +} + +void CSound::SetInNumBuf(int iNewNum) +{ + /* check new parameter */ + if ((iNewNum >= MAX_SND_BUF_IN) || (iNewNum < 1)) + iNewNum = NUM_SOUND_BUFFERS_IN; + + /* Change only if parameter is different */ + if (iNewNum != iCurNumSndBufIn) + { + iCurNumSndBufIn = iNewNum; + bChangParamIn = TRUE; + } +} + + +/******************************************************************************\ +* Wave out * +\******************************************************************************/ +bool CSound::Write(CVector& psData) +{ + int i, j; + int iCntPrepBuf; + int iIndexDoneBuf; + bool bError; + + /* Check if device must be opened or reinitialized */ + if (bChangParamOut == TRUE) + { + OpenOutDevice(); + + /* Reinit sound interface */ + InitPlayback(iBufferSizeOut, bBlockingPlay); + + /* Reset flag */ + bChangParamOut = FALSE; + } + + /* Get number of "done"-buffers and position of one of them */ + GetDoneBuffer(iCntPrepBuf, iIndexDoneBuf); + + /* Now check special cases (Buffer is full or empty) */ + if (iCntPrepBuf == 0) + { + if (bBlockingPlay == TRUE) + { + /* Blocking wave out routine. Needed for transmitter. Always + ensure that the buffer is completely filled to avoid buffer + underruns */ + while (iCntPrepBuf == 0) + { + WaitForSingleObject(m_WaveOutEvent, INFINITE); + + GetDoneBuffer(iCntPrepBuf, iIndexDoneBuf); + } + } + else + { + /* All buffers are filled, dump new block ----------------------- */ +// It would be better to kill half of the buffer blocks to set the start +// back to the middle: TODO + return TRUE; /* An error occurred */ + } + } + else if (iCntPrepBuf == iCurNumSndBufOut) + { + /* --------------------------------------------------------------------- + Buffer is empty -> send as many cleared blocks to the sound- + interface until half of the buffer size is reached */ + /* Send half of the buffer size blocks to the sound-interface */ + for (j = 0; j < iCurNumSndBufOut / 2; j++) + { + /* First, clear these buffers */ + for (i = 0; i < iBufferSizeOut; i++) + psPlaybackBuffer[j][i] = 0; + + /* Then send them to the interface */ + AddOutBuffer(j); + } + + /* Set index for done buffer */ + iIndexDoneBuf = iCurNumSndBufOut / 2; + + bError = TRUE; + } + else + bError = FALSE; + + /* Copy stereo data from input in soundcard buffer */ + for (i = 0; i < iBufferSizeOut; i++) + psPlaybackBuffer[iIndexDoneBuf][i] = psData[i]; + + /* Now, send the current block */ + AddOutBuffer(iIndexDoneBuf); + + return bError; +} + +void CSound::GetDoneBuffer(int& iCntPrepBuf, int& iIndexDoneBuf) +{ + /* Get number of "done"-buffers and position of one of them */ + iCntPrepBuf = 0; + for (int i = 0; i < iCurNumSndBufOut; i++) + { + if (m_WaveOutHeader[i].dwFlags & WHDR_DONE) + { + iCntPrepBuf++; + iIndexDoneBuf = i; + } + } +} + +void CSound::AddOutBuffer(int iBufNum) +{ + /* Unprepare old wave-header */ + waveOutUnprepareHeader( + m_WaveOut, &m_WaveOutHeader[iBufNum], sizeof(WAVEHDR)); + + /* Prepare buffers for sending to sound interface */ + PrepareOutBuffer(iBufNum); + + /* Send buffer to driver for filling with new data */ + waveOutWrite(m_WaveOut, &m_WaveOutHeader[iBufNum], sizeof(WAVEHDR)); +} + +void CSound::PrepareOutBuffer(int iBufNum) +{ + /* Set Header data */ + m_WaveOutHeader[iBufNum].lpData = (LPSTR) &psPlaybackBuffer[iBufNum][0]; + m_WaveOutHeader[iBufNum].dwBufferLength = iBufferSizeOut * BYTES_PER_SAMPLE; + m_WaveOutHeader[iBufNum].dwFlags = 0; + + /* Prepare wave-header */ + waveOutPrepareHeader(m_WaveOut, &m_WaveOutHeader[iBufNum], sizeof(WAVEHDR)); +} + +void CSound::InitPlayback(int iNewBufferSize, bool bNewBlocking) +{ + int i, j; + + /* Check if device must be opened or reinitialized */ + if (bChangParamOut == TRUE) + { + OpenOutDevice(); + + /* Reset flag */ + bChangParamOut = FALSE; + } + + /* Set internal parameters */ + iBufferSizeOut = iNewBufferSize; + bBlockingPlay = bNewBlocking; + + /* Reset interface */ + waveOutReset(m_WaveOut); + + for (j = 0; j < iCurNumSndBufOut; j++) + { + /* Unprepare old wave-header (in case header was not prepared before, + simply nothing happens with this function call */ + waveOutUnprepareHeader(m_WaveOut, &m_WaveOutHeader[j], sizeof(WAVEHDR)); + + /* Create memory for playback buffer */ + if (psPlaybackBuffer[j] != NULL) + delete[] psPlaybackBuffer[j]; + + psPlaybackBuffer[j] = new short[iBufferSizeOut]; + + /* Clear new buffer */ + for (i = 0; i < iBufferSizeOut; i++) + psPlaybackBuffer[j][i] = 0; + + /* Prepare buffer for sending to the sound interface */ + PrepareOutBuffer(j); + + /* Initially, send all buffers to the interface */ + AddOutBuffer(j); + } +} + +void CSound::OpenOutDevice() +{ + if (m_WaveOut != NULL) + { + waveOutReset(m_WaveOut); + waveOutClose(m_WaveOut); + } + + MMRESULT result = waveOutOpen(&m_WaveOut, iCurOutDev, &sWaveFormatEx, + (DWORD) m_WaveOutEvent, NULL, CALLBACK_EVENT); + + if (result != MMSYSERR_NOERROR) + throw CGenErr("Sound Interface Start, waveOutOpen() failed."); +} + +void CSound::SetOutDev(int iNewDev) +{ + /* Set device to wave mapper if iNewDev is invalid */ + if ((iNewDev >= iNumDevs) || (iNewDev < 0)) + iNewDev = WAVE_MAPPER; + + /* Change only in case new device id is not already active */ + if (iNewDev != iCurOutDev) + { + iCurOutDev = iNewDev; + bChangParamOut = TRUE; + } +} + +void CSound::SetOutNumBuf(int iNewNum) +{ + /* check new parameter */ + if ((iNewNum >= MAX_SND_BUF_OUT) || (iNewNum < 1)) + iNewNum = NUM_SOUND_BUFFERS_OUT; + + /* Change only if parameter is different */ + if (iNewNum != iCurNumSndBufOut) + { + iCurNumSndBufOut = iNewNum; + bChangParamOut = TRUE; + } +} + + +/******************************************************************************\ +* Common * +\******************************************************************************/ +void CSound::Close() +{ + int i; + MMRESULT result; + + /* Reset audio driver */ + if (m_WaveOut != NULL) + { + result = waveOutReset(m_WaveOut); + if (result != MMSYSERR_NOERROR) + throw CGenErr("Sound Interface, waveOutReset() failed."); + } + + if (m_WaveIn != NULL) + { + result = waveInReset(m_WaveIn); + if (result != MMSYSERR_NOERROR) + throw CGenErr("Sound Interface, waveInReset() failed."); + } + + /* Set event to ensure that thread leaves the waiting function */ + if (m_WaveInEvent != NULL) + SetEvent(m_WaveInEvent); + + /* Wait for the thread to terminate */ + Sleep(500); + + /* Unprepare wave-headers */ + if (m_WaveIn != NULL) + { + for (i = 0; i < iCurNumSndBufIn; i++) + { + result = waveInUnprepareHeader( + m_WaveIn, &m_WaveInHeader[i], sizeof(WAVEHDR)); + + if (result != MMSYSERR_NOERROR) + { + throw CGenErr("Sound Interface, waveInUnprepareHeader()" + " failed."); + } + } + + /* Close the sound in device */ + result = waveInClose(m_WaveIn); + if (result != MMSYSERR_NOERROR) + throw CGenErr("Sound Interface, waveInClose() failed."); + } + + if (m_WaveOut != NULL) + { + for (i = 0; i < iCurNumSndBufOut; i++) + { + result = waveOutUnprepareHeader( + m_WaveOut, &m_WaveOutHeader[i], sizeof(WAVEHDR)); + + if (result != MMSYSERR_NOERROR) + { + throw CGenErr("Sound Interface, waveOutUnprepareHeader()" + " failed."); + } + } + + /* Close the sound out device */ + result = waveOutClose(m_WaveOut); + if (result != MMSYSERR_NOERROR) + throw CGenErr("Sound Interface, waveOutClose() failed."); + } + + /* Set flag to open devices the next time it is initialized */ + bChangParamIn = TRUE; + bChangParamOut = TRUE; +} + +CSound::CSound() +{ + int i; + + /* init number of sound buffers */ + iCurNumSndBufIn = NUM_SOUND_BUFFERS_IN; + iCurNumSndBufOut = NUM_SOUND_BUFFERS_OUT; + + /* Should be initialized because an error can occur during init */ + m_WaveInEvent = NULL; + m_WaveOutEvent = NULL; + m_WaveIn = NULL; + m_WaveOut = NULL; + + /* Init buffer pointer to zero */ + for (i = 0; i < MAX_SND_BUF_IN; i++) + { + memset(&m_WaveInHeader[i], 0, sizeof(WAVEHDR)); + psSoundcardBuffer[i] = NULL; + } + + for (i = 0; i < MAX_SND_BUF_OUT; i++) + { + memset(&m_WaveOutHeader[i], 0, sizeof(WAVEHDR)); + psPlaybackBuffer[i] = NULL; + } + + /* Init wave-format structure */ + sWaveFormatEx.wFormatTag = WAVE_FORMAT_PCM; + sWaveFormatEx.nChannels = NUM_IN_OUT_CHANNELS; + sWaveFormatEx.wBitsPerSample = BITS_PER_SAMPLE; + sWaveFormatEx.nSamplesPerSec = SND_CRD_SAMPLE_RATE; + sWaveFormatEx.nBlockAlign = sWaveFormatEx.nChannels * + sWaveFormatEx.wBitsPerSample / 8; + sWaveFormatEx.nAvgBytesPerSec = sWaveFormatEx.nBlockAlign * + sWaveFormatEx.nSamplesPerSec; + sWaveFormatEx.cbSize = 0; + + /* Get the number of digital audio devices in this computer, check range */ + iNumDevs = waveInGetNumDevs(); + + if (iNumDevs > MAX_NUMBER_SOUND_CARDS) + iNumDevs = MAX_NUMBER_SOUND_CARDS; + + /* At least one device must exist in the system */ + if (iNumDevs == 0) + throw CGenErr("No audio device found."); + + /* Get info about the devices and store the names */ + for (i = 0; i < iNumDevs; i++) + { + if (!waveInGetDevCaps(i, &m_WaveInDevCaps, sizeof(WAVEINCAPS))) + pstrDevices[i] = m_WaveInDevCaps.szPname; + } + + /* We use an event controlled wave-in (wave-out) structure */ + /* Create events */ + m_WaveInEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + m_WaveOutEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + + /* Set flag to open devices */ + bChangParamIn = TRUE; + bChangParamOut = TRUE; + + /* Default device number, "wave mapper" */ + iCurInDev = WAVE_MAPPER; + iCurOutDev = WAVE_MAPPER; + + /* Non-blocking wave out is default */ + bBlockingPlay = FALSE; + + /* Blocking wave in is default */ + bBlockingRec = TRUE; +} + +CSound::~CSound() +{ + int i; + + /* Delete allocated memory */ + for (i = 0; i < iCurNumSndBufIn; i++) + { + if (psSoundcardBuffer[i] != NULL) + delete[] psSoundcardBuffer[i]; + } + + for (i = 0; i < iCurNumSndBufOut; i++) + { + if (psPlaybackBuffer[i] != NULL) + delete[] psPlaybackBuffer[i]; + } + + /* Close the handle for the events */ + if (m_WaveInEvent != NULL) + CloseHandle(m_WaveInEvent); + + if (m_WaveOutEvent != NULL) + CloseHandle(m_WaveOutEvent); +} diff --git a/windows/sound.h b/windows/sound.h new file mode 100755 index 00000000..d75a7682 --- /dev/null +++ b/windows/sound.h @@ -0,0 +1,119 @@ +/******************************************************************************\ + * Copyright (c) 2004-2006 + * + * Author(s): + * Volker Fischer + * + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * +\******************************************************************************/ + +#if !defined(AFX_SOUNDIN_H__9518A621_7F78_11D3_8C0D_EEBF182CF549__INCLUDED_) +#define AFX_SOUNDIN_H__9518A621_7F78_11D3_8C0D_EEBF182CF549__INCLUDED_ + +#include +#include +#include +#include "../src/util.h" +#include "../src/global.h" + + +/* Definitions ****************************************************************/ +#define NUM_IN_OUT_CHANNELS 2 /* Stereo recording (but we only + use one channel for recording) */ +#define BITS_PER_SAMPLE 16 /* Use all bits of the D/A-converter */ +#define BYTES_PER_SAMPLE 2 /* Number of bytes per sample */ + +#define MAX_SND_BUF_IN 200 +#define MAX_SND_BUF_OUT 200 + +#define NUM_SOUND_BUFFERS_IN (70 / BLOCK_DURATION_MS) +#define NUM_SOUND_BUFFERS_OUT (80 / BLOCK_DURATION_MS) + +//#define NUM_SOUND_BUFFERS_IN 7//200 /* Number of sound card buffers */ +//#define NUM_SOUND_BUFFERS_OUT 8//100//15 /* Number of sound card buffers */ + +/* Maximum number of recognized sound cards installed in the system */ +#define MAX_NUMBER_SOUND_CARDS 10 + + +/* Classes ********************************************************************/ +class CSound +{ +public: + CSound(); + virtual ~CSound(); + + void InitRecording(int iNewBufferSize, bool bNewBlocking = TRUE); + void InitPlayback(int iNewBufferSize, bool bNewBlocking = FALSE); + bool Read(CVector& psData); + bool Write(CVector& psData); + + int GetNumDev() {return iNumDevs;} + std::string GetDeviceName(int iDiD) {return pstrDevices[iDiD];} + void SetOutDev(int iNewDev); + int GetOutDev() {return iCurOutDev;} + void SetInDev(int iNewDev); + int GetInDev() {return iCurInDev;} + void SetOutNumBuf(int iNewNum); + int GetOutNumBuf() {return iCurNumSndBufOut;} + void SetInNumBuf(int iNewNum); + int GetInNumBuf() {return iCurNumSndBufIn;} + + void Close(); + +protected: + void OpenInDevice(); + void OpenOutDevice(); + void PrepareInBuffer(int iBufNum); + void PrepareOutBuffer(int iBufNum); + void AddInBuffer(); + void AddOutBuffer(int iBufNum); + void GetDoneBuffer(int& iCntPrepBuf, int& iIndexDoneBuf); + + WAVEFORMATEX sWaveFormatEx; + UINT iNumDevs; + std::string pstrDevices[MAX_NUMBER_SOUND_CARDS]; + UINT iCurInDev; + UINT iCurOutDev; + bool bChangParamIn; + bool bChangParamOut; + int iCurNumSndBufIn; + int iCurNumSndBufOut; + + /* Wave in */ + WAVEINCAPS m_WaveInDevCaps; + HWAVEIN m_WaveIn; + HANDLE m_WaveInEvent; + WAVEHDR m_WaveInHeader[MAX_SND_BUF_IN]; + int iBufferSizeIn; + int iWhichBufferIn; + short* psSoundcardBuffer[MAX_SND_BUF_IN]; + bool bBlockingRec; + + /* Wave out */ + int iBufferSizeOut; + HWAVEOUT m_WaveOut; + short* psPlaybackBuffer[MAX_SND_BUF_OUT]; + WAVEHDR m_WaveOutHeader[MAX_SND_BUF_OUT]; + HANDLE m_WaveOutEvent; + bool bBlockingPlay; +}; + + +#endif // !defined(AFX_SOUNDIN_H__9518A621_7F78_11D3_8C0D_EEBF182CF549__INCLUDED_)