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 00000000..def8ea54 Binary files /dev/null and b/windows/mainicon.ico differ 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_)