kew-2.4.2/000077500000000000000000000000001457005474400123175ustar00rootroot00000000000000kew-2.4.2/.editorconfig000066400000000000000000000003461457005474400147770ustar00rootroot00000000000000# EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true [*] indent_style = space indent_size = 8 end_of_line = lf charset = utf-8 trim_trailing_whitespace = false insert_final_newline = falsekew-2.4.2/.github/000077500000000000000000000000001457005474400136575ustar00rootroot00000000000000kew-2.4.2/.github/workflows/000077500000000000000000000000001457005474400157145ustar00rootroot00000000000000kew-2.4.2/.github/workflows/ci.yml000066400000000000000000000010571457005474400170350ustar00rootroot00000000000000name: Build Check 'on': pull_request: push: branches: - main jobs: build: name: Build Check runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Install build essentials run: sudo apt-get update && sudo apt-get install -y build-essential - name: Install dependencies run: sudo apt install ffmpeg libfftw3-dev libopus-dev libopusfile-dev libvorbis-dev libchafa-dev libfreeimage-dev libavformat-dev libstb-dev - name: Build code run: makekew-2.4.2/.gitignore000066400000000000000000000004671457005474400143160ustar00rootroot00000000000000# Prerequisites *.d # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod *.smod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app kew .vscode/ error.log valgrind-out.txt kew-2.4.2/CONTRIBUTING.md000066400000000000000000000026051457005474400145530ustar00rootroot00000000000000# Welcome to kew music contributing guide Thanks for stopping by, here are some points about contributing to kew. ### Goal of the project The goal of kew is to provide a quick and easy way for people to listen to music with the absolute minimum of inconvenience. It's a small app, limited in scope and it shouldn't be everything to all people. It should continue to be a very light weight app. For instance, it's not imagined as a software for dj'ing or as a busy music file manager with all the features. We want to keep the codebase easy to manage and free of bloat, so might reject a feature out of that reason only. ### Bugs Please report any bugs directly on github, with as much relevant detail as possible. If there's a crash or stability issue, the audio file details are interesting, but also the details of the previous and next file on the playlist. You can extract these details by running: ffprobe -i AUDIO FILE -show_streams -select_streams a:0 -v quiet -print_format json ### Pull Requests - Please contact me (kew-music-player@proton.me) before doing a big change, or risk the whole thing getting rejected. - Try to keep commits fairly small so that they are easy to review. - If you can, use https://editorconfig.org/. There is a file with settings for it: .editorconfig. - If you're fixing a particular bug in the issue list, please explicitly say "Fixes #" in your description". kew-2.4.2/LICENSE000066400000000000000000000432541457005474400133340ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser 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 How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. kew-2.4.2/Makefile000066400000000000000000000035411457005474400137620ustar00rootroot00000000000000CC = gcc PKG_CONFIG ?= pkg-config CFLAGS = -I/usr/include -I/usr/include/ogg -I/usr/include/opus -I/usr/include/stb -Iinclude/imgtotxt/ext -Iinclude/imgtotxt -I/usr/include/ffmpeg -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -Iinclude/miniaudio -O2 $(shell $(PKG_CONFIG) --cflags libavcodec libavutil libavformat libswresample gio-2.0 chafa fftw3f opus opusfile vorbis) CFLAGS += -fstack-protector-strong -Wformat -Werror=format-security -fPIE -fstack-protector -fstack-protector-strong -D_FORTIFY_SOURCE=2 CFLAGS += -Wall -Wextra -Wpointer-arith LIBS = -L/usr/lib -lfreeimage -lpthread -lrt -pthread -lm -lglib-2.0 $(shell $(PKG_CONFIG) --libs libavcodec libavutil libavformat libswresample gio-2.0 chafa fftw3f opus opusfile vorbis vorbisfile) ifeq ($(CC),gcc) LIBS += -latomic endif LDFLAGS = -pie -Wl,-z,relro OBJDIR = src/obj PREFIX = /usr SRCS = src/sound.c src/directorytree.c src/soundcommon.c src/player.c src/soundbuiltin.c src/mpris.c src/playerops.c src/utils.c src/file.c src/chafafunc.c src/cache.c src/songloader.c src/playlist.c src/term.c src/settings.c src/visuals.c src/kew.c OBJS = $(SRCS:src/%.c=$(OBJDIR)/%.o) MAN_PAGE = kew.1 MAN_DIR ?= $(PREFIX)/share/man all: kew $(OBJDIR)/%.o: src/%.c Makefile | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< $(OBJDIR)/write_ascii.o: include/imgtotxt/write_ascii.c Makefile | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< $(LDFLAGS) $(OBJDIR): mkdir -p $(OBJDIR) kew: $(OBJDIR)/write_ascii.o $(OBJS) Makefile $(CC) -o kew $(OBJDIR)/write_ascii.o $(OBJS) $(LIBS) .PHONY: install install: all mkdir -p $(DESTDIR)$(MAN_DIR)/man1 mkdir -p $(DESTDIR)$(PREFIX)/bin install -m 0755 kew $(DESTDIR)$(PREFIX)/bin/kew install -m 0644 docs/kew.1 $(DESTDIR)$(MAN_DIR)/man1/kew.1 .PHONY: uninstall uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/kew rm -f $(DESTDIR)$(MAN_DIR)/man1/kew.1 .PHONY: clean clean: rm -rf $(OBJDIR) kew kew-2.4.2/README.md000066400000000000000000000147231457005474400136050ustar00rootroot00000000000000 # kew [![GitHub license](https://img.shields.io/github/license/ravachol/kew?color=333333&style=for-the-badge)](https://github.com/ravachol/kew/blob/master/LICENSE) Listen to music in the terminal.
kew (/kjuː/) is a command-line music player for Linux. ## Features * Search a music library with partial titles. * Creates a playlist based on a matched directory. * Control the player with previous, next and pause. * Edit the playlist by adding and removing songs. * Supports gapless playback (between files of the same format and type). * Supports MP3, FLAC, MPEG-4 (AAC, M4A, MP4), OPUS, OGG and WAV audio. * Private, no data is collected by kew. ## Installing Packaging status ### Installing on Debian Under Debian Sid/Unstable you can run: ```bash $ sudo apt install kew ``` ### Installing via AUR On Arch Linux, and Arch-based distributions, kew can be found in the AUR. Install with pamac or an AUR helper like yay: ```bash $ yay kew-git ``` Or ```bash $ yay kew ``` ### Installing via Brew For [Homebrew](https://brew.sh/) user, you can install [kew](https://formulae.brew.sh/formula/kew) with: ```bash $ brew install kew ``` ### Installing with quick install script To quickly install kew, just copy and paste this to your terminal (if you have curl installed): ```bash sudo bash -c "curl https://raw.githubusercontent.com/ravachol/kew/main/install.sh | bash" ``` Please note that this script might do a system update before installing kew. ### Installing everything manually kew dependencies are: * FFmpeg * FFTW * Chafa * FreeImage * libopus * opusfile * libvorbis * pkg-config * glib2.0 and AVFormat. These should be installed with the others, if not install them. Install FFmpeg, FFTW, Chafa and FreeImage using your distro's package manager. For instance: ```bash apt install ffmpeg libfftw3-dev libopus-dev libopusfile-dev libvorbis-dev git gcc make libchafa-dev libfreeimage-dev libavformat-dev libglib2.0-dev ``` Or: ```bash pacman -Syu ffmpeg fftw git gcc make chafa freeimage glib2 opus opusfile libvorbis ``` Then run this (either git clone or unzip a release zip into a folder of your choice): ```bash git clone https://github.com/ravachol/kew.git ``` ```bash cd kew ``` ```bash make ``` ```bash sudo make install ``` A TrueColor capable terminal is recommended, like Konsole, kitty or st, to display colors properly. For a complete list of capable terminals, see this page: [Colors in Terminal](https://gist.github.com/CMCDragonkai/146100155ecd79c7dac19a9e23e6a362) (github.com). ### Uninstalling ```bash sudo make uninstall ``` ## Usage In case you don't have a "Music" folder in your home folder, the first thing to do is to tell kew the path to your music library (you only need to do this once): ```bash kew path "/home/joe/Musik/" ``` Now run kew and provide a partial name of a track or directory: ```bash kew cure great ``` This command plays all songs from "The Cure Greatest Hits" directory, provided it's in your music library. kew returns the first directory or file whose name matches the string you provide. It works best when your music library is organized in this way: artist folder->album folder(s)->track(s). #### Some Examples: ``` kew (starting kew with no arguments opens the library view where can choose what to play) kew all (plays all songs (up to 20 000) in your library, shuffled) kew moonlight son (finds and plays moonlight sonata) kew moon (finds and plays moonlight sonata) kew beet (finds and plays all music files under "beethoven" directory) kew dir (sometimes it's necessary to specify it's a directory you want) kew song (or a song) kew list (or a playlist) kew shuffle (shuffles the playlist) kew artistA:artistB:artistC (plays all three artists, shuffled) kew --help, -? or -h kew --version or -v kew --nocover kew --noui (completely hides the UI) kew -q , --quitonstop (exits after finishing playing the playlist) kew -e , --exact (specifies you want an exact (but not case sensitive) match, of for instance an album) kew . loads kew.m3u ``` Put single-quotes inside quotes "guns n' roses" #### Key Bindings * Use +, - keys to adjust the volume. * Use , or h, l keys to switch tracks. * Space, P to toggle pause. * F2 to show/hide the playlist and information about kew. * F3 to show/hide library. * F4 to show/hide track view. * F5 to show/hide key bindings. * u to update library. * v to toggle the spectrum visualizer. * c to toggle album covers. * i to switch between using your regular color scheme or colors derived from the track cover. * b to toggle album covers drawn in ascii or as a normal image. * r to repeat the current song. * s to shuffle the playlist. * a to seek back. * d to seek forward. * x to save the currently loaded playlist to a m3u file in your music folder. * gg go to first song. * number +G, g or Enter, go to specific song number in the playlist. * g go to last song. * . to add current song to kew.m3u (run with "kew ."). * q to quit. ## Configuration kew will create a config file, kewrc, in your default config directory for instance ~/.config/. There you can change key bindings, number of bars in the visualizer and whether to use the album cover for color, or your regular color scheme (default). You can also change the default color of the app here. To edit this file please make sure you quit kew first. ## License Licensed under GPL. [See LICENSE for more information](https://github.com/ravachol/kew/blob/main/LICENSE). ## Attributions kew makes use of the following great open source projects: Chafa by Petter Jansson - https://hpjansson.org/chafa/ FFmpeg by FFmpeg team - https://ffmpeg.org/ FFTW by Matteo Frigo and Steven G. Johnson - https://www.fftw.org/ Libopus by Opus - https://opus-codec.org/ Libvorbis by Xiph.org - https://xiph.org/ Miniaudio by David Reid - https://github.com/mackron/miniaudio Img_To_Txt by Danny Burrows - https://github.com/danny-burrows/img_to_txt Comments? Suggestions? Send mail to kew-music-player@proton.me. kew-2.4.2/SECURITY.md000066400000000000000000000013161457005474400141110ustar00rootroot00000000000000## Reporting a Bug If you find a security related issue, please contact us at kew-music-player@proton.me. When a fix is published, you will receive credit under your real name or bug tracker handle in GitHub. If you prefer to remain anonymous or pseudonymous, you should mention this in your e-mail. ## Disclosure Policy The maintainer will coordinate the fix and release process, involving the following steps: * Confirm the problem and determine the affected versions. * Audit code to find any potential similar problems. * Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible. You may be asked to provide further information in pursuit of a fix. kew-2.4.2/docs/000077500000000000000000000000001457005474400132475ustar00rootroot00000000000000kew-2.4.2/docs/kew-manpage.mdoc000066400000000000000000000076261457005474400163220ustar00rootroot00000000000000.Dd 9/3/23 \" DATE .Dt kew 1 \" Program name and manual section number .Os Linux .Sh NAME \" Section Header - required - don't modify .Nm kew , .\" The following lines are read in generating the apropos(man -k) database. Use only key .\" words here as the database is built based on the words here and in the .ND line. .Nm kew music command .\" Use .Nm macro to designate other names for the documented program. .Nd A command-line music player. .Sh SYNOPSIS \" Section Header - required - don't modify .Nm .Op OPTIONS \" .Op Ar PARTIAL FILE OR DIRECTORY NAME \" [file] .Sh DESCRIPTION \" Section Header - required - don't modify .Nm plays audio from your music folder when given a partial (or whole) file or directory name. A playlist is created when finding more than one file. It supports gapless playback, 24-bit/192khz audio and MPRIS. .Pp On first use, set a path for the music library (this is a once-only operation): .Bl .It .Nm path "/home/joe/Music" .El .Pp Typical use: .Bl .It .Nm artist, album or song name .El .Pp .Nm returns results from the location of the first match, it doesn't return every result possible. .Pp .Sh OPTIONS .Pp .Bl -tag -width -indent .It Fl h, -help Displays help. .It Fl v, -version Displays version info. .It Fl -nocover Hides the cover. .It Fl -noui Completely hides the UI. .It Fl --quitonstop Exits after playing the whole playlist. .It Fl --exact, -e Specifies you want an exact (but not case sensitive) match, of for instance an album. .It shuffle Shuffles the playlist before starting to play it. .It dir Plays the directory not the song. .It song Plays the song not the directory. .It list Searches for a (.m3u) playlist. These are normally not included in searches. .El .Sh EXAMPLES .Pp .Bl -tag -width -indent .It kew Start .Nm in library view. .It kew all Start .Nm with all songs loaded into a playlist. .It kew moonlight son Play moonlight sonata. .It kew moon Play moonlight sonata. .It kew nirv Play all music under Nirvana folder shuffled. .It kew neverm Play Nevermind album in alphabetical order. .It kew shuffle neverm Play Nevermind album shuffled. .It kew list mix Play the mix.m3u playlist. .It kew :: Play the first match (whole directory or file) found under A, B, and C, shuffled. Searches are separated by a colon ':' character. .It "kew ." Play the kew.m3u playlist. .El \" Ends the list .Sh KEY BINDINGS .Pp .Bl -tag -width -indent .It +, - Adjusts volume. .It Left-right arrows/h,l Change song. .It Space Pause. .It F2 Show playlist view .It F3 Show library view .It F4 Show track view .It F5 Show key bindings view .It u Update the libarary. .It i Toggle colors derived from album cover or from color theme. .It c Toggle album cover. .It v Toggle spectrum visualizer. .It b Switch between ascii and image album cover. .It r Repeat current song. .It s Shuffles the playlist. .It a Seek Backward. .It d Seek Forward. .It "." Add to kew.m3u playlist. Run with "kew .". .It x Save currently loaded playlist to a .m3u file in the music folder. .It gg Go to first song. .It number + G, g or Enter Go to specific song number in the playlist. .It G Go to last song. .It q Quit .Nm . .El .Sh FILES .Bl -tag -width -compact .It Pa "~//kewrc" Config file. .It Pa "//kew.m3u" The .Nm playlist. Add to it by pressing '.' during playback of any song. This playlist is saved before q exits. .El .Sh COPYRIGHT Copyright © 2023 Ravachol. License GPLv2+: GNU GPL version 2 or later . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. .Sh SEE ALSO .Bl -tag -width -compact Project home page: .It Pa . .El \" Ends the list kew-2.4.2/docs/kew.1000066400000000000000000000066751457005474400141350ustar00rootroot00000000000000.\" Automatically generated from an mdoc input file. Do not edit. .\" DATE .TH "kew" "1" "9/3/23" "Linux" "General Commands Manual" .nh .if n .ad l .SH "NAME" \fBkew\fR , \fBkew music command\fR \- A command-line music player. .SH "SYNOPSIS" .HP 4n \fBkew\fR [OPTIONS] [\fIPARTIAL\ FILE\ OR\ DIRECTORY\ NAME\fR] .SH "DESCRIPTION" \fBkew\fR plays audio from your music folder when given a partial (or whole) file or directory name. A playlist is created when finding more than one file. It supports gapless playback, 24-bit/192khz audio and MPRIS. .PP On first use, set a path for the music library (this is a once-only operation): .PP \fBkew\fR path "/home/joe/Music" .PP Typical use: .PP \fBkew\fR artist, album or song name .PP \fBkew\fR returns results from the location of the first match, it doesn't return every result possible. .SH "OPTIONS" .TP 9n \fB\-h,\fR \fB\--help\fR Displays help. .TP 9n \fB\-v,\fR \fB\--version\fR Displays version info. .TP 9n \fB\--nocover\fR Hides the cover. .TP 9n \fB\--noui\fR Completely hides the UI. .TP 9n \fB\--quitonstop\fR Exits after playing the whole playlist. .TP 9n \fB\--exact, -e\fR Specifies you want an exact (but not case sensitive) match, of for instance an album. .TP 9n shuffle Shuffles the playlist before starting to play it. .TP 9n dir Plays the directory not the song. .TP 9n song Plays the song not the directory. .TP 9n list Searches for a (.m3u) playlist. These are normally not included in searches. .SH "EXAMPLES" .TP 9n kew Start \fBkew\fR in library view. .TP 9n kew all Start \fBkew\fR with all songs loaded into a playlist. .TP 9n kew moonlight son Play moonlight sonata. .TP 9n kew moon .br Play moonlight sonata. .TP 9n kew nirv .br Play all music under Nirvana folder shuffled. .TP 9n kew neverm Play Nevermind album in alphabetical order. .TP 9n kew shuffle neverm Play Nevermind album shuffled. .TP 9n kew list mix Play the mix.m3u playlist. .TP 9n kew :: Play the first match (whole directory or file) found under A, B, and C, shuffled. Searches are separated by a colon ':' character. .TP 9n kew . Play the kew.m3u playlist. .SH "KEY BINDINGS" .TP 9n +, - Adjusts volume. .TP 9n Left-right arrows/h,l Change song. .TP 9n Space Pause. .TP 9n F2 Show playlist view. .TP 9n F3 Show library view. .TP 9n F4 Show track view. .TP 9n F5 Show key bindings view. .TP 9n u Update the library. .TP 9n i Toggle colors derived from album cover or from color theme. .TP 9n c Toggle album cover. .TP 9n v Toggle spectrum visualizer. .TP 9n b Switch between ascii and image album cover. .TP 9n r Repeat current song. .TP 9n s Shuffles the playlist. .TP 9n a Seek Backward. .TP 9n d Seek Forward. .TP 9n . Add to kew.m3u playlist. Run with "kew .". .TP 9n x Save currently loaded playlist to a .m3u file in the music folder. .TP 9n gg Go to first song. .TP 9n number + G, g or Enter Go to specific song number in the playlist. .TP 9n G Go to last song. .TP 9n q Quit \fBkew\fR. .SH "FILES" .TP 10n \fI~//kewrc\fR Config file. .TP 10n \fI//kew.m3u\fR The \fBkew\fR playlist. Add to it by pressing '.' during playback of any song. This playlist is saved before q exits. .SH "COPYRIGHT" Copyright \[u00A9] 2023 Ravachol. License GPLv2+: GNU GPL version 2 or later . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. .SH "SEE ALSO" Project home page: .TP 10n \fI.\fR kew-2.4.2/include/000077500000000000000000000000001457005474400137425ustar00rootroot00000000000000kew-2.4.2/include/imgtotxt/000077500000000000000000000000001457005474400156215ustar00rootroot00000000000000kew-2.4.2/include/imgtotxt/LICENSE000066400000000000000000000020561457005474400166310ustar00rootroot00000000000000MIT License Copyright (c) 2021 Danny Burrows Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kew-2.4.2/include/imgtotxt/options.h000066400000000000000000000011261457005474400174650ustar00rootroot00000000000000#ifndef OPTIONS_H #define OPTIONS_H // Some preprocessor magic to generate an enum and string array with the same items. #define OUTPUT_MODES(MODE) \ MODE(ANSI) \ MODE(SOLID_ANSI) \ MODE(ASCII) #define GENERATE_ENUM(ENUM) ENUM, #define GENERATE_STRING(STRING) #STRING, enum OutputModes { OUTPUT_MODES(GENERATE_ENUM) }; typedef struct { unsigned int width; unsigned int height; enum OutputModes output_mode; bool original_size; bool true_color; bool squashing_enabled; bool suppress_header; } ImageOptions; #endif /* OPTIONS_H */ kew-2.4.2/include/imgtotxt/write_ascii.c000066400000000000000000000111431457005474400202670ustar00rootroot00000000000000/* TODO: - Debug arguments parsing behaviour. - Options at beginning or end applied to all unless specified otherwise? - Improve 'scale' for plane ASCII output. - Preserve aspect-ratio option. - Check if 256 colors is default. - Check if rbg colors are widely supported. - Translating from rgb into 256 colors. - Windows support? Modified, originally by Danny Burrows: https://github.com/danny-burrows/img_to_txt */ #include #include #include #include "../../src/term.h" // Disable some warnings for stb headers. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-qual" #pragma GCC diagnostic ignored "-Wstrict-overflow" #define STB_IMAGE_IMPLEMENTATION #include #define STB_IMAGE_RESIZE_IMPLEMENTATION #include #pragma GCC diagnostic pop #include "options.h" #include "write_ascii.h" #define MAX_IMG_SIZE 25000 #define MACRO_STRLEN(s) (sizeof(s) / sizeof(s[0])) bool brightPixelFound; char scale[] = "$@&B%8WM#ZO0QoahkbdpqwmLCJUYXIjft/\\|()1{}[]l?zcvunxr!<>i;:*-+~_,\"^`'. "; unsigned int brightness_levels = MACRO_STRLEN(scale) - 2; unsigned char luminanceFromRGB(unsigned char r, unsigned char g, unsigned char b) { return (unsigned char)(0.2126 * r + 0.7152 * g + 0.0722 * b); } unsigned char calc_ascii_char(PixelData *p) { // Calc luminace and use to find Ascii char. unsigned char ch = luminanceFromRGB(p->r, p->g, p->b); int rescaled = ch * brightness_levels / 256; return scale[brightness_levels - rescaled]; } int read_and_convert(const char *filepath, ImageOptions *options) { int rwidth, rheight, rchannels; unsigned char *read_data = stbi_load(filepath, &rwidth, &rheight, &rchannels, 3); if (read_data == NULL) { fprintf(stderr, "Error reading image data!\n\n"); return -1; } unsigned int desired_width, desired_height; desired_width = options->width; desired_height = options->height; // Check for and do any needed image resizing... PixelData *data; if (desired_width != (unsigned)rwidth || desired_height != (unsigned)rheight) { // 3 * uint8 for RGB! unsigned char *new_data = malloc(3 * sizeof(unsigned char) * desired_width * desired_height); int r = stbir_resize_uint8( read_data, rwidth, rheight, 0, new_data, desired_width, desired_height, 0, 3); if (r == 0) { perror("Error resizing image:"); return -1; } stbi_image_free(read_data); // Free read_data. data = (PixelData *)new_data; } else { data = (PixelData *)read_data; } int term_w, term_h; getTermSize(&term_w, &term_h); int indent = ((term_w - desired_width) / 2)+1; if (!options->suppress_header) printf("\n\r"); printf("\n"); printf("%*s", indent, ""); for (unsigned int d = 0; d < desired_width * desired_height; d++) { if (d % desired_width == 0 && d != 0) { if (options->output_mode == SOLID_ANSI) printf("\033[0m"); printf("\n"); printf("%*s", indent, ""); } PixelData *c = data + d; switch (options->output_mode) { case ASCII: printf("%c", calc_ascii_char(c)); break; case ANSI: printf("\033[1;38;2;%03u;%03u;%03um%c", c->r, c->g, c->b, calc_ascii_char(c)); break; case SOLID_ANSI: printf("\033[48;2;%03u;%03u;%03um ", c->r, c->g, c->b); calc_ascii_char(c); break; default: break; } } if (options->output_mode == SOLID_ANSI) printf("\033[0m"); printf("\n"); stbi_image_free(data); return 0; } int output_ascii(const char *pathToImgFile, int height, int width) { ImageOptions opts = { .output_mode = ANSI, .original_size = false, .true_color = true, .squashing_enabled = true, .suppress_header = true, }; if (width > MAX_IMG_SIZE) { fprintf(stderr, "[ERR] Width exceeds maximum image size!\n"); return -1; } opts.width = width--; // compensate, first character on each line is going to be blank if (height > MAX_IMG_SIZE) { fprintf(stderr, "[ERR] Height exceeds maximum image size!\n"); return -1; } opts.height = height; printf("\r"); int ret = read_and_convert(pathToImgFile, &opts); if (ret == -1) // fprintf(stderr, "Failed to convert image: %s\n", pathToImgFile); printf("\033[0m"); return 0; } kew-2.4.2/include/imgtotxt/write_ascii.h000066400000000000000000000010061457005474400202710ustar00rootroot00000000000000#ifndef C_H #define C_H #include #include "options.h" /* This ifdef allows the header to be used from both C and C++. */ #ifdef __cplusplus extern "C" { #endif #ifndef PIXELDATA_STRUCT #define PIXELDATA_STRUCT typedef struct { unsigned char r; unsigned char g; unsigned char b; } PixelData; #endif int getBrightPixel(char *filepath, int width, int height); int output_ascii(const char *pathToImgFile, int height, int width); #ifdef __cplusplus } #endif #endif kew-2.4.2/install.sh000066400000000000000000000053431457005474400143260ustar00rootroot00000000000000#!/bin/bash # Check if the script is running as root if [[ $EUID -ne 0 ]]; then echo "This script must be run as root." exit 1 fi # Removing old files if exist if [ -d "kew" ]; then echo "Removing old files" rm -rf kew &>/dev/null fi # Install dependencies based on the package manager available echo "Installing missing dependencies" if command -v apt &>/dev/null; then apt install -y pkg-config ffmpeg libfftw3-dev libopus-dev libopusfile-dev libvorbis-dev git gcc make libchafa-dev libfreeimage-dev libavformat-dev libglib2.0-dev elif command -v yum &>/dev/null; then yum install -y pkgconfig ffmpeg fftw-devel opus-devel opusfile-devel libvorbis-devel git gcc make chafa-devel libfreeimage-devel libavformat-devel glib2-devel elif command -v pacman &>/dev/null; then pacman -Syu --noconfirm --needed pkg-config ffmpeg fftw git gcc make chafa freeimage glib2 opus opusfile libvorbis elif command -v dnf &>/dev/null; then dnf install -y pkg-config ffmpeg-free-devel fftw-devel opus-devel opusfile-devel libvorbis-devel git gcc make chafa-devel freeimage-devel libavformat-free-devel glib2-devel elif command -v zypper &>/dev/null; then zypper install -y pkg-config ffmpeg fftw-devel opus-devel opusfile-devel libvorbis-devel git chafa-devel gcc make libfreeimage-devel libavformat-devel glib2-devel elif command -v eopkg &>/dev/null; then eopkg install -y pkg-config ffmpeg fftw-devel opus-devel opusfile-devel libvorbis-devel git gcc make chafa-devel libfreeimage-devel libavformat-devel elif command -v guix &>/dev/null; then guix install pkg-config ffmpeg fftw git gcc make chafa libfreeimage libavformat opus opusfile libvorbis elif command -v xbps-install &>/dev/null; then xbps-install -y pkg-config ffmpeg fftw git gcc make chafa libfreeimage libavformat opus opusfile libvorbis else echo "Unsupported package manager. Please install the required dependencies manually." exit 1 fi # Clone the repository repo_url="https://github.com/ravachol/kew.git" echo "Cloning the repository..." if git clone "$repo_url" --depth=1 &>/dev/null; then echo "Repository cloned successfully." else echo "Failed to clone the repository. Please check your network connection and try again." exit 1 fi # Changing directory cd kew # Building echo "Building the project..." if make &> build.log; then echo "Build completed successfully." else echo "Build encountered an error. Please check the build.log file for more information." exit 1 fi # Installing echo "Installing the project..." if sudo make install; then echo "Installation completed successfully." else echo "Installation encountered an error." exit 1 fi # Cleaning up the directory echo "Cleaning directory..." cd .. rm kew -rf &>/dev/null kew-2.4.2/kew-screenshot.png000066400000000000000000012616631457005474400160050ustar00rootroot00000000000000PNG  IHDR{pW pHYs+ IDATx^}l}ɜyqɋ+d)l2nqᮐ@IH'AvdSxMiAv))}$5YiďR? يl !A撃K̙9sܹo| ̜3g|w^())))))))r~}oۇׅc())))))))v(^IIIIIII5L)JJJJJJJJaJWRRRRRRRr Sk_lOLNN2:: H)+cJ^IIIIIIIq>L^筷⭷ޢ^sa'[e)))))))))6qz-|}z-|}yD)JJJJJJJJva~m~>QaVWWGbdw_+կx;a(^IIIIIII5L)JJJJJJJJaJWRRRRRRRr Skx- V#餄m}ⵗUI VۓۇʮgV4zXm~] $>wO /ҕӴڰdjzGuP\$'%σ0)_%xF߶$ߗ4 J[DQ0y*W7|)q A-=%y^ж9(u\FiK RV/ϚYt\n>9Ƕ^a>ov]֫o/lڍ іdy0#QKM֑ӕ0nHcC~iyRTO K#aLRJM$&|h {g*t9Iuq?x@2P XfwH[hG6KŕԄ'IKi pӑlKqxMܼٮ[Zwzv ALVԘ9jRX}}\O%׾oh+ + .wU9R.H*>ymԸѩl˷@\=ӹ&0/TȄ5]I &)BZzLG3fl]2JhF&0eG>jGֽݾ2zUVQac˴AhIg`ƾHR z/ YgGDV$vI&3Ob/ Il+ cyGkNϙe6bt7V`u&z#[".~Ώ!!CIqd\'H>{f{:]m2M _LK-kB !KwStH{nXoNL.RIJl:uiNit~\/<]ƛF捎{3Oo">(eIth9#\wmn^C^JD%ʨetcO:^'|SԘiK.F^QWN狐UiϘ+^з5^>@#|6:gyp`5!˯y4NJov&ƥK#ͮtGWbTPQQm*Y?v~oVɪ+%]N`%1A͸׵H֋K7isXKJ6$ł~߈%P)4kN6+/@il(l>Ym_$7j֞ygʶ):yn+ɲSy@I$ 6x '-E;4ƴoZ]Ymz]/f3w1|pZE)Cv=镬|]_93田aJ"$*TٕJnt4\7DˋW6"NMϛG0IK9/$-\?z^DFV\(%tH=5?>j˵Sft\C#IڥCb5[4tS.žF`C ca"[`uQ* @F˪7z{iEl8R&+&mgwL{[ K^@pf^͵Y'nʭ#XϽ:=cۍޢyn?9 97˟tlcb6Z@{fle> gY+@ꤙ~wғ"@K%3x !fļؗ)ҹ幺|+,"n{;b?KZ'WWhx;"z{FLtd/R˷n+CG;Ner-p-f^tǵWoaqw&^o7gްVc-(V"tu{}[-^lEѸv,ۭ̇ȧl-}{ Q}2-(릒$GW{=zfaB n~֝F>G7>$e)Rߊ\znÌnS&YJPmq zI`B" sGPҢ?-U;v !Tyov%;>X|)R$ӕP` =vkP/rvEw`ۿ O{OsM 뚾=P˅Sj<"&:NEvϣ{IVRTd=w -|*x"o0|Kyq粞#-ޭ{#9KO^{յEѲrNKv o&7t:l]d]bO{b%Ot2Ķqdeyn败\Y|1Gjx:GZcmҶ1+7/Ry-Aعioj*bIZ%h[w4WXǾ\cvL83F`yT2O(ڣj#;%<幝 Ȟcau&QZtaگGYZZ|'?uuLoAO1vi1?(+~phS|s^@%L()٩8Y͒ÇYYY__YYYI~a򈾉i'1lIV[~kVK%q7b9Q^qSjMIdO#c;"%% MOז\s0?ٷo}8b]ø2RWksb/"LׅUI j{{Vz_ϯoFTyʶd~`y-?:v◿}_ڵ>ѵ˫miᄈϧ3zȋ/\W0|Vn3KG^}/8I6+2'!l_i6gZ{HJi&C[yͦCuȧy;6[ouYeSn[) Lg 'galWbxUm3Z V\.tOI"D{{tNgw8Aw4.m{zOyJR+)ɣ+$ŧ`k侵\380XqJ/5_$+6kz^ho-0ŖI!@?$Md .!"_ݷFsEI{ )7%5'n87YvMӫ;y_ߢ٘f?I޹őwd}$wL/8GJM*z]AϺWCݒFo.-"PJ?{;@T9r\!rӭD:&2\w&\9t>"6)3Y `gUMqv<&44;7X1iK3vj٩.lt ~d__V6F8Dju ´E#Q^BsfViv,EHd!D{:v/- EiM[xK~8 6YKa]'7^@Pf.~l$뽏]w{3Ӟ߼y<^~D[xLIҒm_: 뛵 - e/)N{#TEE;m%%{reu%6y޽Qiĺ^IvA Qa7AudfK3]ΒE,/&2E?'NFGڳn'c -nR<,{-ۙ7c%;۠S.34ۅ^.>gw0sn60BLlܓgGhoa;=gam|"䅱Cdr! {sub?W$<="䵣4 5wVߢIfeeSRm&7/ms{/Y2ʶQ/J@!"FW#ojb_v&zìHYli+B`{<M{]>ݰG?G,,t]YS ǶĞJf7\pe݀U@%=tN;k) -Oy} x¦ܫd'.cI/ϫ?~d윧LlT] 5y_% 8ӪKe]?q u#5sf^z{T7 it*N5$7,9tѥQʊآ u*u|xx+C3$&@QӼz"O_H:J]6vk&,]u:Y8SwmloEٞ:< Hr\d`HCU&@|.;)!w h0G+ ެt0v4(}z_m$=={7ϜtyGLЩ[ra[EWuZ״{m9Yfȳ#92$/N}ݤ<ĩ*(H oX<;]w!M qߗnxWHVUD}`ل@^TH RZK*tHYu??OFv{ێ[$89$_O1;Bw?m%RI6eN/^?1=Z'64T|415Ґ$\!@X\ ƫ:$W< \iUMmdzu.tg~3ï@U 0 X[^ڃ;]#jU =37=쭨Ľ=3ڢ: MWsy!_U2G#Opp}Ǚb6>gz%+HZ`#_7=ux-S;A[vqLks),PwpHK/'հl)cZTy&W23:bFFMr]obwOp$-%̡4Ó;a~`l-?FZ'z;e[Hj6:%'/%b޿(M>DX>j޶/h##mKEZcyPq)PlzT+g_\_a̳^\J|BMi)oA {GytC:z zWy73ZB- - {صo]Ct`W(7뙂0`r{Ds+ӱ᰼&C'{!=2T/ (n+Ƴ뵍9+fկ$&"XO7Y)ܹ*CZk4˜9Sw IDAT(#.M&9ZYE\YALZ9€Z.#34=K3~F;rw0?}S뻨. kjoN7Ln͆c@3%%&i<f+тRi9e.]ʥ_M"V67ڋ'8wr/'{&&GG?ȓxF yi&vL =-#v]un:x /NY TY^\Os3CSLA\n؞sOK(uAq`Zչ.}?}gM8?U{ćyMN2>6(Z njb@M_4}57&`Myj{~Ϝṑ Nn9tW7r_f{H |iNvVkmo" Ev·nFwX SEB[6=:Xea03{S PDTԂ(Ԫ GaX=+@{ʢ{*` 2-Sğfn*'vף85 ALPks }Q0X>}韼:{#G5Ez]R̡E[%.Mw?{5ǫVkC&mmvz#r5iay.#~Wd84Q%_qYdx8' U[PRj[SaZW׌ ݫsӤr03fu=fYwʩv7OKeiyϦio[ }'kx!+zH;9s|,tQ-B Z5*Oޮp˻oj.NgKross-&v#&2Dyx6iתU|1h6>w>sfh0{{\|ܣG~8eE\!4 SJc͊ )ɠ|* }~ ,왬q#{!;03XY.kkcZ<;?Oמ9Խx8,/dFqE[l{e۩m ;^oN{UfẂA~Ϟ~O~Fgm[`j8LHK;/|`w^n.}<z[z@8 L?sEy|ldm$Dwbobс+Kp`Vc6g~#N=3m1M @2PE=}ϧ9~Μ?>gG?y4 o[i NqaǑEu^)Τ땴2)rM/eFlw߈Hit)7wmwϫ7jc5 ZV0:X'/ȩ ;jpkw-Ǎc{xSy-.4V( sHH7.hJZ-UaY3s!چt=O[GdfϏ`b/K{uO|sVyy}g{/y{ 5Fϥt%z!MdaA^֋_Jl.'aTB6h_1  &u/N>[JPyxn5Wm%ԩ|Wʷ<ͥKWg\yfi?z*mG{{gԍP0}! 6iffsMQn A$ul2ř_jV7Pkяϑ;Mͭ[L{-.D"Os70KwfVc"nnA0/B/}kM(*yDtmgp]fтuukP1&odY C"Orp3czoU_ʶu9Jm س;.54=WО=!`&??pɧ}џ cX$|KV&jAV!+b|1m` ]\죏2q |ß(7հn uvAJl/z4YO{}ÕaXwmY3^r7x9~xk0x34.0IzH88*pv)4ԈѩV HE"INR{V+m= dcZġEV ڙJ-- \&'*b1~Q3gx|ɉ ǎAThECW u7c)\ д~mGJ?֏Q"TW/qg[P(N9ʻn]aޔ,z4[-.\r=/rKNҭ nM?!Z> 7[>O:O1}ɲuӵ+Л~傚w<{G}5V'Э- -ߜ%~[_k-+džk=7 S7}o~OȇNr@Y83%7.n $ĭ0vBwE{%y A~8cWehemcq1eoEz<\AYō<|0Tba^.sC? ?WDCAՆsmHy %݈ -7 1` a߇J,{(?T4~(7N2{40;ɟKsga|)U*6=C7zU`<#%Ac‹y?=<8Wu',$s߼jX֡pBeԄ%8Fx8_V꿢EP^ ]ŋYW4}~CÉ6ioCZt- tf<:/Z֡+6{hΫ/ dI >(7*^|Ds"1"Iyhl0Ae)Ba7Ir8SXe@V_c>ŋ<=]bl"%0=`1|TՊP. Q#/#v&Sgy| |y|N cR֘n,:qwKQ]a*m`7,1Q.1Oh$k:x}2kTlvzMv5?Oສ½zuNY}'w|;W VZŘW=7P_ !s$?_>ļHL7IQ734yFČ#:F[f'*8$g?K!J:}߼w"n2]n?(z3;yҟ+$coоg[\f~~: G=(䪜48#*>SVWuam%pe'x@=pn!]5*^*z,,*ݫ| Ͳ_$onG ~>Z?OsO|,zkt75ꏔV$-I vtaLªgJVӳ+^!}Ie@ OcxrBJmB3xPo2=೟8$7_.))YglE[z5e?C"/jT=SL3}G{_̕9y6R#2GK{ yYD`Kz@-yS絰)Gu@Ԣ ѢR+ -BW}xΟTb#PU?U/%a GB᪴Ǎމu6fUvτ[1{~G9އ/2hRࠄP'l%Ʋ0X.+9zo4> t/L)x@_"R'*Uizz㈉ 9<'=r 3뮹i+;tƊR֨v{ƼsN{?XoN5;G&mDǡWKNfp]A. ϟ:lgU8ʱbC0 9h0ZW ۚB P/bktU*a:ʀ /eS W~ׂZUqJ}pE,gs%v8}PҪoz[tU%\5웨rR&7hu;F,0_̱%|j ~oX),LТAF­ZúA`*A9_r;<$xx٤bt:ltl,v'r\Qf qSbLz es la-Z_{BMoÚ ȇxWjMW晹<(OeH %3hLenIm='-]wR Jpi1EޗJ 7w-?Zqۑ\i6 $_2jReWHٮ#==m 1r쵗|7&q^*,z/Wg:>WzZi5qY^MuHA,LZLLo:m:M>j^-EZT|>i|Oq`c^o GW #ڏgCkzg/aP+z>v`^ӕ l8RB^$~(P c;NU{_ '0l7Yv+L\S^W߈yZ9ƪHpעf%b'Q $6Zٸ.Ryش+m0 3v,EPJZH ktPI8=+o|˚ 9& jU%iHٗ ߾GX !|F"$h1Y$5Ivo sU/LzL"[t'9 qxBՂ&ߠ4&k+MGBwZĭQiVvf6}͘5֕H eeVZMZ2h+Z{0{ N:٧Uۉ֫NN|E^_6R %=u}ЫV׭oF^3e@B.!OA΁~5ūGTTTL]%waf6[ʳycW2MeC#9}b=,t*,-~~z}B: 4ǹZ0121Ѝzk ی崞2:_7= Ԋ[pb2O}v-D8=0kyӤT " PnCU*JjTn47R=,ފH,1{ag砾 sYƃ 4=oG[[̑n1Ø6+Ru|CC{:LMIFГ؋辎̼ Lӎ!U% 0l?O#sW?{z75bW p(PʬVBVǢq|֨0Po{cRk^U ?~nnqY5FO'h:ˣ S}8xa@բbM DFL-$Z-T;,ɰ0хDdק21됪k|Hh AdT+ csD_&>͇n ;DWíU tD+Bfs\9i@uv,Ƨp]U~ꕲ*+wsFDQЍ5TukCPRFBϩUcyvuսV;-[S)1²>>FjUO@k-೿ª\욎GJ^['fܚNmgӘE y6kR bDT$ %B~^'+b発<;Ig~,M wQ+#:B/zr-h&h.#*Ϛ]ʼn+<&T&+9轭lwTsx=ُ8+6u%ګGS/_sc%4(wmGZkT*U;HT}~"_}bnB}+ȦEq[B5/'q}jqwJN8ljû::jES*>I'e D/!]CqL쮷8-ފH jܾJ(^]&F̍=w͗`~-_* pwvkc"Oî)#vc}.>Nr%w IDATϘ_ X۪XS+jǶQB"b ~5w֦RMtzΞ[62=7W"Vb7/$PDF0w7t1f ~$\p.Pfr]?p֮kRya_bV 2Sj͏dZ")BBSSWBV Eԟj R!b,jZJ{- .HzͫZ%_8,=;{ęGK_TXq>ㅟVS^틔ׇN쭶"n'* wqK((M#{"NƟÿ{{mKl_+QXl‡zI3\&j\:@eOYCEU(݅hƕܯvԂ&TTŘ8C$Az@}"MO zW~LY:@m\@ʋ^xGwroo@mHjE &!]-#1fg)8A-ļ@Pj_HxB]z8xەJ(#qk\<9k ڍJ 9תxRFoJ"e>g ӻt'I"H5Dm{Sg8vdw?n<GV^1-"XPgMʧ =},VG(%lZ2!]$ZwEi~>~{xN= #T6¬@f;=Jxs7Xe=||wm^ܳ4=DS #i0@4u b^,7PxbR,)F׮vZLy-hPUGXIhZiah 6,`uKujUhԆH H|9DŋZ8CmnH ?ۻ:-_}dvgI꛺!,GUra3sI:]= =]o=vö[sE?)"wu}÷NEI9 C=z5n*w-ts=T+>*uwxAc{; ΪX#G̟y0 Az gXgndя8J~] I xe%| z\XG~._[0ԁfȫv$jUp1_yf,jt QxcB0\nxJ %pzwr~ uYU^>Ŗ:6j1tJAAmK=_]]!b5PU5/ca XTI3=yY Y wߥl·ZaOy=?|֚*Qi_.k^Qmt싒0t:}s_E%n_۫WP*>US0أz?$ՇpQF^qi4o/\a~, {-b-Փ7T,~AVj|&u/8sϜKUA;L"KkTT?tJ_]zӠ b{D7u-0=hzZN,_]Q s_Hmt[`|l~WMĂ \_0W*%NĨ$R)!Q۟: E#zN mbpyq`E A.֫']Xw29Zt֔V`tDTZ/nf>+pcW}?fUzJS:)j\ٳERu("a5_/0[R 6N?/{Q| 2@T2ŦXh`۝4ېeKL ajDb/-KGVQO{^ 0`ҕ"9VlFv #NGB| cþzAJxPqKOR= V*kʠ'zJ(5Yz=^o~o4i7+3<"wvX)-]ͦAY }ie٢ p,t1 zx&Zi/V;G-h3zY4 -|jbLn][ʸ#03S7Д&Ƽyr'6z^@bӍP_ٓZޟ~S}Ώfbdz}.7Aޔ,^-Ƙo4i9 -J )QtcY,֫7z V!U=\V6S6%xа?{Y=8\PHJ%Gy <%?ş2DjtJt`%|{I<:?)/`c(8;qkI;<8ǨO죙%M?f`#bOiE:ڕ [gQU¥W/Uþ^VGLqg j&CB ė,d'Z1o 鞾c#c+{wżע:<4= "= %[*:5iev _\qu\>K =ׅUo=oR;s/XehnQCi-H#pCxQ" BREcH &y&ߧ<K O k/.^ڍZڛijk 5۞gR!s1 dGT*j@-=h(eSE^(F#*?ަy-H 0W Lfb7^婯~<-T'%-߷6l4j;~`ЛӞ>ٶ˚FaKjƳ5i?`e}ǹ&4CZ\VkIyjǯL_8ˁvmڰn͖ϐ~[͠X@mm-^G-]|`߭31ę8Z@J2!?zF~!lp`==K\7ݵ{FE`o 9M<8]$hO>g~ba/+.]m<γH;&7F]h:*7. 589"7_rne^[VlVB0dԼaP2KJ.vI `r+Lͫg`sH7UKeߡlDnێ̏>˅s(lR*ƈ.-`R+;wuS0?73=ꍯ$3ߎZk.G& nБ.df~.rK2-œ-ϣŐ*\ ĩfz,`j,QCB-Bmsyzz^ub* odZߣ4շdad 5==pZck2:5(0}YR9$ZE8"WCZHV,|đ6g*McY?u6G+Ogg+K?H˗<i0th~I{47ge6=z;x~}f[7D֘uGIչԦ3>,z_L - snCBr_y>ܠ CKMOxZMhp&5m\l0uF}<'@J*ˈx)OTm\aO<|{ar TpNZ񺷤^S*TG好z1'e,:mKp"镪M/=FR3>D67:?{oyy~P=dF T-# $ ƘXFP"xWrdK6H.w],6I6  )6.1 ǐw@%b6M6,TA_=͗p}5zꩧz^{{Y 1nOmLf4pg ٙ (۳307H!33&1@}&rBGeu`'qWm컲\ #m$ܽp^3 *4'jTgG8|Xyq˭18`?Ȍ-z6G5{4vw V$,PЁ6fOO:{q%qe "+hL>%5|ZzwՕqBeϽe,'5fZf%yT?h? GAG,ԧzWMTE!x>7C!paO8GáXY=rAmȺ#=p'ORU%ZԻ(9&/˧KL *}[Y4q)Tڅsb ܕ(l-\/VB[(EBĢ(K!lZ\Cl8pdzHl>ջOIMY򝼧D{}OKv  ]w_:Ϟ#%?/=t/ˑa/po_Ad"x5jn ԼIv a_Kwto" ub߄Wѳnv.?4\ R3qT갎i4z*0Yg5LJXYa簃aݗnGMV̩3)W~yO!(:v r^ xpⶑ<碌Ҩ|l͒ߨ3!6tWgN T+ԚdncLYxYlhnYجyPkL_zW^{W;Xlﴖxƾ+]멋_ZGգ;I$u;.C ǑZ[RыHI٣3MKH2,9!G@]!QgZBg%gLJF=%BVjKi8%*ے.+NtIܜ_<]}oǖDJX=Tyϗ17iq(+}GG1fTog!%A:pox3O4$Vj=fo&{xz)ɻ82}R8("n@9Jj^VJ}%<>??x%)OhHbr@%w$c9&h#L?tO罐UR9sP`2;^\rklmGo~ Ѷn1QDfJ6, 3u->5bozǺU ' eĮ5DB5a B^f8Zk-J*]$߃M(I γܜfI֣^rFr$9lľЮa%pS`RZֆzaDJ\|>x$ J~^~g߷L'3my:g=̉- bmDvny$q;-ojn{xɚuV&]~7g$Ƙ(Q; {cW@)L`<30tA̎+22 JK^U_}|]ߔ@$=Q,7X0BuD0nWvQI۳3͹j2R( &S &ZV5zBR{Yc<#Yڷh_} u.>!jHյIҹ R#6QZXR ԤMc2۽^ iHJt3쳽d,^!'?G}nA1NG"{5c]Hv?eTQ^>$]PlqGMUrV#Ic3k5ms/%)D~0DOGzrЦJG^&`nW%zYl,≼4TH-QoT<,1Tdub(euJ4_{ S5?g.}kt5ӧ5V !=4 Xh`}kU87l;j &랤bP t.ĬoJ<9 ޟp`=)W^I\=j8,\LMuc1vm!y*ǖ y/1U!}L^}[T/ *WBB, 8Q%PԸXoUI # VrC~I_a8sv&-p>G[^6e=~ƿO; rXt'ƑǸ7{Ԃ-^.YayyGb²?4(=\rXܟXȳ 1Jo} &f#J>b8g"@Uc>lD6;켿&d6`ra@؞9Wc~y hmՒ`̅+x.A??.CӴk6[x[6RZT@B7xvj IDATdb@%1aDi]~OHYVh۳32m*0kϧU%RF"Ym 1#ג8c^vM.^ #[TR2TlV%wu2]~׮q:8^Ũ{ ['Z1""6ݎ<>l҉`y!sO#aY,fZs7k7;I f|*Xw$ / '"y,VFYWUWɘ]۹B i]ھw*s= yK/P_XD3ǘq`L-Y]ݢh6(+ШֈSl04ݷ$΢~!z~QxsrL$"grl&̻ K9km,_`R^ ˩+rDH8ITKB,l2rИV?-iL$%7W8e۟HAN%$q2 u܌DJNDtʶR!kJr:7"QO͠nz!KFM{8ٻ] rU)ȗ+w6 !}h s-k-Yks}usK |β*<(f1 Eu^0e9H:zw(%;(X)ŮŇ;L.-/~C` \ww7 2b7ը1Z ރGDJWK.,4Ͱ8;ͭ-z휴˭N[ɚњUfPyM=zvgqgilMjS l\!kC"qY;\;C ! ukqn&Ǜ*1b{W{7=Xn厺ƾz1j>IÔm& 퇗h>D.箽+L$ɪ\h;I=WoB64&6cٌoٺ:y<4d0k'*8(b'B^tuWiU;5`bl@D87XNSC.>4\A*1^NˈZt'Wkf=޸qMD18|;iԀ$xZs4~ٟ~gE5 {.=E*+Oޕ2|w{!|ftxgqq`Bl 1%vejl?> VzAcVgq,7$&{ #ulB! s "Ό=m{#OAڡ-LvpPXB*^[``%q{G[QNJ0,#;6|rHϨdǢQ%uK9.\~RgO!Q"&x?8l4LO6hjÐ^Z:[tnΏ\,q~~,Yx,L 4hJ*MYYlʯ퇗h}Dպm*^-ri%@IHT+U^8Oau!@HRYr UZg |aC#"!o1JJfDIVH4l?+W*J//HeHD'VsͯcyB"$ Hm8e{6y\{|@*s 86FӋQ)? By>mz0^+O.c9]4E|| =̉2ӥd'^ 8>߆g[MXh%okyo >8Z;\dNg8B,*W)%zo !LlQ$yV'@R_;+D_,>$Wj^Pdz$$ $.cDHsgѬyt؄[Wɼq]W AW_<§! bZn. 6m1>]5O,ySgDI-Ri:G͞U\ZBVkBHHaV#y1-^Kًa?`ZaչI06*=Ǽ#'wj\أ%uUv{3-4쬵駞u5ANŚ'x)johz#mvAwC/gQDp 'G?֎FkLkZ=0K7K_w 6hsKK4A4XwG zPyVdAQȈRDz|m{($L1ye}}*1nn ڣ>y7%rϸ@Z yj@37H`V)bz~嚀9 hwSl6#|0zUuD0 lu} Awl,\<憔ga.<$5]:J$˴Gc-9י#P$l RB*QjHcw%DqHtDkv 9H \K SB2;ɾ91ߜ exȗCGدA/nw קXj:27 }nv+ֻoyj4(&%eD>oyAu4v2PTUP~w{H9v;YCO_s_z&5A`gL '4_/o?S#g{unqvGmsU/jk;= `mm{4ǩ۱vHtۖʋ(þf/f}fBdP1~שB*=-/=4Y-m<9u6 dʈ]\Je5RR\'- HIȝĽ[IBUm me_g,c:`*7])Gj==~)m`S36 v M嶻8 *2>3bQ g {|F<&;na ƄWc5Νd=\ԅQ\ @"6pUI~x*bs$M=0xơAfv*6z7Q~#' P~ݪ`*%ح+BR(Ryrd0 I9@:"q !VG2ruO\};\_3j i&rQiĐjF͘]p0l/(L:{Jl^8?/6pU$hxtK)^-1>tmL&.=Z:1N 2]""+Ȥk &\pdw1L;矾YڸE.+Ku&H^i-D˛8:͙I>s hLsu>q.oURNf 'g'C N ` G뜙ZُGwLȝ"ű4^3e2FȖ 0Zq&,-N~ ]RynivQ '*cG9qKP*}zMH&"AW_GO8sV1!|񡇙\v5\ٳ &=e(;hgʩTZX^nS1a- <k8kO-f| |ǹ永y)L.0L_8dK75e] \Zry~YHFUGsR'}#P֜-3EJ;_.W0H`uv"kТhFF25HWgj^D+!Z%R< Q,QRPԮ.i3Fg68iQZ<)~B@>i%zQ$Sl*<1&s.3eIgsAm^qzvJ~O?pd *n';ʢv'G#,=skj Ew6gf ͤ92yNV'%;ktL?ts^N4b(i(Rtv|PxK/ Y>3^8n Y|Ǚj 7J7(JaGXwҭ%C{VBBۋ|$¾^xf`S$Ny2XjuEE]  k2-/y>k玱z@dP7iz)IA%lJeR)-!V8LUڭsVuVH~P,f0'ի(QV ;Շmk'xQ]GA$jR*^du*{ )c,6 {yb$ 멚a/'^[*j۹_+%z& ^X$proצ,=qsr;-r ɋ4`O a'8OSbT^knCThΏ`?`%33u^8g?4Y_Od-Epq~VpQTOv=?r jzM H ȑ޾1-p 1W=XwVlTqc^2;bpa0p0til4rڭQA[4Uiͱ'^vpdڼ'{]ggqF@cR ^[צO&Q ({k Ġ=R[a(:cSWnNek bQR>&u FK[CAk]U4@Yf\/%$TW,L!JRƀ8SM99GD2g :( LeFV.9i@ 6%%^PZ5d!X=nCњ׶Yz?XW|>(%g+k5w@>xR( Y6[уsUaEYLZx DrvAɭzTIʍ ,=ƿsTZ~/:te+4Lb b=7 {i3(m=߲o\r 3>v`n_pbDr^XQؠHnzYN(ڃBZ+GkI)FIujk( R=^M`Y ~Kg>JkTqI%pV;A"K:zWgݜOT\( R>L.V^aWggS"CԺLCZ-i5N2S #ߓ*}e,/"JS֔=%BBV98Mctoq7lFGG^70ectG/(Jv#t IDATMGfk* 0 i~@XjQryë{|e-90XNxR= X>{O|ZCA@םXNwDSOJO0(=[>|5&gg,'9$T~YccXȣ!p;IKl~< ltZP~QWkf=7Scɺc 8>ϋMm\\r%R+nD 3k!mz,Jy{_G(^[*F4]s۽|Y߇͆Y ")NsjoZc90η5>Ɠ/Ф] G1r*=ocYXaP?gU2"s)֩\ <ٳDI9S(]cat}PZ@"9nU@6Jq*(NKtȽ ::bm W&hRUvЏYGEҾoU!o`E2u蚳,:ϦoUNy5i29$u קx?o[b 2LbC(+VTj5| #0L41nJG=.oQox4 uޏ8t Ҧm~AcWc6!}SߍvRu%S^ ҽ~?=&%gF )GTU;nDx;"q T۴JavN"!qLYoaL4ti!y0K(rQlYoVGP>p?nb"X1{`u/y2N@}_RcQtbS?\o?|W'_bUZ}|F^࣭;]g=燓#WPzWivZYP+9HIԇ>lG:h}#S(5FPoXfʭt 3=pz[ߋiL<ϷXɗ4oڬ˹I|AmN?41؎c ·Š^n؁ه oz0猭lI84AHZb&zχ^ SƘGK;MCXTC)$ky"gχY+PHMVe6Q$8뼵&_x\zS;1cKl{?)z mTU`W/}&h0_F9L51Ibc0#| 4qdk"!%3ו )5@ԿN-!A~&߃Oa"9?51@!&f9]0V[[0;1!ҒoLyA썻->CI!5-ˣ,26{IUi%ȯA Qu6|qh?3O wt5"QC똵7h-J"> `-Y>1RV&|e . c+ Rc|j˗_-Iy~v^y켃l;VaqFSju'(Tg곇>QY^b$|cJyE\'o?ZDU{~ F%11[Tv2tv*ͣmt>|k}~aڑ8 7ylvť9fIZ ($RÞQ:CmZot@R'W<ѴgS،fc+wxvϞdeF',]捻[ϷՒmdYծ="˛毱kVU;``c:i,Le厾ľi%~9 ae!j_Y#O"@7nHh_/)i'(B1>AB\d߅0w2\r#4<'G&{U9R |8VVǠ]ɣ:(U(z*XxSC~'ԝʄ[c/@ +')D֞[Zbfoqře—'z}pWyCCXgt-|np׿ >.`ǜCܹg9tF<1kU:2N|Ž̛e[פ5Qnj":3S 5ƜbZ_gt K<tm]_b[ga=l?*wgjqP,y .w  2v!D`h%ZhB\V OK,8LݕyI"6^jwum[."Oj @/ʫFժb3Ww~Ư~[-cX[CO{΢pcZ*K%+_ꑛXvj^F;]iZg*Wk R #u֎| Foav,2wH8^A~ܷq,$ovp,7ԁ+#Fܥu98Kz)iKs  I.,_% E\Ez,t ?C /Nȑ%a5#Ε(Y(d@;lf'-=/'j)>}FR cݫ|_@D`ʒSd۶zCf!H|"u{P^mD=aGYxR^dc~Y:3LN 6^ۄ83SggC鷞?JefB(w!6K9_msMy9B-y]&4um19`^.;>ZF8}Pta@Sɋy] &kO'64RB\W8DSӹGI~ O||(S鳪Vֵ^R K2M:!IzŘL[MUv\&V!z :^; :@Ջlp4y8H,0QD ޯj;pdѿx^s4{!_c} [ io]F1?,<=:J{ QqzxuXyo.g֣gǯ ½Ǹ31WE0Q;h߲K ΝG'6z "pkIb+Xh5h>oIM͟ߨ5+i@I`&(HKFҷ۰j?4!lACw[3d$ugU0LQpH!]֔`%vtVgì{Guwt=%yGrRKN ]N*LI{>┑4+D@}=U9PU݌% { ~FwpX6O+IȞeQBLlu.+PW7+0&u?ӗe׾j–áX)^ l'ZZ;:]̈=O$jY9W>_!k,"S?/@d´չʿ?; e`r?8Mhx[Ps=Q33TSKluyÔZnE晬{ݢiEmsRL [z n?WWf5J%eI0WF^W&s:`j'jӚ/{\' H?6gzހH$&u/ 2D=]nz8ḵΙb2ٲjLΞW6߼ۼǗ_>k]n무 j,Q)z]_anɴ9oYetI/-01kRh `7zgv1`L*5 F,LY TEQQMouȇ˙ n΃,i.EkIFYZ QSJ.$;&?{mz1I,JKTy䮛 hߋܗ T&jyQcU8L;.FVIK7$qD;/,o^c'O'$,; u)Y^T |2ÄWKah-Pˉ-=U-(䉠f>I4#2{C8W䵯 v`^%L$f ,wGsM` ѭqre^B"ge-R S8&gNϧ(/k_?y~O0m/}ʕh:݆!ogµƣm<J*2$s`LJ{&LQ-! VS9uq)׾ˤamDe@g S~ؗo<[_[r/ª}-#)= {^X9B]B*{97^bX% Ҕaˊ7ă$U c"Vt짉>}^}s?O^G^nZ [ktVE&lȮ:ipӥrenyMVzu׾KW DkaaӖϟd.12Hmc?O =lؔڃMf&קHY:t7;\F1ɮWy?jL#>ڣh)f4&[uhss8zs9^ݠ>7ceK \]Α^m~K rkL4DE$һ(*{vg(lBt`z/'jdo{vt[<-0>@qi8E'N2g8䎍iIg=gm& d'E2c~+L6&|sjeϞefGlryF]{3348s5uebFt𸶾xzBZ2=΀1>aXYxd)]C MjZ Q7x# g1J'DU5y£` +[zM5d _R[瓊/@FLoxpN/ZvbbHέBJd 5jL%$ pL'e{([LRHq5?{oZy~)J:IH9H(j uDۍMcxbk2x2cfovrl{}݁ ;sgnKLw 皞 Fh q҇#Txǯy[ݭoSsy=x^J RJHAe`,Yu^6PkI9kγA'8=$;!>) WNcnQ¡%{GFt=ô{7TqEq~Nm>U'~GUd{{f["]eԉ$Q8m˶1B^ѿ|; &.]_)q|c%>} =0)Hv[ IDATG%Cbk|k7Ѓq ]f?|g~+;sjkFԫ_|5Pp40Ƥ}af3$./+'*VM΋> \cf;Vj>$!ZP,s{[)8Sl#mT*8X)*z񎄯FQT/Ӕ_n%#Rg,4r{g&(oDSI<խano$%!aDm^%)$zB\sq!O׍A8i#=% Gso/ ɓAf05ޤН<~+G7X{oΧ.,$1/ <[ear+ m^9҉`Y>踶"4j-Œ7VG+/ [,|ifivW$}(w~fw?<z/Bgϗտ|_—?$d/7oe8>)4/;R)_ےmBT2U>jhjaڭdQ5!Wb^psz_gP(܌4ᷞBu Cqxk/H[su&\}.Lҗ) n^"ŋ @+f4/A Zm$[d 2w\YLj:WpQn[a[T|WH@țbyBLV[񪍝52^H'ɼt϶30yGQJ&A$}Q$0?H^yɡ.bQЂ(`2S$rOf]!{'φ.oXI`ѯa8J@ɥo̍|_z\MKe QS ՄA/Dov|U[b~ygyKNa`/ Iyp^|צu&+gg}k^yKy-֜:~y5ַ k 6Hz^mN#p;D rm5%'ز+o oq ;%ߨs#.%z0;\gfq mн:l0A'4FDg8݄ZDLk'DضBbi^Vv]\A'EyQ)93޵ildg!WTRZB0mrK;#' 5W]Ј;tB\%nݺեF hU]&(^NqӋDZin7i0U8ɒsJ:=#!|fO\#nZ7u!~,A ~}›)n!w?9>}*>m2<[ &wXgrD-츹8JBRd0  ~6dR͆!kA\OrlF!Q>{U)T>'^x!S󩬬d`Sky'\ׅ(SSfX0smQ\?Eq-喕]mRB6kmlb"5ZB"^QmU\(Pq Zm0!YK08i $uIkTl;H;[ҽÓ>63 ,dR)# ٰ$t6a@0Vr˅h;oaG5ZA~e)X(dbl$pqLbOsdwΰ]>όu^μ(vs-aÎH^LAyt`r X{&󵨻 K0H<$qhxa As^.zjnE\0dybuG+OqWp*+}%Vテ[`yS_x˗YN3jqS{QrGGPD+Pq#Voq͛r}Z d\/Gi!.#F҅Ծ0Ld큁ݧݰMէ?K*E^Oz#T}pNvZFk(/\e 7&ws ,\u"`b3ssD#\H0w[ct>"zEtu*ًn{CT!єGV`… v@}瑪Gathg;ZHVsXg#Q.(mƆO!R=C8"s-ֈb 26wyyYBe{Nɚ#iL"%t'+,-7+RME=ˋ1_t=E1׵ә ˔?IHdϠhQ<$Osm@6GBGw[0Od,%|POv&n[!|[xhaQNTPsztmն]lܲTa~ySOxaL|mڭTN%wywn:f3ɇu*0tf*T}wuj&vȣq{; p~㷾33@2԰a@y%gkԚ!/+/۷y9,*zJ!m|z_C@윱{ZRbpTF-w0{͐[eDg;@u&*l@r-"]0hX[jVr% Z"%HP*7[duh)%R=HG\pJBlP^6Q4F< Pӓ z0-N&{,b^j<9 ^މ[b?h*i"`K\ ^"1;Z۸J2W>k44Zm nѸAX{^M^xi`,-04GPc,-sa<@hЎ EI_FUzB.!Iw {.isbmthkWnU1A- 4ˋ/m5Dg- *Z1\&KǙ|FΠ$y;)?.[k)*@\*1 ˅!FeP4>hSVⰱvq$p2LClRrq˙p('4UjynOO{#D%!xث^Jff;[N:oDu]?Oَ 2 ) h,b@}0&{Vr5;)2RnޚW/ͪͧC30 kFj4xS Tg|g^GDkmxbA#L>%WQueKzi8"p@ݽd  ow59% ;LFol? Wpfؽc<{ ~ /W>:vd'1c+U}uU# 4RRYpyq`0+{1ٮVq>햐HRȫfJ 3 3je;M࠸{d#掤eٷxlMOkŵ6Q$7aQf?g-JcCEo.y zGނ/1lLUh4 _}C^qH>&n}kË_|:I8m/`b lA=)Jكۍm \:Zfryaf]CqT0 |E2"qk7E gv0ޥ  ыձF=⦝֐V 1T|;`N0FzXDƺ{3Ḱw8.6/_{DW0_JTy$Ш>CZm d;"T]y\sO̲05t{)V F|ӃV:Q'ko a8 w6Du)kzJ([%k`N8hMԊa4 CL~0BXmD:)EG]}h>,p&{y! 3Ȑ<鮣$II`^pFF)Q+VE o}gf*~5p}jll:te$\а~E\6+O,99Uw[[M<…1Pz19z* r2 B0n{z#8e?;yEL4{"Y C06~ZKzĠRƩTp\WQ)9Nv"G p4~WRVjRBj0 5pcY8w4u/dv|~Y_DNH;q:z>ud ɥW{(~* ):6$ JJ-j>HVCK WŇںogoIV>x0:=H´ E ~9k;\Q r~J"|*0K Ոft mk:tPZqU.7A0}#=1<4ޱ%734Z"Y;K^ $ f/ұJ֓v:RWm|}jNGrj,I+{|o?y8^/(Mcu"=Y /!fK+E9~dX?tky ;z# se c*WGEuE[aiB`#t@ IоGcfBʄ\z9 !«KwmvdcHl38 h!3el/etTءD㻳7ٍ''Dwl AIeMm|1:rXW%-i wx )_Cq50/cgBu8J\H띸nHo$ȶ}M®8Sl6ݽ患B^A?Aݱ8 ǑwgA7r>}W^Qݯ I =4h#My44n|$®w]kr=<_v?wp8;:F6 IDATi"u_F׸;9Ao_aa=w 9/ƽ8@a 퓪P0+Xv^>uzAqdV{â~u-~*H"t0˶7wَBJeJcB֌ /Hx%%zvjWh*r}i v=:l?Ǧn"}QnA&vdSMUDMi;CNYj,sAS8^&RcQU,냦ٺ!0RZwaT !Vȁ8vC+}=i*_;7nsDe97Qj e>/yq^AJb. El֤>@)$Kń@)&Q$i} O Nɀ%{biq]4y4UQGefoZ:A3q1v#Ы F>]3=EgNHEh R#,[[k7ָs]f|>@Pe-3$,ocsoiRO\ϫffWt8}kOߑT)­}L򄋿J_y%Zfwg4KN)Qsܤ2CΟPXc"Zً0~8E ByX)1pAGAހ+`ka0?9$hr WƩ%:[2 v G?)ܹ.~7>Dr}|r[+? 7䗄<$JgI{B]n@ v;{ÜM` z{k)M&~KD*==%$ b)^=]W!SFbEl@*ug!zL7~zyQ$5YB|ʣB>;:ѧd\[BcԹ45lyE`a^"dz~}+&űc@ţSy;XWBcyN[*o>UzI~uL2;=ۉE@$ygmވ(n^eJ>{(aQGZo~,o׭_u$Я>wq֦´%ٷ{r Đh//,hC#՛?;vk |{hf}Cn%wELy6-f %+/^%-Χ2Y.$ltt{%VqIWwUbgObv=TIxZ߄i!+^z q!0"JԸ@F9cC,*}Ȇw@m)t5)Y2}FkTZ)QQ)qG0x n`$ilɟ7"d!æ㹼~~!?\/ں_axmݰR[p1q OUb>rCG])jm|__sѴ`fKz[[xWSNJDĖoR2F4O]9ǧ.sSUqؕ{1㣆C3r8DO8Co3u8"=8}\~4B zkIlk5Af,D/闞 ؼyWȐ!lv0%zvhB@:kC^5ȇdqNEv*c4=J-:>>٩D끨\]7%c HG̩m*s|HfdEB0P 8'NQz q`v Jɐd>F;LiHx ӏR.;*MJQؐt|}>LݻamcxoQ G}GŽȏÓ,f3,Q^NyO^|09՛LUlAlmݢ\`|rqp׋ \:/,\^.SB=\.,&Y1akuw" xdh%xOBڊ*[A06ꍶ8alr٥! dk4|#.cK2/LbFqHߗ]2EsG9UL]̵@://Qd%O Y) pЁXh_<I>q8M5)?8 ݮ8s ¨EÔ^Y#}>|\qeg_XFpZEg09AnDR]deb ?1 Vv%*80ȚW8̵7-S7փ]i롟!kU7\2R߳Q}n:$$jBii}Sv` Ydɒ$kJÄ`X!tZk{ı"!mlF H a8gUv^^I05 dN{,-OJ^GeA gGaYGjiߓ?/yHN~!n:7D,U;{5sڳX7]ѩ$[sod!{V52Rz]ցK렷{M SLNRJf:q*0d˨RmdjUCFd #-;V(" ^Nɑ׳& 5՜.̵N'u刢B'M15A0DQ?-3pwG׌^@+#4X ^_A@+*{/^!&{R6Qpl@~b?N'W{">$?4ÉfvWpb;,[a0P=a`|λ`h3t21Rz{]=&,ӗo?䵟~)v }u?P&u>y^}w3j\,B?qs!Uv![?7Oӗ-Ҍs)M Җ~} ^ϊ9ڨy#r.gWlBٍcv1_Qh=&Dzgn{[&}T{@QRI$SG`*6s>y*iԖ#Jh="!}g荴Ͳmw( 59 p\7L{0,T|7c\G0c.__|Vrm轆Cw‹_o_~7VJN ~_ XͶ(_(BL%xy ]OGw[䶭^/4eCH̾zXފX&%§Uo& UiЁJYUH#i^ vi1ViBD5 4[Rg:T8Fg҇J5oOW= 8*% WǒD|# njÛ Q->e4++ş~fu/imaap\x3 ~ {dV&"(:f!ex0uJR"M1zs056WWU/[)A ?WE{E;m "z&mWOS|P[㗄@1ITz:iH,d'0f3l{(HWV[ R ޴/ϨNiH`bB^WCZ'S(J?)RZCݤnjXff !oLWJu08[$Dā|0ĶJ}nV0LRܧ.}B>Q&>VYDNZ̥ͅ8.?ME Oa/&^1 V^{von/,,dVL@-xcHG%R8^Jv8¦uJ^B!^:Ev>pU^AVrfU-$Фժ=H뒔m&Vie/BPNmN'tSIS̗i˗_/0罊q!"]*!Ia8%F#hb{{U|RmeG? W:@v1("Bs!|_tsUO$4z@eGwQ&2v!'O2- БXvn2y`i cTgfZyrmo v0'!鋉Q陠w[ |c_"Hk&<ƅ^Uo$,;kTcEEa+r(%ΨznaXxJx0&6짤nqBry*Q" ="{T'*taKr޻oj@y43h8wTqURi3٘iTe{QG{*0pQBB]DM>?._NyOaV3VׯlFع*O H6;[ݜzU x $- c8!\`T`<:Y"Ea`s đ䙨N [zh`ą(QFBP! RIv5AƑȞFL?1M R<9~`G%|E1>DX5'X:Ҵq5h [RH6jX'bZ?dTi.#stK l櫓3>M06 ^efW}Wx{lƁWWlO= N 7%PfptDt$߂hyW@8Nn(%Q9_ "FVr"`&&=Ahu=r4[Ѩ7N1`d#Feʤ0q紑waWuMz(S>>,RrʑS%G}CÍD mgAsoDCK"wPT΁Vڇf= t]Sv)}gZ#`[)e٧jxs+&ܹ..Um V뙐,Z'ҽJv/]Vw!Y]GTFzgAK cI\a6Jw{=/U)Uj7&\wrw$g`HȓK#a+^dde # *]Ȼ1+NL1 ×(H` $^IA6.C~:m$upax̺9un>ev 7WJf>8eZ`dY觲gD2?C mo?V c IDAT5aO~)mnBmEuQzl|cp]9H }sQMTDў6}0*>Nߩ)M'qy&@c?g`lr,\_ 5]mMYԃ5$`qː$pG@^ P;?x4D&<Ƨ~Z&TBTUWn05xf R>%j `U#ͳ@ْ9K:)*Q)̧==ԭw-AӳJTSs|_YNV㮭 {lo{,D!"38N [sR{Qáq$Sɥz?)wOmZ+AЧ퉆j0l}`!i!`8u0ܘ]0}7z4m]aꥵl !gJc)pctl3i!kXK/v~ȓX$QDD4C18:E;a7kapCW!+TNRu=[ X˹qg-h?ty}Xf ̵7h}OZԮGzGz/AK1d|@$ztxr6!ho &\ۀ=;K̶h#U8Kَ÷lOOQ~<# !=hG\!eq#ubJF;x{z;A{!v/ygOVݓ_ҕJ)&{7i5@k{qnl| 3u.,:bq ^>)bqxhJ ᣉIjNG'G'џH0̸'q!0~4pv(X\NruD:K =U?"}ffh|{+쭐zf$kfҕghoT `\;,l$~X`B$ݣO~"Do͎#j4%xrccl_d-R=cG9jH0"ʛ"$|| 1z=-a3Ucwg F/ )UyLtpf2R=#{ݶo7iY[x!/|+2SY( RU.ĠhHUFBZ{d ;-*eEiJ(o|oWy&;.Һ1ynB(@h2v 09Nj=kc5!.NJ\%Rvlfǘ|2Xa-rE!HtfZ$Z.][$HMCF+s稭Ϸ^6Jʉ'(? g-~fB&Yd\r"1!VeT hnl ؍/OfF WRs"+BRB=(GKlׅ#alRycc^X,3dVНǰ0R'븑A^NGï\yFCdn]EvɌޫ6 &dק%[ m䃊Cf?-bSc)䅹: (B6$@w.-NE @?l& G(NXPPL,߆"_|br<%6|Z[yX$>8h@egCU(hihKXaq28kkf'̓y6{Cvg{˲,a<0a0dpLb<cFQO#ѫ[MjU?9z˼m ]]uԩS|{9s>bFeqP]>U'PݴD[}}G%1뇽|>\8q'XyGP[^enq^|IS<L*uBC 1 6/D%xХcTTo{DWnqmͥ` Yc؅D.A $z SSJwSK3&"HjU6@8Kj|"Wҗl`[* ?G{|U<2/|ӑ֦Reп8nA _񙳧sd/7{á^gNr2pҙș1 af~ Ӑ_iXMpAD\Q~ {< iIQGƈcO{y $9jQ7D>K^it8z%f Gw@允?r ́ [b&~m/쵿a1~$ǯ|^|qk Q07R*t9}K=?]wj7dWR'y6Lc8"ť,i,9R)x79F2yۍm*2E*KvST-@ewSDOEB)oϑEgq۶n j4S":@)[J}`ػUou#DE4A-:R7U>zCZǁv,K\}\}.\k"((;S`4Q*ߵ de$=FKC})fJ,S+~|ϾBV 7Acܣ<De&p$˰\Q~k7|{ oN*_3C,Y]Dz,I0k h ;h>gS  ]W*:A|0"8;O?>:(J=R{xI}H~kp-?x~%9{hi|L}G75)?zLVmFe,Q6Ě'&F`| ټ03ݫeMnj0=Yiiպ FN;`[=}"$I@իt!5J* { 񾢣rX3pRnŷ9 A7vk$y]s٭{F1LT֔aTvz<iB1 LDO6[k["qP</d$aVM$5[N$k?%ȗ hJp#p+vKl)ç}BZ}\9 s@omrN+?΃&\y p|jJȴ!&/b4mpvqF-¥VK[V'\:m3~ז7ȎY|) ;XW^M% ,ÇCNu[t-j;nk).L=ޭX[ۢ^a=!/pcy)BXB}db!˒/L/\>ZC!wUgDͣ"=Y>w>+({z_y/yak>=P&t N)=a!#}}o SO{0%vB>C>ieBz ƷcNQ l^e*s}IxR'A2g L4%Οd4 DnhQ>{vgGL(јH'zPӆ-ܖepI"?|!<A{?Y ;[[bMaQLa3ð=D tTڝm\bXhh9IFs6RSsdr6Ӆ<$e8m=0/4jBtcH&d]|77?N|#jIa{\u6}1"z,cبͱ1S/j_DW mcF\D=G=5ř)ng-4Rr'Yx,ۍ6ͥ۴epXTuSG65^|Fog3g)יO뀼]zW!Ut01#$uN߇.z߭ l5ea @EkwpEv_,2}&D`:VjdĊ|cҜצ?͏ C<̘ai|ɇ"{#"n>"BCw\|S=| ť)}deyNkVk4{?,z)Ni#L' mzAG@E5u~Z^8 a'xIa'釄8Ut"{QɃNпAthjtttqg>{A$&~дTνWۀksD4ߩY?֓`9תto~l[-FMr#bzW8O)zCY`0=7ʺ*ׯ:Xc .Ip*_ꍻk :XsxZUfHL*ɯ"rrZ0V &ݦ1$cAGşFIM3z Vy: p|\Yopm6_ ?c`!@t>26W Q_Y<8gm2`]< _ )By ]GF&l\ƗS[@AP\%7 o!L亸vucZxvѻ-dL%6h/؛e! Ua睖C~VSra95Oi:ndL#AD4?o{cM[A6S@o{)~n6C<t0 H>!"{~Roj[_`3_ O Ak4NVIXcb.͛k8D& 7&MmSQʊ=K"2vi+[_ˢ#\׮IW1|R6$q}FL@ôUɖ rb6_8͏?AgXC:H>oCC=޸|rsKvz=|MLյ;xsSqb5, IDAT $,YY+C4 @S)+ Adr[~IN3U3qTh;{`z&WvtMiU@}>+zJ0ט-DזC:g-R)^鹞q'0$'4*^?_<&6&nSmxyl BDOŐ 0d~.=ZFD:.EIZ,s Suju݂Gս0¿oDifJW5 ^+eSDO.aßs^^#Id/Uk 2s33@o(ɼؽPD{ЬC*#viyEm_Cߨ 'DjStn Q _/H 7C  =W[?|;5?2Ї?nC4qA>b<7n]'JL 8o4g4 >kfj/vu0 2R7'?YCo+'hPJ:Y+LuҲr©qEgt(eLyTvܸS!ʛ^2J)el: 6Q>%&,--tuVáP _d*k~zmu+ِDDA7eC4)jОu8SZÕ7QVy &TU=ezF^\dv~8CnXZ^rJ^!*NjxL'x˟0}yR 0{ÎC +MOeMm]z onPEIIzz+T6\lܺ'!AJB(s?qӓP<Cۮa&D/I:ri?h"| 8H!n~hԔY۽G]B'lw5&o[Qb$/]ǃ fcE XЬDwݣ>ɾwگvu\^hԻ33У oA g'p}*u4`t *ATjAP0r7(@ F Btw>EȍD˂kdaLɍ47Z-AJt= Ypk5A妐)2ehN%=hΦg1BG zБ|m|}|҂ا0qn\Zo6.ͷh>UB+HR"or c^k \Ð a.i3Mqq DţhAߕdX <À X&edmpu_a-$YKcl|hxB3}f6..^v>I& - V*5qB#hyFŸu|+nRrצxgJz*CC=DOmqˢZk ){1T=I`0/ag8e` IcLXAN3w~b!],2bYRz7̫&|Uv~ϡ?c/!|A ~ +epSzi2$z L39j#\hy&Ϝ}3$zj vzCTE2:^lUs?: "V)E27(&: rF>f]lbnM=b}t!T6^4GRӃ"GC /43y+b-׭:E߲ZbliܺA=e3[8 !+<o3s>N`Ϝe1paz|zUFYf8x,Kj˫T`MPBHn4E|?T4"8͙ ISS؈4jFJSr"uꮸAe/vi5c=7 LKNT 9#)*"RXtﮗLˋ'~S}9AW!z& DT1 0 6uYP~w l{UZN zfM6 KtW]k\8g9¨]Ϝ T2ݶ5qn]ɩ|J ,MGD1Ad#߿XH2&zmUє0a]<Yoݧ 0EJYP:>NhW|_ 菊4 KC{0T _0+{o<*_mE*2 >>:z q7dۍ?o#4idٙ '흵:c-N 掰udi>wuk6b 12]`Opź)O[[ 7mfXW_XڬщFmÄ́d.T|_%OqBO~w/N^ΊaK/ Kua~@67L*F1 6 ~v* TŎ9$ Y)qm|TƮSy∛j*AD"2Td%ߤplqa`lqS+ygkWnV1S*Z1W [e^YbFl"{;:.Üs|i򥫼?}6}Фx!sBя_bGeЇ"{zhƍdvg]AnkrGa$gl3$j;N l3ft$IaA_O6'YI-ZNCpߟ8UtbGPg#EHׁXA#mX>SC2' GiO?[$vL+VĘ(VljĽ |"0KT%~zz'U2ZtO_tw;hL'k޼ nmr1 O- Yw]zw]H%M~Yzs-hmv.LuiaQ4M.~r2W?_Ɏڂ,"{E:*7nY8PB&[>z[ v1LB@|o7mQ ͻ &O?$uqdRoWW t9~ߢ"Z;FKWy񥫌\QH$LdL$I^o? ⓹s`~c/Eo >xܟ6'ZԤPtVAX`Ssc2K*/ißjQrкqYJ8-úJ a3ÎpPa[ʍ^X@MI]7I<_t]#XyʜՓsEN.ˋgy~vCykԥg/P*hKsn8W^ƯƯC«l0c"E=LM*2;;WYK|UOpvȝR´,ɹgOp_ѼvW/_ËC=]?ʹ8u[6LŃ6Ɇ&m"WhKB(Cn3OKSL>9#=<:]dBY~+!+_]27~xY 瞡4ePV/ Dʒ>=,)Ch)D'XB|%4C|%s/=eMCYIqW"?*?ޣ{$0Ok Q1nzy.^x{TCaH#cAjNb}QAn F}3e&~yrr)&l i`鉴U'j8%ǡQgZ,ѹgO0NNVrˎcuW! "wFs STK[!^_{~Bn ,W]Xr-M 饉/`i] bg.]ajo, hg[LD"hBf9<P ŗ9ېj"qGcWEv 9ψ]ge2wab0C4ӈnek6Lq^ : B-T 3 (n΅(B*wBtEZ@RXw,b0Cy1ZH<ˊ>K瘟=K+3srNnR12u }ww6?D1 v='X> C+/ ?gOIZ-Tνp^DUy/3?WPd/`9cri=N \ˇ d*CE=#+sn;[[H`<__"`C06ɸ-U [*FOfUתC|_ F#Tn^챼B_^ M?4s0_{ɀOO,_AuO$nY͏O} ^Dq#~'y^Ms$x?7'ϳ/'yC\. 9S[w]4 Gӄlc=g.G u6HSzAD F|-#o[ܙC}|rGIR[\͜o4ilIfrTQ䶃Zo9yY)Mvʭj8T_ KBDEfa<.6 ^>':#Sq? ?"{"JAXxܕ.&ߌe[^X?ΟS}*tgCom7g?+׹򉧹ЪzpHWL8KܹmTL)./mCj$zt[ᔬ;.;:"iĝuAsIVT9egK̟?EѦ8XJ.vueFr/~\8QXt@'M{(+C)wQ; s@ iK?PկMH_㣢ő'Č/U&=NukayEIfjv¹gӠA;§ju\Cc뱣, ,+4Zڝ5$QDK3.V pLLW-8x^䧧ih1_yLCIh,jѾWj4"C0t4à},Ɏh|SKWiit}BXOE4C*|ʤkP$W8>/ ghT,SQGrƑJ0ѦI^56'ƻw  vzW*ISD]Jq$w^$A⪟B\S/-ilc +hڕ[s^Z)?~r1ffp~qqPdOh[thɴƆs8~g+ܦ %NIo<G' _iW)zD0~C|K,<)gX&s6ǿ,Q7 C_X|% 8J%6Y:]4}]9 SI]Pչ4eX{`D~_z,= Dwxд~JAռ~^Hﯣ&XA25D_L_ӠvWt.أAy/z D8vTEPvijcjڎۥX*ņSkAk)A24D$$Zũq„,] 8_(t4BQ4j"JgqfnT,]yuᛷs(XP $WE&,ϾpR)_ L5Kı:hȑNHR =3t\*;]Aˁ'̸)/ =H^?UUDob<Avൡc.#<_&Y4ы|~d.T>exm;>MTqP8g$RM;n{ IDAT=7]*U^) a֍Ipd]cʜaMG˪DNn-+LWJ`o^K>L H1H鋛ڿ,wl*̟?|̯eCx rEW"?ŧQ6m>FlR4栏}~xo0iaB@qm)$z.Ìe<_C4]:nF)X"q* Vk\f2U)^֒i&ŏ8 DEH$v*"(ft5qǞr=`it\j E 4:i&̞O8so׺@סYc4[ alM)}N1 )Ny=+9 $xqhx^H亖xTA"Ba-^$OA<ym!zaso<|iV5'H>i$"h#F L}ɹ9}4{t6l6\Y; $UKk3ϔxGY|gOpFyX~pdh˧SetIn1ӭR:+~eV7۪P~myr7*0+rG:ҫW a\]f4P¼{|=΃{5.%ǰC ;ٳK=zM*ڶ 4놊^ hՍt6L H8^*$IђkŜ2㉗P/?yXM"_W:}[];4~r&ڔ>yOp)fGcKIe) iHw ƤVzr'A槇Β$8GKR3LeT;[OI= F܃ʏo9&Fv[ޑAG5Vv}!nzc^}KGSŪNOyqrumG7t6ۍ ~O5Ѵ{x<Dtt?ZN]~qxv:h/a1FjU?_[X~CrSޠkuUf4ӿ2!Qc"M!+ )LЬl%ѲĶ#Hy/Ū{ 5oM7Gܡvhvp<}T!Ȥ!Di]xc>ѱ[ #qA(u -z3.;k ʏ=^7q(_Y+l~ \K } gykeJWXx9c?q8J쇊^B<,K2[<QxOOЬT=Ez.P_yҗU&_ 9eM3|;q~iE0*=s9_LHTX^a姈R)e'tk?**W}[40w- /W6apAO;}L_W? s/׋:8/gɹނlQ 7*2ZAN=O|7 h)0UlaeC?<Ͳrf,E|_LNJT7ȉ'өؗ.j&/h94U2<` R jHSOi)25ѯubК(ػUH|M ##^2Q{T4Tߡ\#~7%5})*+U:[0XðRmOrS?udN >FW!W@iЇCR U*us> *ɳ"vᾙl~o/ŵ,M ]4ٔS gKA oZG &Ud-qXj˫"$~7~h+x|˟xAGshzjRy%el%@~9$6H'~(A APDZW*=m~6!e7Oq4qѲG>s{8Oy@6>;a0^hAaQR%Ӏ*P$!+8mG\D5FFEmajogݪP$ y S]}֭{T]UҬm3 /6|͈:z YE m]KП7n0lVvb  %L 4ML H^B7#"%Hc\YFd/$!!1Xw~N=*ܱYz"fE6?.{-kꅥ^|mQr<ĘјT*! y*8M6G$v\pb3[ ̨i9K,VhJH`0fYudV: j-SO hAU41PZ1m`<~Pۡ(^do|hC//܁Rty1" /?_Ȁ bGְ ų4#cM{1hu=]'  _TĜof$J6B?3f m.$K)r ^^~ȗ/qquOS V^ukLL{iM$'C1\Fr` FBv;h\H)1jeQv_D?E3F+$x`-t<tJ+CTF4N&M=$zq"~lGU=įs:\`PɛUGKOJOCBMbk<-$|סz??0M.>Ph֗ކ0DQy UH99noR}T (S`|fgX͍ĭ6׺@ܲPmҁO*U/[MU7nվӟxNȈ@L>CbI=샐$" V~t_ޞH8H8}q>QoIҦka'=QjBNiA0>M _kH&Vٹ0cD`z i7|TPk|o~ԥ L$g/M+ rV #IwS PFs6ۍH̬HHnޕenË!3)2,IPx ;̃Y0t:ܳ&/r?9CJ.w+./1r]Bz Z3hq@ CP D0TDoN|6\v gsA..[ٳ/ȭ7^'6>] ń !b Az0 ZFE}ުetWUWUWW烻iSceegeǥdž \#څi6z]$ *W5yV]9f/X>@$mz bQPn$bU^^zo[1‹N$Ry~z@2V@='ۗXhX| ._*kLUN]¥X:uqw80gø͏ YNfك͏u  's}|%M}_?7pwؒh{> R9ִq;twGM|7Qrļd/uĐI@AS% E+ܼm\Qƴ=Ŷ;[RӐWfJЯ[Ǖ T8z^_w㲝kk ټfM-GRU7޻3ɀ{ l:fS9*Ze=kr~5[0t$gUI`B4U"6TZ>OcWJ4.Uc A;@YNW;J8qJ;O\[w u2ު{n/ zԴ^H!J`a*3d"`rSɞz1PX/htZOX^b y` , Nן { (woE"YjIJ6jgOϮ[Z/Dk(#=="pu.b~smbi;zya\D/<@C]k/{Uo(dtI =M<`NDS$x;8F7TAn7vYgif2By.W mLȳkʴO _zs[ohqέ?sr=ŗf:4qt yb&q5 4ǥ?MqǼl-pq>,(*Qׅӝ> e2oɌ9*V aV*T۷xk-䅕,vo0H lpg}{ZXHQ]P\j%@|"3u`%sB]#4qzv )] L{5C v ^qRjtYVm[d)!P989m/%Zl[5ɭVk+~8Ďe!oګclFh7,\J,4jY"gxUgg.a6YX9'=8G Sa|{XygYG v8ЃG =y?<ܸC'00lS E=[){W2m:qfdÑ:0JT~1Y#4 I{qy<.oFon3kɆ>8duY\>DڕNax,_ם4@MSe=:w7L57 B<`J7kzGzHhe[CH}XX,MqTPU,;8;;CpQk]a*E/\)g* Q*&($MK 72h|};qh@/4?`08Cͷrjk4iޙ'׿ƕkb;̶QѝkR B`X1.b+ @ ĉb(ʭb$.sɚωq;Q@1럽٪ q*K2wu/+ʉIJ/]#tJ%t0S=kQ P%^.Rw3l=X#C*wyo/y3weOlG!΄ rGekRYM4>jh8MYcm?9cGQ8~I{q e`w"HГ]eۮnuaHAmnw$a@SֶL6$1mI]]w>9(7Eݡ"dT]H=.37j~rKAwȷ&Y.>0: ZeY]2M><[׆<h48{̧:Ϯ񙫟O`d %ɈR,w5WO HK>p(c)A2*z3T5H"ZZ\xdDҖ}qMTX ;ˀ> :˖8:;|kSYy4+W8D^'TnVYp } n˶:a(?+#]1d6YGٝP7,tEb"EmtF1~+iʨ/Wʬ052L!Ž%D1^r-Q=]ŖW=xk}%Y. IDATO恎o3vS N]AVCqҠq)1PaA>2nsUI̊<<}98(Il*mVB2Cp]w`("vlt?wW 3RsAv-`JޓfAǸI)(5 # " VIݹϲH,^Ѐ^7; # q AQ t^_/jK0J 6meocHMK}SǀXvB$IVW)^4ѤrG3+ Z1 d'/s-.O"T2ȳӒgdž$_Oʜ"לRgo\raGԪ_/Q7VYʋDw.=qQY[nE XS0V or;ܭS9\n&!$oďQ {-:AIusOձŵ[ K$sMe;0EQİ΀:kk;663׵4%A}=VU+iL(/suSPuIQn5V!8fJܑb| jc?)"PA:(r};JymS_ KĪhq+^ŭwR)adwR)Eu3xF nelR#׶BnI܉F^v믭)Oȹ?޺ gYxbw_?t{,/:.[7yohò ,OHYRyGǂU er,>?Lmj ;6+,aÀh,<AfcWw6׳yr$b5P>e$!DT Bg%$[8E6:.;{CHgYU2nt?^ ĕ_Y)yxqg?]FQ -ʥd0WL,~ o_.{u|r>&ISJ%t9Qҫ(G $@bSnG\ .|wۇQe,x8aC>g& ~."'m{Q~g j3"yk6CAOXV.T"2o2أEz$766I:?SX. )v3o}/u12ugIq(&0RTuZ K % +|Ro$QCX%lR | nQ I[=uX\Wx^5?0/0IHb$]f}{#Te F9:dT-r3od[:*w0y}^ ymKZS6U.!Kd[ UWJ12\+-^7Ea /o߆$[7x][wgGL,ʢ]t[Wo1TA"aJa7t!.I{<sS~Vl?υz"iAkSKKYZoo;fלX LrO1iՓ$%/M"%SDn̪N˒D?Px\Wi'MA)}>(Z[L(ۗZLGgTD[ Anm H"zX37-: dFE+^%pǒ}=z\vlwA'W^.[$NA1#jU/ptKW~i? &RVaB;޺W %?f5Y>3'j}^thaϖXB DkS,{"ǝ*A j-ϲ94e:S̶`FZ=nZOlO-@'i4IV3tGa J'gD.2АciQ?,@ (XNROφD]vި/.7l~/x.\ 8{UcvQ>Xqo?:8fZǹR,}<%ѿq5~C$י2=;Ghxt{ =߷abp3a~Y>lm !P-\|fͥޘlko!-ߣVq̙[Ry~q[?`'N\͇!@$ -ln5 sNяoh,'_<SaD}HwR~=vw-L]Z\HYY!TJ {KJ(Ce\A0Gx'g˨jOL|F\' T+!B9U&7Z7Y=vr88QuX]dQ•k~{OӴϜֽ8Uzf{D UQW`)q[m]wr]+$SfK4:8dYf3Oe URgQ;`ot "X*K% *.<]߫6u.-4ńj.iͶYZa~d \\avV F9d9\ItyN};]؟0|~<䅫W9ټAo|5^8@$pgJi>wx/! 1T'Bޡy\=ך.GNv4gA[;)440`(n`YqvxEЎQ>[ <2mGY2O |}?98TI4$Q<&NT9 jWrN{U]RX? W͖=IU$xR`2WObumzU|>3W^s稸iJ 6 څv(z;>(_0{vX)E8|} Dj4gR{ݭ=&o)Ƹe5n(_NbC)A<}aou{$3ە,y 6|oGj8pNi^?Ҏp\,rL,cKA@ӮK/|*n*4 P FuXlPfǺiiX`;A 4t혛(g++ĊmMx&oI!wmq Ppfq}]_Hj`Jb ־B{f7+4K3&|y'r*YK ݵ'Iu82 py<Eڭ x)q+{vB(V\ί,q7~Cz=~~?י"Ip3ӎyl[>W0?ٝKq> =RԸ {x~.mEqn2ڧX0~8d벶L2'qY*u&ԦhD {=52SfLXed(ښ4Hzٵ%eT yO6ʀ꽼kܡFi\̭wAJrE3 Yjܼ oIvzSw-8O`-!+B0hK,}UJ:Ր'<l'Gݪ@pǶ% nE[pIIȥy)gzW\v6UF[!WҞVUN̗?a>dyBVJneQHε; *5+7^gbwq79H8Z6vgAz)߷ ^7=QˁtְKH)I d62x^{X認&Q;nLWG^')}h˷yE,N{PսuE24˗S$pW\VP7ȩd'A]!~0C/[r#\pWy%Mt+iJ}k9;odܾKOrǥY@w7jcv~>Jr6 Pܬ9NUZaz'Iq$=})Hí8ch5jr$=bݵ,s*2͚f^h-഼z3ϔS=DvAך.i 5nIR<}/_v~ ~Ǖ ̙ aAg4>׵Kw:w]9W\7T)I:< kAZׯOdtJD7B(* CoVP.Ed'\(i Hfcwp7ˬ,;ic0~xaHiT.P_#j̣kuԸ;' K 4u^*9iq*?&?ݺ2CIh\߯rys_~ЮZ&{+@㉀wltPaAz:!uiͶ37X#[ Pz$s`^8>%0?6'IRI֎/I46Hw7~ȃM]h~Ouns'VhXi7Yn-f G鶎ժyUT8 =([y;JG d VO^^ζ俅)7oӟ Sq[naƦ/\%h6OKQAU]w;y}`י{1J;q ;AO O,*Ryyt*$]R"k?KZO\j7nBL888{"w޼u7k-^ќmCGˠωe`(ѵSgv)AJW.O0e݋lIޅgyxz]'=7IOUXZ7N!g wRY,"d|B!yϠN)+sZY2"l쐈o4(c#3q W&άҕFN(fo :lr2c5,U/2m ]?&g?{?svje+$oT`uQ*6ȬlR#/K0B=OoZzo׵"B`o\%dh?f8Mxt0\$}7ٹ}[_{wr=ǩg/\\̀/= >85TemZzZjx,êdBT8zZGY=U|Q!߮qP -ℷ,ףVPWXi7i?sii77T'A4U ԕ+U9[t>H)]T/o20s*3Fbgn>=zV_>ue _% f $DFhivB 57 IDAT9^R]:QKNcq-ۭl+0f5栖Y,>UUYvl(-Wee@(%(xUW8uܪOEwlm^c63UN4Y4SKK,4,iJmDtE5s~Jk9;t֞@&=nxQ*D7V%5Neؤ'-*~KRz VKuJgp9l ~wz:ΰ}T2.ZsR ~jT<T05kEv y9c#=z7:$o=,s+̏Fh7x\ ˆ8gP' 58 x,8l-5lG|*VUp!KJM8!vsC񼶯Ǻg=hbb5ʚ+=HNx;KiZjEփ_~)b[G., i8 PS$!4TsNg Zu`dmiHnގt@/2-r*ٲӁN8ۏG̻.suQeM+aö tIxiWM{U^:4%[,koн&/Vm6 (8E; y56ϕzg:*·q`:^ʏdI Iv*; Bf/=.fvE]bc0MG{eܴY0S>XEi`wj qG.][-) *n62lϰlR&:#;5W& LwQs 7sE`ޡ>H3/?Q\7smò I<%nԱ?Pwxz]~Ph+6dSkt1Bt:kC|ZQ 7/b>HSк.?} z_~Py:£HAgR)Ŵs5N<{trަ`'Ry=ߝYނpvwn ?GAO?\X7J.pz;2Dt:' ka PdIvo8A[Ga\kVzqn^.ze_[PoGNm-StI>~"98 T>\yׅ$eեTĥ+nv}(#ϐDCv0TY^EY9y N[´b gM6to+_ =ȳ0miwף8ep,8sǽVv`y?:^bYeh{Uoh˝k/];ŋi]DrMl9<( ?vGOwSuAQ*ejįy7\OavSbʬ< :@% <*`4- &>rHn>Qn., Φ}ԲntQ3j5B 1&Pf 䍓XMęb wfs{"*怙|v;e .)޶~ M͔ݞ}=ߧ}q]v7{=U}hz!hsuNA3clQ:8(8jME],g.2 J_f5 |k]uOKY"oZcEsm+qxS5^8x-|";v(' 5*T}w J"~8`/Tr@* g"a* ƒtK;u\leqOP]hqw|]޼vK/k:fٜM P $!v.1N2=(ȬuAD|'GʟiUmGIұ{#gnEJ*vKZ߸gCn>PG9ˉ-^|ZW.sj~q\玵 {DIVLѓ?t˼I &:<~K_ߠCTEϞ% "*Й=s!/9܉jnOjo'y-(c<sbRLz<)FX)AnjVH-y| cJ{m}G#N[n yˉ>D9#EnDs1Q {͵!}+@CetgYFțw?C~7CM)b\pU%H>+Kߓ{,?ne[F,1T[N%Aw2JZ' KM8:REu6 ZQ^OaT~v"s8!101tfP|Ⱦ[ZY~_=8l}ʶ'(so4+XAxI^HY!8DXM{.ӳ33d~v&''m˷Gko.l=Ȫ4'Wh7SԪ~]m8'I4&g]EsSL}dy'P~R {ܺw7ڻwu83:/]֙gN75la?, /b[gU?J`ǥDZ=J @).:/||M$Svt 5uv;No^fsXhju |~~K'ٌKq? piV(/]⼧ʗ Q;S4dUs%ښk]b2m~_o=%$1PܒLiۧ{KlFqՅi0I̹g_;1Rʒqbuu!kUŐ+n1V(a>%&:?HFL1z&o غs2~+rW^vM\a/}(zuɾ5ϰ=c5k-725o@s _ppiKvVCB΢j`$]lKg,~betdyǷL2pȳAX*܅+ uvLy*\׃/0 c]9.dݨC~Bb a.< !(+,?f=`*SatkAI:jNu>X9BYgeFů#\Ñqx {i4fJ\n[ $\J>4Sng[ܽ^xP X:9ǩk l68\d䞵/p}BgdOOSD(fЊƹi˔c&IaY8*`5CbFɚ`ԯs6:[|Up!\e]ힾ_.ZF-v*{\Alx:ފ_nnR9]>`l?3Escl@O&mͳ5]e+d;ӸN!a>M9q/,Xt; l֐tа;\]­XC@t@ۢ#.<9$Oz'V>IXtuS N?b7?3;QH՜a }ڐJ7ƾ\+| 5U\[j<{Yq>Ī7IXGszl؛"]19mnrj.ɀNT{H:O@>zPt dɮpBDέŘuKTkXgp$ײm'4Yl,7m$>0di[Ep]SPi~6(F^5}z\7}cC?ֽPl7yVfѼR%g#e=m EB0Ь|sm\^W tˆ7tKWaKuvAҙ6&I$[bjr%m5ɦ`"Ya,%~,:ISnݛpO_ugyy~{V Nboce+[D A1.o| o4sڲ- =MTݲv \9tQuʠ)|ӿn^Eݷ{W>IfP) Qw/&ffl*OHǁZL}s?KO-CE^<,>w?q 89,#NWKu JԨR] N4%ELA"Np M_~0aHPѠ;YÇ}s !;2rlPs*j0" ʲf]̭kt"w~x/?@S/_D|Rv( R1Fd}q/˜<t?"~=F[!U$ puҥׯ0n2a*+fO*:T1u,q`_i)iI8#$bٳV A\80$d[ڪ'0)ָ. L@P\_xep鶒F 3ޔ8N'')w\^F}ٱFVˏryhдXHq,(Asg z]'!ӝLl{;{C|^<7ټUWzcQ{Mﯱ%  QW; omܪTF0 n0 O,ѪשfQJ(}k@z{ qwYҠ^q,z"yU#&Z>G/V[wڂ723s-~xgkF auD c,$8+C\=ζxqI5,x&mOCN9W_GZQ |/ӯ\e OsNkBQŌB)ETRGC^x޾-nz\_G<}(*3Jiq}!0i?j邆( A,sVkkk4^y[lms5X#m܉"g$Hpv2.(nө4XT_d4?@R\m!׋"<_y:;w鳜9-4i5v]Tod`N0"ݏli*? B2$}X[&pBf VЪq4MA(@{~˷)ٸ"ϐcF1R_6a9lX0`w Q>ug'xηOJ8I( }p7NDZ>8|=j%ww C/< j>U<ֿ}҉N8ȭ]mūXٰ`P_L}f%m?I/\'ZmZ'dSe/2Џ(U i7 .ڥr¯7}-d $Zh7w< q!QD0DWv]ILSοt˞ҮZ_~!MU̡Z8<߸t%n\~`C_^i\vPaRvQO!MkDx@e*uU2t]~~?k ߄Vz{%q{]h;اc]+T}c 4@O;.0bFܕlǥǂ=%a@d4_@!ОYy EPIB6I';`Z^^^7Ot'AkSKK4T~ "Vks,$I j R:=޺q&h>{e~(T,ukKٯ&YlX$$VuIߣvQ*C^U'BXlꥊFBB^7_-: ؼLsy*@uac.-϶L4j,9EhX3@};إJC\@?ٿK;dS45_M|qHuN-DFFpѴʁq)BӍ wC!{aBP"8xog@ݫǹ%O秉#88حܪsPE[D[Su' ? U0\vl|_Cr>J:Qd_v-GO؏d*&mb qY>:?~~."{:D%2B[ԕ]3ҮXnqj#,0 4˚;܍ĵ:ވ"2T{gxṧyB z2T AIzt.uYKf"¢^ltLa\ލ<t2 qa$1yYD? );7o$9ۡf.QbdRدs)\ۡ8 XsUN~"*hQ";qheפK9u[CCfCmB[L&,P΀<> gȋr_g SʼeX4@q!m(|h рDرK)K-\|,~E/SMj-B3"^Aҿw^2= C詗Tg|VȻEŲ0l5|@5Ms.Pgi-ǚw_[oc2P/W @^[^VFzTm]F6s2#uy|,tRIT;=tYk[I[B%zԮaЧT7br%ꀩ}FOڞLKyN<,[EuMp>k+!U1Vf_?}F_3;;ϳ_8=6}Q}ԪC'#-_Q{q=7^jK̾uocF3RN|Mȕmя GY rK'&;"F<~%00y~^ [2C 9u %3U*u"0nZmpLBuj ;J]pLKk8;3ΈTxgaloSX`&D#6F7wԎHR@"K,+m;"jac#e+T~fhmB[eX&Lsn׈MC\,"@ɾXdZQS}Ma\Nf61 E%3XR)))l l L̼M>ot C%Ĺ9u$ym `MKЀ.F*7  !їO8T܉ '1oDCǧO{|#s? MmO K 1.$sqe_skf~:q0´MOWϝZ#Ev -0 D}f@6tpKy"din6x!yC{#κΛ8Vn08tBhxk -,EGvL|bK4^̟gw~.yn ba:vBNHungL|]kWo[cNkRU >9\9<: "hP&!r9 Q#MYWnOu%`D Nѝg;bBT]cM]f.片<_o}6 Pu×<@O~$0NG-9!' ~`mFNZzmU[ir Y]nT#@Z)$G T9BٶM.l/?ͣaLq==,NJN@ $xZWJm_xd6KYN6] A tev1Utt! )g OFBu=fuSRw)'}xr&OpW^]b\[PA|/ 3t ounevZw4r1 >ȋhF@[5sd.Wou֢];1V#s59| Yln,pw:TMK_ J}.P$>{!$WZaol-Ce]Oܻ,Ilzy0-W=\'>s6_o(?,ry/Њ&2/e+t:<:!\hԩERr[bI #I0r([k؈G*MR{RɈqvJI9.IACD^R! dő>0ɆZ+qdլL#Ni0Snzफnm}<|҄$IENsp2"ͭrYfo[ٓ'YnIiAKA렷t`ωl' !; 05Ʌ8 <qQ0ǽ{ ֒jMj/0W84GGx"<F?1xϊr eҝ;Uhqx${][ G< Ӡ Vl`0kAD$y~HUW6NӓxA+gguV%AOTXD!bxh,9f*IvB&FzY;rvy uh3vvH1<9țbaV nlA~xC,1>(!ZKlc']X$|,[l;P¹8ru_!~OM~ٞ=@||7&_au&eNU5W.aaeYyB[l8x9?ܢ.p6|)/&seeAU=n>J=}=x3O2 B"4 yQ1 f9rf 躃2k[e}ATK3WW F;LCök,mLA)ZlZ%?O piös4d Ƨ..0o;H :b#T'+"Dv+i"|ѡ7b\\%8W[70;NC`ZcT7ii\~I~B 88z8. tEA )w`5\ <2Z3w~Q]iXY%/l4`O!]ҩQο%t̢dd}rT ГĬu:z9ReTAB\V]~?ӟKI$jm];ny4]3;rj^t{@o`$U93\x7/6c`&sRYrY%#^>"mQtxx! u'.?0]tEF;]&ƣBQC5 X 7v hu;9^kIJ%$>ym@B!i"{nx ("Xo9/WR~L2a`G.OgIV\9F@-S ,"eáy#PrEۛzN ՁRÐ ?:wW~u'+L0\Ƙ *z|Fm#'۱\VHnO=UdF_Qxܓ[^=j$v )+Op}rgC'Np(4nTWyw1N133͙$`N5r^I]~ O#Hz)4}LҰITkGlSTHIx{܎[_ARTuOy$}pwۍCī!\5KϿ.a_衷wKa><) oރJ9)qE:o:蚦?PCy7fIQtXWÀ ixMyCTPeM:- y0D0OrDC}.xM;1?& hm1&/ޖ)Iϲ_*Ʋ- 9%ۊ0:s/iJ5I/+?e=\, =UDL/jlm%YX}fg1Wшju=ǀZs+?'ixnٷYNknE2[9R Q( >G:8êª|͕U+#2+@u}UjϿ^@gDJ3)C! =@丬9Eԋ)TPR? r2 ])Г݆߭58M/ ( gN6&D.p0խ7ZMZ,P?uS JIP-V7[gT8vqi)_O1_:V|ހ4p2Ae-k05Y fIwd'6 Ӥ8,޹K\c:lz`Z}d :Sp2E$Щ`~x߲r{?,I۽NR dN(@.}!B>3~p taU*Uӏx ]i&~l^P?>'mS}M>׮܉ rD0j7h[-j};''L+5LHqsБ() }GҡcH_e`k#4.aK0[6#F(hqho5iw'4 ^5_|QGC!vPqt-%}~C gNDfjׅTnC|b;LZ׺z%g)U&$R^ye}g2(<qc/Lx V,̿W\e0S2bS+Y I0$բحJMiR1yrȵ,IaH$&ɲA `9r3|zbȶ u׎`=n]C#'aǠ&Ar@2ܿXIH|ocG$cJp"-eI*yezjC%7qL$/TcVa!>-t]N a2ox˿ɵ$[k< |IL[4ZNd=,k0 |qg)`lRf{?Wg?s3]鳩jZ=?9y[>#.; OtA\$tST !MwrTGGB?SN0`/Ptco{^%, çW'"G+}w;9Cg΀fNsjFWjVQ_;ysqr'n;|$ӌI7f#7q\ړ#*?X!rI~#>hLτ!Boh9cѨyCC9$a߼oGFt [:aw`{T[bF L gx)^˼Y}(&r)ٖ4-+ htQ`/gtZ/XiR)'y[]v!":V%k1\I_fej81ɐO^T*C'ѶEuy]zo0NRAOV@R]q'% e(m7klUb!3oWAOu^@qTJp貴J@Dh MdIw I7?11lqBPAO (gTqvi>/ ;7X[s/]\"Wo0>&qt`suQjqgb*8HtlKX: ϥ+ 8z)k^ze~n |^:șt %;:<;Y5[ūέBrTb7>t<+j4&˒K)oA  zkqd P/KP, DMkژnw$opT1^rM0ӧS'a4cw/tR=)4ٍ#%b'4\ޖ&a[{P(feة IDAT,OkAy ]Eg(nVʣ u0XT; jIXq2=dڍ:(F| U+Mm0X-ݾږpGKEPB \U;Xyvrx:4Z(8sɹ9>sի?oej4'蔏'ۊPkf6Ct1DZ'XY_bo^[`574g(<RWy }ܿ7*xq#`5D&a`cY&!Ź\~:!Ͳ=ɠ4X?0ECl5/O]ͽ Kfqm1jJ2<> gŦ+)wާj7x4U 蓡ܱ _0b36q$4st"ɣpdF+.ت&cE񇙷1m׆#.昙Ƙb@ކH SHāc!HPl,/*..;h,^jd\B5V\""Mr"%tނ9MnC &͔$`hC "TܕZMoS[x[&c/b;N?&7|TvRG\!%[f>df%ܚ j /osaOL01y 5bqF#FFI3hu)i#aa\1vXrLASkׯ\W<1'ltO*gڔKG4::!~ŀfz=_Ӹ_-\-ܒ?VUwr2zsh OYn_/"x /襬O^`ڞ_Uұu< *"a;jm2=}^ϧ ->.?Ξt^qפ̙rʷ&573$]; 5eqP-`٤ l-5KlB0UkXEfFN:u4z<74L-x&Vorөp<{MS%^`nyvnt[e!ˮuK'6i}N%g>M}GPN tRYL11unV o*u ($X鐣?%J* .UYA~`KzuV~N"&$_4MX_k rN-}~?rU`un6f23I\y7 v>Uݽ,G/r&G3f`Ѻς|cPmaE nt>\1e ˱rG8?$̌@Őm604M1i} yMN]yI:a;B7߽zzlz[Hju]+E ToWSz_]W*I-2ʌZp x=>שazX{BRC.9IT lA0P d;4ɇPsC>b1ql'xӞ*PsTϥiXc7H;pB rYn7(N޿ ٶ\@ zy=EY:v<ўŶcEN!zv"}#1dȖ.r6.޵kuL[nf ͖8Jam'!K癘Mk|^[o/0=}cŎv7{fX)̅scw7Y[@RUiG>ցA%AONHrA>UYG8">|= YJ[RS_A;n;hk;Z^=qifA>g€3OД=*;Vs^[eu?\$aȹexdʣS% q_=0bTIk털56 Vj 섐))N T46*va<>Ws_%oԄv 0p1g2>0/y>kK}`/oP{MMdC=OEd$@MQ <²i6G801$38ΪHAVT & sk nw/ɄEg88pƚ1CPۯA_B,ODrI?TNx}ٹ;֘ cS3NjU;,_gnacI*Pg(szWc%*uìPU%`C [i 3=VN!wP3(e;MpyglmL9*S)f;ҒM".&+MGywX^g23xW05 zhV' dZV.ՒpkA㳗adf dlSXYvJ:%Cr\孤C@K=Olsm~ZÇAѩ ebb7 ,%V6mxJ0&"ܽHc-!J2Qt1*k?3= %p/ 3q$I}zKM;hN |fW;g e<]F2cu/Q .6xk&_wi-Y~ʜy™S!N#BWJ^P 2,P7"kutpN.IEk{SexYדyl IP|D9&nNKu!g;yh7=&847$%2 KJ 7l:ƩTZ[D6F<uR5/|oym:͘o@vpwEޏyYnXd|A'Pl`4ӦyF (prMnGK&oK ^9R:R6>je@/IK`(>[ЧA7KSGzTedg\&UYץu*|g2+)6"&Ww#wlvB yib&g/+/|Q: rwl>عC\oM &nv7 oW^ǎ$x6x^<6M*ay| C2k6qJ%nsKyMM4JY֗jI3?rqXM//B-ڔ\3U&D4߆2Tխ8 @@^d)Hc7^~ty8:Z~NR 2sLVEbҹӡMN˒{H_åoRӟFsҗz$Xՙ͒:˴},/`6NTEKku: ūn@iw۠R!ȋЀ$W[qt)͌3q/CD]yFujѶ87UW1Cw[ č5S*165ɳ/xꏮus;\廫][IZ퀝`v!;K?%V% }0SL!{gC-FJ /GOUgǧ߮0 N);,Г|p 9VjkΎ=# 4Pa E/>8GMm'g34&w ߤunX-PxtF }ҝ?]Y kTVchA1]́c9z'r!6G/N.t@ENNr[.r&5uY,P;.P[;.1j|gܾ[cynaR{exǒѱS;~!ši TqIwOw:]0UW;u\WV`N(;y&*Q)ͪ}^D](!^J?bd 5,% :z(S @rt@ӟ5]>_?@|f *gcaqW[ϾMףּlˑp]>2'' %Ed~-WLUպ|e.y0>uS'[& W:<^R}fjxa]P.?~z n:a0Pt(šQJ.=uޭ~S `+Mj3{-[V(Ǧ|8*NB!!IĹK=Vu})=7ONᴁ+49'ǶeKOm!.&bg1KW~z`z0'JD)C-CxXG?G9})vwK`}y廫n>8*ISۇM|l=ս@[qwUOS^;ZtCet$E iR-ljJQZ9n熴#alE>Oo*t}o~+/)$CHMDv)VƲq>?Nq쵛T|[ CNs噋Cv tN{$IS}Wߪr@/ˁո LNW~'5Ǹ$0JRHOJ0ʂ-%kAM㏿UVbra?wb~PXYS\@"jy~;)P~k|P5qK.|O_@6XCWqew0uY}6!|L"pƀ-m!۵m6sPsR¿SNS061NL`{k ضD9;ͫE7_eb ocN\YŐp⼼ I˘U+nu*;49vfȑ}k6 IIhؠ߯dΞf ݓ^=+"9MdfL|f^Yњ[dynqOwĔhc9x^qC$ž\@ڬ iy~4-}T U×i!]肝 jסt$(I@lG6a$pYn/=EPr\WJoqic1V׶$Ў9F2α|.=4l/_ӯqﳽRg}N}gg҃ NM ez~AO݆>o%=k?W.Yq+e.$ur{R>/_[*}XS_ 9]zϫ#NqAչU0B<8? pmN{vwMI)@0bċ`S8~2t A0: d\W?1I9Dzsg>ɛc K^ -s&c%-Bu ƅ 7kM(^cv!aBvئBV~?|OScQznS'{E[n\+\ _f¶Jhm޺1' )vv|.x]&F "WG}tppe*I.oO%G)%|{ߕ,Puoǯ쉪t+=XaT@T;ox?wa$,e/z)R:ѓ^>vdtrWO'%FvOOw||/y_ȥO*TKc&R&DIWQuʂDTWQ/ԐtFbS$A`۩>y#*YSv_XTHS]?0zҗ,<,Kw ST[\xU6c{Eh ՟wwchL?VM IDAT.'V[slrv/_ߦe\(ŗG fz~[5g`ިSbWbȰSI&6,> ʜlWT(U]ow ~z 1ͤ$pun7`Ob?o(h_G9+[]%~s6B廫='b6 }çr['B Ws$xsN|znaNJH]C|2/;SեOWAO+Ix^Nl+'CkAẔ[nXqԶ-%סج;V>UP?W}&yUDg?6{M=N@} ]TIa_H@z8lrfw }:>MoA'B+ Ƥ{2_oz{XRQא| YCe`;o~$@/t셓sߥu xy+SM sX\gK5jMyy>vfOƵz1Jim &\9Ox8I`jD#_(=M;D7+ 5O.+r93G9P\^79- g=^ەNZA5gN0,(Ш/[m`'@W<֏x|+ODmӧWPM-5;rI^/@(r9_5uBxz,dfBձL/`AS)2vԱjuGPEZoAH 5^{Wo N?fKS,\>S-٭]$\ʋ7Mrv I [- ``kശ "M,.n\[\vc ?omNckSO_djlxc8fpC.Z, !A# ,+ATHNh~ :cosmwm/6Aۃ{]5kADCX ,dd$6$ 9,ggR pwg1}+fjgr'9~jN)s~ P*{ޑ$)3U= VVCiPSQ%;@$}ZRօYERf噀чw8S#pw09.28$1B0;5̱Y΄+KYf6wPhYp=$N?_UM7ĘHPڃbA3;&z1T9\_QwJꜻR.];ty.fd +19H _J4qʕ aMMw d@i:ϝ'pgW?p5/0T{I`5ꗑikCσݸܚiSl.DеWY>i|m\IMg#†I:lb>xnϥI tɘIE~҃$`6ž{c"a?!<~U^IzͨUD.MPfH2rt^O =,wy#O?>g|喗Y[$ ه̹qj nԻJ9re~s zEUF _.^Hi,sOLPV-! ڰ@*aDVkU$.HklCF,ǧw;~vnyM.SJ.I< rx*yiO>]N==F'޿"Zo^쿫6 &\QcťMi4ݢ6,&涼Oe$xqBPyS' 7oɛWyg.kw3O3wɬ_!n)򧷙D//v˻1vIM,6}O\7K*}. [)PYӰKM㶀vW svDm |ʞNGͧռ\U/NԤ3UJ;Y==y͆Mmms8Z`Υb< \ʘV)M=S YXM.7o\oދ v)tOo^ 0I^RA~yJp-MpvpS%TMl  $T.\iWLo"I.]S,aecJ~V`>ac|D=7:|b+g?ëܾx`N?_7gUjÒ`Ziǧݷy/ɿ洚w픾5oM- b"ol޶p^py"_exk;}0WTE~;!dr? t]&P%WSxqսb7;yU;q>?|Vn]H,\"t=HܤSg.v[ d D[ꅖ-O1?, J,X_xmN; `/dpMSfCtIHf|)'RnĶ= |9&R<ehFަ9`nW,YzFe.\ ӕ˯mh&*(b9aP)wב}U6:Ơ_խLr= 鬸gN|8+x7ouKJiSt[Y6m"<& &ebNG$Є˰2nCl"*U;QsֆFĭEEcbĨ6˘NPLT mvkwqB묧?s1:csrsj,{Ve{&_$1T@iq>rukiӥ6wAǛȨb'A^\+yyMr' #DUُ۵AԄ-o;?|g`§21;54CO\ke%]46תG8񓌝z]xzH%K. JMmn}D}&zz~lι@V/7j0čyn2aޱ R(R*f_ƆKĻi`"sPIU; 7Yx=yN8(wxX^sE^cc[ϯ1iC`dbz9S:r/|?$|[,5Zi/VE[n' KsE03BV7jrs"vDO3!|yGWs;hh/ލ(trIgJ(X3Pjf(yDIC{%zw36z<o=Bx ƛpQoh}+%Gc[;k4._#jlJ7+O**f3jWh>]f~XiwVMe50WinLWK D۸u\E2*1QDh,b.+Wz˄8"Daڙ0SSn_SnQ/ndz}|FS393o\)?m,<yPLj8,:f*uz[R'IDI5֞GC1k"aVtjA4m욭gl簏WziuM7>oykC~ګ8xg,\̝y$S֍hWnjcwB'`/6P=u?GEU_ ?߅M=xrnL[R)˶u3 @6^WK"<) :A@j~)Acf'sVIv A#a!|[12[K \nja:k -79Rx2,798l!׿ L|IҌqk^VL48#xq[(z{q[PS8>ĵJ@VWD-:.}.\~и'+ɳ`xkbh4/|x~fD_bǯД$uy*+4C|ڥ[ QD.e=|c_pca;YYh5xgPʚcƽDO|GAFL2U>7&z71⧟k"tz@6!C?R'r[+T4L[g7Vib36~nCJ?$;y$τM"]G?zצmw+7#Ϧ7utv677]yJ&E[%1CSȹu/w'6P0\do&l#k1hFZ/ٕt`/X{GKQK[BU?(Lqg63a#C\\f_[ff5f8鱕pY6#_㯁rE矾n]rT=RBdfzGҌcIS%clNd֛~|Y;Q:t$*M:q@^%2nn`|'b&,n}P'w7}Q`YؿN ]n~0 +~<gXhغ7iͅƝPX`huN>TʺPTe]Lg}:~LW~eW~ƥrF Og^LRGmU&isǁK+ /#P̞ӄߜZ;(N~-+DC^_$ &lw&?B7< z=D˃μqy`rﮯ w`q: 1G-\!.ɠjn[ċK?lIe5/ Te{X\pAk]"QA 3x.R{LbxF!O8riY:+~7aFɋ"S]ىeB7QTtF_4&'"E;,#{_HM4c?HgwXUOC%m<خۃ ݢЇ=Ƅ>,ߠa?F'$Y=?kK-.܀#_>ڷ=uGn2'}XV4&bHI*.˾Mx8GRR&^rehMǘRǡ۵Z9 7 Rˑhv]\qgY,_Xyco<ߢ'o'=`~ 1y~"y:͞?yc:>+MNL%Y<&sU> yn]Ow9&?ao86߆9p/Kdz!Շ9U*u mVM!?؅G_y/ ϟP oኄdW [}LLA'yU<I/@ I Hu#ޖs 3Q-gW%\ۈX.XmL'|3p׶iCD*DZT鴂=oR\_"\y\ˮz_'_Tk=]zEנ#s-xda Fޅ/da=AscZm^=)h2'(NM3>^X,S-k-_koЄ;ܹ?Vc^'7KEJ܍VAdC S3[Aks{jBg*t:Nn1$SiP/_gŽ3,%)0}@^=O騸 t[2W}C խmzx+_EU{sH,Yp&eUlݠ >g&1yn_ujhױ613]:OD5.)԰]Y6ȋ ʽͶ>z9\Av_~Wԍ $ sJ4xb#ygx=߯$x+ϳrc v?6?eB]N*rJl/3%O&z"zt]Q[O~#j"<Ugfp<(U{fLu$L$J۬h8LVIn^PjnMf&c\{hChڰ׎3n2v)U IDAT1h!ByS@C_慯c 3 Z{??@JK# pxg>OE7[1ϫ?Fbf; .|SLת24*˒hhsP&}:C'$E$kǚqbv[2I2"nSDD+\oiH7[y5N zkEyjcHV qڧ~=j: D퀛MƆK|_roͷy?>SɌDT&)V#|&ӏC5ϩlw4kr7Ywv A} î#t[B&:E?2t5Ey?#hym֏ٰ2p8ۭmUL299Y}T's8=FEELo/_v$a$pc -߄6Rv%a;c5dMqv-΂_]} Wcc.0 89{mwpTeVI˜kliNz;I@BDѫw$ i<4ܚͰT8>w|T( ߳(b^GL# ̘ }s~'t/Nҽ.ZsAA08G2?1۲aAG3xgF-.-4rq!S8?/n+ ]g? fMhbM/jFr8Bվ˶tGiyK&q;uV܎#hH`p;Q_&nArob)\&/'{q>95IR2S($[yN3wlU/h&IPռ։!ZkùGp[//|ifkWNSe$ 9tSo$gw ǘ>,:5.&Qr[b"薍zTf\a$ƭXu?}u]Ҥb~)ycMMg~c l\y6^?ao/}#dk'z?_x?r;?o3~b_zTO~RueÒV ޙs$ xO-]W Q;QtI$&1Zớ>Rc`7$P(ɹQKCT."{Ev)$>gjR+ow=M쾺f׌ð6zys`;RRt2ɚ~a<{1yk< lWxJ0 **GgvXYiU_ڝE,4_W@~>$R*xY&^^vq vvh&my}a~uk/7곜_Gf}gr[-ӷU:Sqeruw>0PBi zyY[5PD/ma24(H{1NQSN T MT]&}ݸ>As(hBR|:?W%[lpx>RF+*NC$>e@=,A#n}zax;QGr+)ى*^&M#X鵌F{IobQeGU$hf.nɖp\@_yҾ4ϥU=3om9+3V\FP?Q@JI$cD*F~Q*4n7&k-\3TC,E=JmI.M{ȞV jOʰ$ViVQ5E* c5ˈ QTj)CY!iRK2Or9^ P *c䳶چ;|d/rlN>~Pvcv0 t 5phGHOG'"o&W/WM&Ӫ_vfle;p/$&]r_Џٯ..B~Piwv#|c~hRvW^x>wfZB D%O?Neݕc`=vbǤ'\%T鴪(bؖ}fSų@ wm$[1OU-mlSV lJf&<^oD,5Z6 zʩ6 w?,BvF3 (9ulNzBn @{ϥ== mEa􎳉X0IȌA3_φZ_\6BM_/A"/MqHtdG86_kF+~c)(Gju >l{~߽iv]u ڭkCyMϮgc" Wn.<,"b#Y5ڭi"7ܺ2ɶ5϶V,щl=^PpU/2QNsT&YzW?|ǟ]sl}<+K4\eŌ*%**?0Π%*c2u,!Bt#j: DamUFyۣ=x,۬QvJIAӳ5d+ժ~Ȉ@JJ\7eqF#9mdʊ1ZZFaE]\H:nr>иr)`$%2N* üS*7o :SvzWJq yu( `2ٹnRş퀡(Ӽ^IūpŋV33|t%VݳYf]H3ow7jv({T->(rA{ _ԖY<%syNE}xe$#|Av!C˒Gk Sb)dmm89dȜ&kyV@(H b&Uf(EP)w\ހDRp[W{\ 4`32_Q8iDPt;'O*?:jEjM"b8;? XAsa?}i~_ K$)c2+]cwָs: ^V ?0:f:LЮL7T=sR( g*y㮢[|nMlRg&}Hm JYO?6F"z2}WqpQe6 s0993\"wl4%jg]s|!!o4\{,ȝMեEXfRR G('@;Prs+ _S߻F!`Xg=Cmѱ:#%>)H2H而q2ۊH>dE=tSfv\׻IPxH9I9~b" ANw ۦmNh[oB`$`oK.ׯjHJfgz6vix6CQ?4$!B6WRށRs<8{o]4. y6l_k+jۀ+&c9t ~ltL][ 7 IЫD9d<ƌ3A/8zM*Ro辸=+[Z3D4A.PpX£؆-[O2vbTig-M87T5lWM"=&X;FXU܌),A5!'\ CdG}23QcV!>uWEZ\ze+?gԓ7ٖpSx|O_ԉTcM⤤6rHI(T[7dDG/%y41}D*Cr({$'O=׾//pru}K/_?uN=O򱓏1Qx1pP#O:[^ێw~P8nqkyk!} nQipexeYZ!"O|?0}lVR-B~e7ɎKqUCCH~=V5ۤTNÌGgM"tא$L/~G,%Ȑz:[]\N_\Ei%w¤K\1ݵ@F5uG!ˣ#~JBvۿT[f&nT]?Zf%| ]88b599ixΣsFA; {}6\T}{`Ofs{~UJlt䁇7_?^J޼:ϥwֈX[h7^TsgQG@;;ӑ31U4u=0u:lKaL]6*e7!znN|i'X|%v_ײ3CZcLW,SMRQ1r{2& CVV4$͘Zv vKMr: y1e"v&c |( HZ/iI&~E:Q\ږ盥ǤOBsR pgTrkCڛ˷Tzv|rjё2m^5q秕@4=C`s#6awXb>ض߶<8p7γݐ8J΋׻'|^PY5|Ꮨ%1eߧnﮰDt(%;| P! nEz,6oo+T. ){U$Hdk,Nv\ εuI$zgïzBNvkxqh Lm$xAk3JG3=Tg[A 56ګ1T;9@-\uVhÐuBăvJp]$3Iaa_e !$]<56{7m]l1-%A"j?ulfOxMJu\xK߿X8VqjҢIhI*lJZuD K [%0d*A'ہ$kqwIS$N.Zj#nzӏe$IXq3LN0qyhri=OQ$g/Bjɐƿep nq(R( %qe".~y4Kbi!Б]K~NT+\om޹)J # inRjqy6I0kЪZs-76&Y)X.-`&jX+`zz-ۯ$O?wqnDnӻa#O%53(\mڍ\f.~9 ۧ U.%s6tDzȠ&j /S *YϕOt03=sO\_WkK{ӑl  Dzm{l7dHȤR[8 ]sJ5q0L( ng7{0Nå7[YYWgT|!RW[??edz.P V/rm? Iu(#C$%뼽`]xp|PN%yb*]EdO+۹MHdu4L*ܔCaEw "q1ⴓp 9!v IDAT >ZR p2+q[5yeDù.6j=;QP/\n#*uF+e:ZF 1r\|g@2ZYID͒+o\I$kݠ <u738^qlB AЫ@P_[[֐XCA<̱S<'׾ǵWqkwQl"~A*wgj.(kGz?V)3e6#|v<Ǚ1zXI^k HUPY5c}jd**C \ܴXs#~A[Jn$ %0gGWi\V=:c=Gg #I^4کvk\Rtֺ+=Vcr9MHrHP+xQ%i@]lӘ-~+.ROd) $R=ʗY,f뱎]3G9|=#7ö+oC{ ّtjUKzP[h2vk]t C8N댔)WJYn2Q~.ΠOPcz|H#/EU^. {C̮ihwMˎ T6]۴ $V.Ԋ:p</M4}ɳ^V?i24Ys_S@ e݋o&=>/H/PpeB7\{KKOTi"=-G(kM lO#o_idm̑ !mD\\GX~[ok8~j.;LlhmuQT1Ze-Ul 5@ݸHfjl0(^$N:ٺ:ZAHVu,-^)=iEx{Wa&[U/i8LzM~iRKo0ʫYRhʎ?0]RA۽wSQ M84=Eh>j+ mŒ9Ln9)2r'J ܞ Ԋ%y! (.^v;P.O?΢]z*WnR# $wqk4ߥ!鬷w3B[/OT,'Ϟ^4[6.37/e6U>.Gc̹{ZpQˠAݤCCA(O=rS'rwvIg$ۍ!} _lD$̀;h1jDmuI&|ZSq^DɃaBWQLSd#Ujp]ʆX^i{ifi]UGwG_uU-X@JIc̍h)5U?T " ETg<@Lgmu=5~CQ>矣XZV,BrE)Ue~ū$X;2/;z'G+"EH_\Tٸ {KK"t4 *&=vYk6f&|9=67c.0۬NYPڕ ]%Wtͫg ښ1!T1&#>f("{F$nr(uy4[$zu7$9݋)֑呯~,7O;=d+UADswy8붉^qI{lWA~HI+ؠS0Y徿K~gߘoptZOm2;z_ds_BMʴ ܪ&Y҃1=g?hC٧VtvCJKz~_d-D!_$HQpY+Fz9Q>ܹ"HISJ&ė^a;-+4ǣӡ Y4F'}"Uc{mP@J{VXJۋc3ct6Ćҵ.З$zsnepy9I; --ۆKO_C|عOa|gR4%ܴ6ԏ۷=Ҳ&q`4m=b>*fW6GP*\V7nMvݧͰ7@^@y* yBmז؍T<RL1YrF$ \ Oۯ,14Yܳ3uIWBarۄ?Wﱿ|hߎ"_Xn62UEV{m ]ukB.[sAW6Iw򔺻󔲷59@МoPd=SIэBgtOSΎ*\IuvHaԕc/ 7@dZz}.f躢=S+Ȯ y.@n&G&e6ʃ} A;ul8}aӏ}\r݆bQonRPs%5=d߇pge~R`jc|PDτVt_5 dv(P<g#y:f6S0Bϋi'&@ 6~ [8 nFiRStiAnSL㐀U+47"FPueD/g$Frٟh%&BO>$y>{ =h S)[~~5-ycmD{.cABnl'.ro^|-KP.q'9~j@owAR ,^Rcմۇq(uQRRFBc#63 >r s0g/0˾weφ̄13gE84r5--zSTulgPSU(Us$"&J1Ԅ >K$ ľ4 mg'Iseb˴.;q[6Ru(ULMT̷_;_٫l^v,~:K/QT@l!=n #O~@ ~vI; KehLi#q۠V] H6a)H )WjT*5-! $2]J|P\.J|qsϝ᯷ixQ矦d%[$QYjvdPF1̉C|i$XZ )ָ} !~TJDZU4;Bp쬟 R{~Gz^J2B/ӡ}#jPFj#I5>i-uI3ϣȒ"O*5X $JE!WUPo0l]O81jce5^]H@>Pd/ yS#O1^W B~PsYAmku5&E~tboS+HRt.㲻ޔ78$ezCuBi39^/:7.ۼeycl(X,/ M>IM/\7 fZ'y05 IBp.]/^ie~PzgsRvʔ/Q'z( Zm/[VBƋ !Cʹ9W'VEWKŮ:-~٦̍P^ċͬ^0IJdd4ݓ^aEak%D/2]߿õ+$eK&ymZ_*wo{mL ,4 =[qN=[gZ(q`j1qq+;f/zH,Th3{Ar!gT"&'\_Se '6zmo[и x D4HW+u폗ى/pOY y.זV)9ZAWW'hًD/biE!I C'zLC(~4" 0"ό׶<>A2_nc18=}6zuf\b}s3ԡ'jn(J_$|,زRC,]UDv QH1V6DjV^󶘴M>I5b %z=DHma:#&J.fE(A?S'%\.=2]Y2pzE=hڔ175Wۏ%;g~*ǘTnt60l$S_gƭ4 #Ҥ9'ͽӏpMcD}oC' 4Zmp! qQF0w="f8ǂr6yt61ɚu;C֚+NGN3Q[I?C'W.kNss]mE}qjނ]1/_>N{u2-b'f9h& ce7^HgXёPd**¥WCwln!Js+!NE>1yɚ-8Cۖq*XkIA3m0J/ +,?N$Ma3k SN_v|,>nbɊ n)M33Ys糷ԍ_Et"' }[3V I(joh-:3,K yr`Q\:SdM}^Òyr[W{i/ " ?~NDB#|85yy$~FK &?T}43 Q\MCT0++6:L2em9_UW4j )uz- H|%Ov+Μ=C^;:/_ԓ>w#z쒦mD*%dϜ0'L*q$aPRX/V:|-"X=#G9},=E~.k4ZXTk|cL'8vD&aޠyHVQ.^LJHPqC5N>UZ O޶u.ns?| a N/9'&f+ec򵗟cs]ۢhץy۝mOph/͘l ,Tys%b ӏu4*WWl ߯tafYC'N*׊tWN+0 m^O^͹ L&.Cn&+) Q*"w: Lo<$.ΛMfNYgMPҏCSZ`/>eF6 ~^xhȉ}f>Ȓ(@C'(jބ"%;: Oa oOG~&f`EmyB[>u|?WjMx.u+O)Ğ )6_{9y֗$JKV!YjkD||Ggd^8!SjlQdyBY0dT{ 9ˏZ B&cB9Qͭe)B4GN_Tb_i+ڶU@77Ej/]Mݽy+'s2DL' R0D3tp^ <~ ǘ 6!v1mQO pi\oe~K,Svx,,3c&_ ~H8§\髽v=Jeע:iTj,RDK}$H^\b.U^p3R<և?s~6? |c)]iR?Yץ꺸ܹxC=#uv[$B6lw?XN|:[T̔ H0"65gc9%Nc9Kb&AD~'%uy_=囧9m{!kTj\˒J8 I]LKu;)K B~Uc $3]4pmJ3A@I6Vf}g#Hx%J=<^GƯ"!B|?0$DJIDK@|$<HHaϘ=%2:8Xd Ԉ~ȓWC{ruD[a{`R;r㏟;#wL|&^׀ 026?Ǔy}$_R/6ڧHBQVڸLRfJGӗ>r`;R %luOHSmuK5;R}T0%\!XndΒyy3pi_{6$2LVy3ƅ ?oI:տv29yj*]8gуCX*KVF DOA_h)oAE |E뱐J D/IT9yacn]IT-^ ؼ>u.Ul'~$NHSȚ:tW$_o%a1'yHA&FD݋I`V5*f~Dx=;j~Tݫ61U.mp{Ϟ9 ۾GDzQU "#!YE pX6CV6r(dj{*{yD/6O:$J^̪{A}Σoj+j>/!]l+; h|2 IDAT ^ xw3Y-dҭ>@L3Ye3/dLot$Ҕ-}kcEѹ._v6}?q)I U`Fx3t9k҇nFV=ݍeل ~xh$rEtғWK!DFM DDuRy<!4>QEѸ]< 8ğscC)GUbؚZcxBțVEU~z태tV R#mE %rE%TJU1Dm7?1~~^rRI̳*"7Q 2˰]ۑVƘuGٗm8ɗX{ϝRA>`BFS!%z:P~' =H!1*W2xm a<8 6yHj+5nɕE{n,Xiť40+ꓙ*FeqSvꙛn;lv,rmyOǕ|(;˧q\idy.fox>8Ͻ "Ιe8 []I~a7Ĉ9E=>=Y}x2WZ_umkJom,hC'a&[LY˫z ҹm *+hy Lwi8$sžtѡ3iō|&3柚gqȢ:]>7 YxIN>QPRMov}V9*smAyS=zWeF-C $* "y azEȫ{O'c/YBp'<%u"l5 +P}[vϏϓHA!G'zzNAQD~x^:t?{6]? mmh0DYӶ́Q$SOqS<:o]O{O4DI U؋) l'i@DS_- ˰+^΁K z JSE pj<ҳ9{KҺ.;-޼˼7oprcϱpaW7*~^LN"j& :yS}R¸No"I!ctEP߇jϥסRO?\DBEa912I]v$Tm8@qWԥm}k`Lsc߽t]J;>KuL*4QM+(r']Lo]r:4޿;`b;O+UfM|f_Rw32=UЮ* ld5Lo-ۭհOyu~x(O n:R迄꛿u&iAL0x(_`Xq K9 AH{?sR'|yx=yxK,5nK4>^G5H0,/-ph `;'uQ (aX`;IFZӱUhy-LmVa2R]IZ8X#^ړT囗';A(/&}VL*X&g9Ϝbn2G&ɮ'곇cr||"͹ }f]&GBAs䰈eAp~HA?7jXՉ bSyYD|X_N<7ZXo6wZ8塔=2 OoP<[k`i'NPDvP*t 9<3y i-K;W:DP:|̾Y!޽+3{GB6@X2'{BjLJ1x~y1(]EaW7m .]"zrTü0Vlf_h1|w:DDJ24`:$V^ю$Wnmz6ĕ@AVhKyę9(0=/)m ymQa_x|{J_?_C!d~[y"TOKRH< UoޢUj[>"8FAc\޷QAoW_ Q~΢vyҷJofaA}NwIpǵ+"q&K3_m1o]$uOB{eyBiV) u9eED&.kS]~7M,BlQD~7-f:.Ama=}/2)iu| \ķcg*C,;%yyX ]_AӖ9TN HT"[Eg} D|Lϗx[B`#eȶ0mK唈k,B|%]5"@+TȪx+EAo-I`Xbͨ2€=~|k: tX"Ӳ+)r< =n\en5 ceW݄;>R:HMz=نsރ@ԝ=H>]eˏ-dE ش+/?V%QDfP޹|APkW'lY*wE("u""&L$ߺW;_EYb88 G5}<2(FPꊡ"*B'wEDϴ z@'vD6z@H jpY8a|9SWWoސnP1p*ñ%*irX:+7V;;fvPU'՜ef߁ ο4=VP.F.ggv ٕ ÞrJtaBF)'%[*{B)PMT4%L)d4oe0S `rՂ'ȠmL4>^>v$W"٤ON.]lt[iŹsG>8MUx}ɔkTi7esfj5oݡZMϭkmzԪ\fzg?WQ*UtȚ S"#)5P/L*Za҄NIFxnL#o fݶ'`~^\ztTQT9uS\yyk,^Eutc**&F˶S,,J"w3"8z(a;DP%y &L9}ߊ%2f=EF+L&9~Gt;H;ce&~fYynHg{LL&uIw`28@mrV$)'O+/Ѹ@bqaN1KzhJ3Q^V?14ZfZys8!v!TɐC/[tM%+[)R{oOE"|ІCv Tԍa!#!M|#7ȭUWjcovhmzLwp/^q`a~_^ۋ4;gLyF~4A,E29t-@C^/9LȾH Z$@'EA3ltEZ"5?j)u(.Z<51-!5ϯ @d@WE$6q9ΣZ7/&fw޼Ƶ+8~zRf*~ZV H_ ƤNE[i?EݯۘrD20 փ^)^E"T/S2vSg;]d>^w{g7%B8 !-gHgm3d1&~<y0}mEkˏcy{PW1AnwV2Ws$_swI{-sq DZ}!A$4_V1hw~3Ǐsu§`;\7 e8s* ʀ!;UK6o``(~HPĵpA&Snuk)dIS(%^2]KqV"⧠nUjb^ C8j~aյLo+* 6X ܥ@ƭ6*{|:0MRk7:})W֛ [H01S0Q?=NFݤc}U[h7wpd=JbeEz?DB6~3o{>O!Q} E 9v $Sy_W"MRvVk>ΩC_aE~xEܐ JߡME磮)bc*<^7α}oiw޼#Qʗ8yqŎxCɰky>#d/>Q$-&]=H,B#ҷPh*JЬ7X#m$.*JӒ@`OĴ~kC0;+MgOr7~Էos23q^ąi;Euv$6ꌔedچZ)Lj~f:)8,&ۿejd~^^DjR ~#z!̯Io~uo/BJFGKjte\v;r yYdUڕ[쟫plT:^r iwP)Kv۬kr]J|2+b87 Ȑ\Hqe~`LTElFd^9GϴI@GOœ'sܹ..LA_^ ؁Q TG'IYF9 W&|U̮f@xNMWe`eYY.F| ҄{"Xq-\[F yLTHcȤ+M瀇&{ߋPN):9C,O{ Uh!jپ n]T| FH3=(meQ9+Gܽ3Yn-ޟq3L:@XQ |reIȜ=, R5/nWP e@g;O=' 6ϰ;c씫"l1 kv=nYzҫ>{rJ}oG"מ/߮~sCz?#N5 pug,aN#CTPV ôL:.SZ^?_Vq>p~XTLSVF]M}LF*CS %UV~zI+f:!(>$r}^[ =Afo˧c[!aWO[37utd(ݱhѸLJmFBoUe"vx+h҅Hu+D[1rnT㵿tHXYrU"M9de R7w䱴@d-*!/]7,I^E "%,IU[(P4^"d!/ 7b jn=[C?nP ^>H~Jާ߷l3R8͵x*oz7{|+ GNR-q+ED3 $~N\ [O1 h[L< vtaDbzBwXmS3ˣM*WqrjgU=<@$qjdw IDATN&*s:]*UvS*[NI6߹xowcK 'oo΋~'O3^b@ld$C?y|P)=Ot`B "c ۦa^*?Ox*^c\CSm/s lSڇCI\Ս4 8fW^y'ۊ Z0E쩶P﫢r6c/;x>_}?{ng0ՊtejQsytژ,1t4 46k?oQ+;?e4nY<B}^< SǢF#Qa/(Ua6 I>Vus=Ԫj j@nnRo0.4A?{!} 96ߦ`3:|! u?O I 9zU{ ztmI_fs᭛KKtޟ2OQ_Y8XuQ@!;Bt&AeAYg)RS</U:PA*wh^=[X/%x-I?Z.$ՉHEо%t`jT- +V6&6W҇,}<T :O/) YV,Q_H^ɔJԓ0z:YSbO'zjEUrJ @D}f w'@n*qGo'[n|Ly3 p닾I_p*T+2U92K\=_0avGɌwhSPUQi"CIT^0Կ/"aH"ݏqLa+ )RtZ IU2Zt^ u-qs~O^a'6|Lww޼o^cY\HkV\^7lY +y!Ag|DҷRB$JLfPu $pjz_4r$EV;Lժ}Cv];錙7l{wɉ?`nѤʾ}ӌV,lc"0ZW("@EثeJ̼ys ޮA6E F7qL`N#QzVy az̦SUOľS?9L:]Μc2 z * `7cI0Ќ2(#1Љ"o%d#&&\83t2|T,|/~J|ҿah\dҮ.F%iZHD|ٓ|1v Ӱ?Nx//Èe1?]eة{(,..q-DHGa-} se|^xh+_s3ò,( 'A7t' \{Bc m꽮ZdREvڕPoP0l4BQ[t•k/?$(_~Ax~e%T2K<65ˉxX%^Yl0q*VmcMN9̦V0jL&t-+Ilz=wSK6x[!Xfj~ L>u6jzN>bu` cC( ɜ蹮L!¤2EOgvvϿoǬX+#YAUxSKo $%>^N"<>3-O˴8B@ *mEj_ј'O9FVo%V %{0bi<է$ޮMNtr`OtG:[ hQP <˿O+~r4mA5߅V\h|6M/dG`L}L̴q%_6=~znƩc!C$zNxoi-r-&20jIW%__i{>cœ/rhZg5}+g"{eǥEQMWD|JN_k=QυqnEK?xd<%{bږ QϷPY?k}JDg{0 % ;m| Hצ?6:>UOҬLLm^zs/js~:̽?RaϾxGN_֡vWt)],uƾ{4aہ^Q{Hۘ)"]87:qں%BJ;e*T+I~3 YCq(IU󶤯;'9寰ZKWi>]{Wo31#gyPwm LMHbQDyJ}odM@S)h}c7dNLrGFVjR-Qzh:Oi =YOMzS,9v}fȟ|󄌾t $z@wSZ1&@-SuCIt3P|e̷"J+ʽ;B^) z|'B+z-[JRB;UȔz޽{ݶ_iOtO*mn&#%sz7_(1semۋrd܏J[x":w_z2 4y^)(=AkCxN9uH!M7Ӄ=d$y 7G&i >ii,j_AP ("9zge ȓg;mM *ϧi0a7Ng|K%+Myk16Es_屳gxtb];"!a( nIbHS-gFk +q? TJm1)TU4t>}q|OFض}JUNS_?S$/^BX(Lפ@MTfĺo~GVPR߿SYXة צ lP5^>տ6+1h>:HX7-8 "YrWӎ e"<ӷ%jvD%{cvH>Ћ)7%Sbmu)gNJ %zv;2ѯn+8t{T>M̉yYlgg+f4pdD~p*V_3'gP"p ZItW/\kkA1~d0KJӣ=S_!zx(7"A+8ceipLʦSq*ckFT} SΫOr`,i9+* bʂЭM-KAb^^+m{I"?ZW2dKC #l}O!OTz-/bW6N䊠z>B_!dRޢ=,ɿy~ %Sx_Ϝ;"n\gôux_9 Oצ芀B>V, !=ӧaȤ -g Ӡ 9Rȴk&jYq"~ " T;qөZvs-SCqCrT>/;/歄Py9 "dq?^x˭=}e(&{AI! 7}/$csJ]Lk'orӼ|dmt_e +j5J9׋HLw-i6|7L%p_ϖWySF?1ce!,d /JmN6͚oL虮yL>$yS А]jFkd 5>^]FJϾxplwhIVVE>uX#2Jv)-噣31yv: U'mH>Q⧩~bΰe9!+2Z&?ZÂH _[6!hٳU$ "<(b YLBݴPDF9= XmwQ[Q@ޗ 9CִyHW5߰=nK/!@G1{)%-yI"1oP_^u(wrI:#pSOXϮ'|7^Lu:M.I*u%nXQ~:PK`4Xw\Fly\!pM&ef>$i̘ecP˨H]cUǑ^^/զ  %BfH9"6X{oQ>^Ygk\D.*$n% J@9b-$Rt ِ`%>Kճl&*6SGgr[@jWN򅫌bFr]~"t(-N,Ȓh1ɐQY"؆VH$Sk0a0[!7 %\M - pHBDcaa:knhmEUx:,J&Hd@)BtrCCicLEvr0 R^r "ѣ[C"Et2+u\L:PQDWGr]}%N>:'}.]/%ɅGJncLզz[c T>iga2St0'h9~p V`:4EױS)cy!6Խ}l$}AvF$C4e aNt# N;}Fr06N8fn. w ;#9 ]Ń h@`"b$qE6,TAy*vϋ/@j&s`LaEziF.y )no}SUCٺ݅=6{|JϼN[U kG7Ă4eKu^P/<[ MM.={(I8~vȔ\~$AA&qNlv#$"~{ըd*H)(Ha֫BȆ nUE](;I+iV@8jq6nCzG)!*anJ!ySQ=zw#N_~tN1DoLn7-z?R_o:/>/YP ȞMi|?$[ VٱQYXcadK6FZq"n(C1r ;Br7;pb< 5 fh5}bI& 1&kMUȥȜ*HJf0 :HR+M0|p &4QШKI-jD(}m~)mZ7S%Sm歪Ukx/L5y6[C|tTŇ"k:%/yW>\ݸ_~3w+'5DŽ۳`L 'R!!|L c/¥K [aW"zn͚O3uͿ}{VNZ1 \ R?;r8B4jXG$J[cVa|Dծ5յ |AsS)UJ/VO(ȝl? k0,.Z K͎bd뫀&"'zU2+yD7 A'^D;mML)K$nsgy|S G?i"{c6'>V<$PzdC=J@$[!xsnVEšT렀M FP >p"ri# G8d2#)XȡEwRUPZ巆!3a7Pn~cU-e|rv??Ӡ9'&1jׄ2 s5z)(Hz*(1x(|F5ȕ1p4m1 [tD$aiD299%޾[o^M׾7wF.w|£y-&^d9k>,@ڥ.J]բ"4#WǨwrי*1Òa+x5^֜"2(g&MWjޯ8ʃDl?1nA*q:g1sk뷻Ws*bznJovJކC3c""xDVYK;&!=`l)J)N=fn♚;~N ѻ.áW~"}6K OZ;KM~kdvA# ˬ >Vt(NRyFƑ<fy+*=,sv{5+gsP"z6 у{/7F)&'qʤ$$wF#d60< Pb}7Aj۷8ja8' 磷'%#=\bmr^Ť v2DEm_ȝ`TI'@t'aTPVU"7MM {0cxAqt9^Lvw3'~}zo7w^Թ7~?up?$S-G p]L _xbA Rle)&[͹ڴuHSL{55y 8ߑ2=j93O_dPDہ&%Vj`D8 HC-ۺelOsnQڋ/tp3AKdD:Nc1IMEjۃֹ}ve 8i)4_>PQʩ,^PF5Ɣ\W6 )smwC撄s\z U/'z(?y_y۸H|ۣI\V;JmQcki(s^Xxlv3YKս#L (8 g;H1pJφIs"b .a}p_o K/9lj?^o"/}\Z3q.TPI< v@ȇi%;](sa{P| IDATFj!tkRI0l2gx"vP]*VEڄHLO:v}斛^/d~J-?ʹ?TnOo Yg5= !AEPKgKݒ\F% hq|0޷ߘ7 ,6D &"Xh eR6~9 ~_JI6{-f?s6p[t,s'ID68'Y3^ IVwګi] 'g&pE@7=45ϕ=g c>b2>sfۜ y +sxH{נogKEh>2 `X%{P <X{8Z&&'|Z_W,/]f{VJ!waT+[*\j Cp8ـO_(]UaC4ԾMwAuҥ SE2rvBU Tac6f;U^D E c\@WBpX!S5r(Mpcglv;EZƌ;1Ut& r\4TI-␒%mY. NW[>xV`R{VއD.u>yf=MiDs66 b:9{ӿƽLk#n Z8 zh"Դ$aL/'a8иn}n8L!v fVC jnc+AW"Ny?~]Iͻ4NaGZZc뇼5PQpLy}t^I@ HI7>4ERy mU493D."h4[-]_93x<aBʔaOr)wؼ }֭K:JOCD |V{C/hTut]fvK~] lhB popp켣Cvf8;`osqW 3f/L}@)|W>(ҢTa4%-!#id Lso}2P||fZ\~[nls34 jNpiWyju(yf~OCl$[V[doy\6Is¦9Ȝ^)SogAРJ6mL"6<LO g6ɳqPɣ!UP&vV]=29A WmDܙoQsS< =qmW⁸I.{(JrtQ<ә;q}|xj2r|npϬv :@Z s[hlT?m( X/ݚԈ w JfqC[Ko훹_i%Ɠ4H||d`|Dm3\*-+r"D5eߚө%nʡfdDllHmݏ@gG4R!鉜GqN&H~_Xe ;XUADCUNki9fN-2 !_ 3DJN $ h,dg,2 I8\n ;*bnOc)ՠlԔTvsOL=!EU:ڰ}.Ϋf>+g>J$yef{E&PHtߝIt3 d+y^>?8mgP%XHqW%wݴ%/"{DjcU=8dh[/q[^x߾[ ϿW&=}^}~˯PVȒuR1~ȆUIQ&oO=id򌪗6KQyvŸeә#$W@ f&qDφMʪp5φٿyM*\6fNBg+s=] =HUpy{Wu>r"h?3fnxS&=斋_ {p=5׼Y8]=nu=R;s~23^5=OF,A`@"?"rRjM&i~1fGs(h 2]p=pfE4ELSԚ-}8f^nc;eDCss7h+}5O7B f;*ǡn}_ZcEqtH`.a>[kۼ9}rEeBVFKTI^n:'ڪTII,"\ #~mv|=W޺;4-b ivZ\jd`fvDjVg˕/-rfoD EG % g,/96S [Yqjv ̐ g)VLQR_ %'z5cΉ_ƣg$Pzn+n2;tX=o+BrnF==^ݪqg0ل^o !XD& 1Uu.qkf:QLҽ^(.ƒlB˞&ݫcݓ{"4+V`ĽI?}L6T57!޽]:s]'( 5]/56-3G6GQ}0ۊgU_MGIvT`3#)up 8:ɇ P)Z"3(7+h-@+V (8J=U%?hO.6 -fssR_\X~t%XXJkF^/|Ĺi5*lW%GRҗ0Ix֭\LtT`\G Zm-N{JkY cz.y]> <\ʥ-g:lnr>ڥˢEqYR]8c4C=S92Lףzr0W󙶾ӏ"˧^,>$<!rStC=IU2_9ܮ9"hšP=83mà'G,*?k%|Ng̰v zG9&i**zg٨>쁵 l Jm }Oؾc0$'82b`/k9^%pZAWӾ0WW{e0L5m/{T݆/{ߦzlT>| ;Z።7{>?wf5dϹ*Vtֲ&7vw4}}w3mp5)7 Oy#3SM\:+;2j3M`8i@FuqN^a=&&8J%K?fNDf, eT~op{ͽ@x35UՔBax~@J 5tPPy">?-;=&ڜg,?q^z;7{:*ׯf3uHBHBOQ[V u] F5DأܢdĖ[}^'ch <$rS'>"IR|yumWGL86vջzMzdV<}:׽]hAf^Lj ms烐'/qknsaYUAPdoN#8j՞K)gF8JG$ pq;js@d*ťP*;2ڎC]nۻ"Eyxs (fTq%Xga ޤ:0c}V5m1U7mLibfYoA٠bjIہ21c3?M?I2UTZ22ɠ/~_dm>{3OH_z8ow޿@kۊd|Фb&h15YC9*>C(C qDCnwڛQ/=eBWU@zHIJJ&~6G@ JU XpRTv~&g%@$L%dnuvx ^e<_b(ہ"z;^os(% xW?ILW=}݄Ϣ;U*) 4Uqk uoG8oM*ib\ۃ>qg~9*yEbVQR%Y6:۰ɪu Z5Cc޸ka`*l&zʾlHh/}1M`D@])FYUonYW.|(V0>v0L|T@z<w.}q#KYuaFpcڋf%HAnK2JDm&#f2/~8S_屧(:~H[uGW~4f/AziAAR=kE`u6~E<ʹNAL:JcpRR[\ޓX:LG`imSl%OAPIvB\׾GSlR8 vW$P&zIߕ6m 7Q]fgݜ(7J!1zBUʤдҗARE[I&yuf)|gE $F {+ S ʥI(&35\>1H~fGς İNok+I>y]t &d>, iH$D>xiPdR_re柏O]a>95"7]d!v˰ D\!AkEZG4NԀ m`j.kU_q I[k:ޛe̺v4hu};jnwTׅѥk7XvzfzloZ2`7UW>d]Vɵ¤nUarYa@)M[< ~}P30ܨUԐ3= ܸ6(Wi۷Lu\ %_̛ys0dԻ<0GlToÞgG۪9PN@J$H ur7Lj-o O.L`ȟg6Hk>P*ҵj|l/Ҍ" bdнM*Hz/{/~̿̕9ӑ=rڻ~.f3N5q #z2.GHH\wԱ Bntw;w|W]sԹon*"\X4^ĠJ PɒAfvy4pPe(xwm)Uh>ƿmdѾL%qT s$3jiFZ|9_>F~^%b:֠ռYU{djJ$^S;[5+WVtnOӕT0By}I&Gcdn*g `L e(FF``_ n܉|&Gi}&Kkq4Mi.w{c&G)5_63B~c*^?ͿyK u2//q9(YQz=45")o\[ xLc9 %ԃ&Rf'UΤ۫pkl ˨Œg (#l(νi^/sYpR姗L cSo/0]fp8><Sϐ XuUi5mµM֭qVP8{? qVll<խ0qζ2޽jZy (pCͧ&Ll+5M?"Ÿ( sRsձ`[կP/jKx(w>r*81bp(¨cwd0 =кHLDzf,#Ў 9Nh|}NII8Զhc *(kBwJRj3^eDPKCi8Ph\_kE Yvkm[[#jpwpxŠ'3$b.*Wqgpyh7јǟvNЄ"~UHMWHf*_9Gp*R;g-DנJpKT+X4Q6&ty2kU{dk'L5 587*:8KV%"YZ94Ф18 sA݌!`_q+P1hVV!g0ig,ڋ:WWy^&\y_ޭc݈^w Fi4|g_c 4 QR Ǧ$Mŧ IDAT PuCCѓ8fm>g:yi1˵gL&c\1LanBCR2[ͫ fZmW7k3$` U5c :{}b]=5;iW?y_z? Z)q;ow!<ғ<i})i)7ߍP. d HH # e.3#IM+wRJwP:R"2MT0 pAKw9'b$Z6B*%CQ "i"E^k2*}ѦE9!4;bLIFzHIqAۋR]),nOIVQ̵3 O=&W{僙S^(MUF|}?LZ~\EG8: 1$wv02ʧQY}^JDr~9ysG-KIOF~%z/bz[6{Q )nd")^n6x,] oWn%[fuN//]yT,S$&%tuPoni,4" AO;F$LZ p|0$ڗ+pI2%|"'ׅ% ;JRjܺ*LQ PBay')]8R9/ղP܆ pa`SEc@~Ğ69AD)&} o2m4Ӧ-u\ɳs: p#8E$Ωjscz̩3.>ަx<(e%o8R1Ad t!aR#faN]8ܧ:8pHn3\J.\Xt0h=;H6Pkt 9FHO\G@e 5eL-ܼL:AT.Q+aq(iدm5$jv=qn˩[ʞM^@/o&o*"Kǻhe?~v8}ſ0dXNzY(vD_5/´_'D?/laF7 9n2 Ү |cC=ӧ /mS o4dDT!Q$#%4o: 2nȜ\ ,9rg͓/0ELu2Q6٪"R6I(bX50I[= CӋSIO y1{IIXn*0Cщ G*izlGL{_ 7I$'rg~ۈӄLf,RQm[=/U@4t5 VuUV%a~ Nl'Elfm}F3پy6d,w}E:k.ఈ-a[w~x$`G]{|/o/2[W$3)~ y6^k;g/}tY|Dq^V6@^}K 03Ŋ+‡"{ Ecې9sJE+&zfIӖV\=hK Ͳ\ERTyE|$eEŃG naCq>\er9@Z$!@jupu\sXU~$zޤ6 &dbTcj0ל!G,y*F -#K٨o&jاٖٞMp{|jXJRSKCK|;P+~Nl8Go@zC8].V@('!W(/D l-%nPd-q[3Wﵮ T{[ikJn+g8ǐ$Ly ɿ!O~3<}"GTȨ66;-s:ٛgW—?1+&wR UИi~ /95V[Ef@,,O>fȞA9 O %1D*p6Ѻ ٪Y ݦ)BP/Б,`ք86:&_'^g,*4sxlI?9< ǃPfsaΘ30w; \`2R$^'$Q1M&i27k4 %\\=%bzbqaL5ғdDG[k(&JŹ1[8T^0}c!^?W4(ҭm$u+Y.`s_㪉J@݃JH9% (u fc&uk^]#ہb`i F>|XT%4VU$ y}mWR. WXiѺ 6-8HFGܕ#;PRt!yd~Rݧl,³yoR*3L0u'yPU։G ;]8'L #ȝQВ|seONq\]_?>q?~Q^'[ptsOjXs!rioEfUM/Ds=C̪*iaݰ[V kqn Iul&^Y4gXt]E?c[abYj2RqD()]E\%`.M 'HDoN#j8 `I e)x%(R#K\\brUϴj W VMS%}"zQ:DIV@-sUefA)%=kǒ|-Qj)]Qb JJ5pP\NަRݳK_s]6m8x8G{)qI&Ii'&uՋo$p, QHGK)LO Ji|E N?c.y)΢JgbvnđH2RtK ;ʨ ZQбE4m Թe}14]D;2f`G>eg#bT=,j\5ENoȡ X_ct~aEJ^ ^90p]d}U~.y ћ8^nݱKWl@8ԛMk79pz\]>*&Gq4ք{ w:﬩^4|w GS*٠A5:h`4,@ǵq#Avy RTڪ!PU'จFM=OI|1Hd:U$o93 Qf*1v 5(i6c\nJyK @AiiPZt#$St@vV_n#s*pt0 6Y0;hFbTIgq4J "pUS:c<&zrQ;HS9}Gr+Rǣ9 3|"GoC"\a'Lu˽S_]GRϋ%$ur_@9,5``(nG+I ˰{=:`K< h]I׵C&OUFwᩕ2/YMƔnqy_t Mz8av0*-orjؾb$K#=BJe&Ud竚kB l_) pOJQ!Swt %s"ϯr3pz ^Ǖ)K,F8pLdؑ1&DYO\^n'IF#vo]a‹n2Ÿk;5_H)5uOX(g$PB[+`T=S#cMRD\w,cFgGzh:XptHv'"% SL܇/T$ПuI{t9t;P&aC&6ᓱO$ ZZN%:h-uZl6Ynsy $RyҌÍ=s>_+<}c'i ߋ#u> Lht*9"W]S_x$]H6ϓJ<eX!PCU*^ IdiG$Ż =/*;Eǡ#u֊9dG"{#(,(&RQ=(s[y<#Tf3đ06[3A5;u̯kJJw 2rs=O~ <@8c7[1*XjԄz&2.Y/34OM=[) 3~ՒY&Ji3m2GKLr(2}2H<:Tq6cݗj82'W\2$sA%cw%Y ?Sn)\sp#SE&eRM?2d"}I+ I[>iszU[O _4L':NՏ'6ELf` FiizH=C DpI\\i`Uf]s%rQzcLA@pB[Sd֭7g4.T¥yv 8RFZcMq_OL8\{ϗ|>o+ŗs&  | ϱW5q+ydO} WǮdaRqƢes<BF2d_RG_ϕ P>{i$qk̶+s$aT )s=HZq5 wWU$\6&(T`RG[M+V!{۴[>3 ?W%aNShvZ?" ]<'I}V}(8^[1p@lvY O9^ʒ9 ׅ4þmH@jd 0[YȲҫɓPDOڠd^q 3A3bJH$5$*2aalUᐐU4_'#߰I A5 CK*.(8"@g.2e3G&CJ+H9!j5H[,K٬ F#[S0PC9U>2J u>,Ju`T)|Z6GMIӧ-FuՐ>l?Oߑ((iin?F^w33k0&Ak@.!3N ΉY\|^fcx\l\6l~\r'B|BuJt#Ԁ` lLg^=*OVӤtW?ꭷz3(=2d{zw81nZ"\@u:~!곿} d=B&!¿^fsm^Ln?/%\L-y[߾/>WkPMxeC;mҔKk(˨ۀX'yאRi}-$^˜^+A$+`.t ?mVfNG05E؂F-*R%rm$ʓ$kcݰM|D6'њ?#h^iXJ2*M(),"pqr m ,T0.˞n%;vc/lg> pZrƽ^@\g*J]YtV)Ӣy9րGt&uL8bJIPY9F1RcNJ% iFsX&e$8g)@8c(7F tI9V@!do4s(d5}3rFJj,=5[0- ^-uF}Ħz 8-^'g 1,B4HՎvl^~_BJ%|Jmpذ&ϭ`+L/ ddonbK(@’R, pBڊs%\Lړ+4gΟ}ط{ Oy@nAk^Çq,/z5B6;ܾ ;yR(=㗘8D kHӹ9~eHƐ,.n8XĕdYi-3?J{Vqd gYV"/8H`*N H2lxgr15T\(#SOPϱ4wIS&)%-*6I@&imdF0,-'R>f0YJeD‰GNiw}IֱʔoI}ۻa~_+M46޻7 Of-zmu"}PR#ruIv&@̵iA3ge4K˄.rQ0xw蛄s3;{đ>8 09%I]&`nF)]Qsi޹N!G=O=ð/d6na<2+MBӈ" ^X\ ѻ,8C:AGBtTs-: y*2։^yqb5V 'i1ؚp KNE# 4v&AHIR8o3G vZdžRh Z{~4{oM06Jizu`00܄;.fʑ,c:k7n`"z`4LF-̳_hMk8}ҹ=!ih7 U1F%-=͛#AcMv0ٜ[訓'krYP`} YKPc@Y兜'hXvH'igA]{I^Zvy Zy$0 |:{~1(6WAvmͱqdD Rf o]&Hu@iPXLڎ+W@܂.>a⏁GaN>, α+:'qT$c-I@РS xMQ& ؆-h dS#t ]5#JR{R2̠hMM?DV:9Gn4SuKD!n0>NݕkFaQ q+ #t)<#N9GbCJixҥ4SNfA8_4)E79;H T,3┵<'fb#`ײ ۅ\տTB[!O?޵:d)* DO(H'z1>uwC|ݎ謪F&ހ}^ Iʜ-(Wecjo\q v_z$y? s%>~xѓ{[Obt4qV|+]J%xŗ^U.n|HٴJʥk/H[_Jbvt}4> 0QE>񱕲^AL#2^⫟P6q[@$MbiYi1ZR<{ۗrǙZXQbnojOq*_:='). l7@k±O=n][78zù̳P;sy9?NohS]te^6 .8FxƕB (Yш۫JS0L TPH)q/Hn$TȀIxO/I^lJ4~6PF󝬠3fž'T3MqAi%گS+@&Y>DSrT55b:MD-@F5"D߆mYsx'`1R4RnP0p#4--LoNU.ˆ4T 9RcH҇qIȥ$0ȹcB8fY<#%UZ!F+0VHRQm=c)d:&V?,}Pm]LTPM_@Nujzĭʡ$jۈ. HS-J =g3ϖ-16hf̴{[}tn0yeR.Q@~p#+!+mi;>kLYVde)եK|?ȍtߺ@gH}^.8|?q8*ЪW}<!~7u3OıccC0>yfsm]̷>! "{U`/Fuvc7,;5p唎E^HІNJ:F~%q2ƢrCDn(I]@9 hD0!M]P/êmWI"w[o۵ظ#d{ l r:|1|lBƵ/.O~+-}M4{E ]7~Q?_fM~Cm Ψ8`ÁQ"T# d%ahxr@ ?~aId7+^Vyre-MЉb8FJ~7PkUG2uHғ4|c\ 0{✑{e~@ UAiqv$װ?ovLL>E.IȰcZM*f_NdSL[ÒjDX@|D ra>ݕ030P 8h%EʶY81he)DJڭ^f9:ߴء B)8GȜ&)If 7$D$-I9кpIG\N ۄbvK˾-$ruyLBDx웜wy AcCtnňEڜ-#.nPЯGO5, Aot t  0w,(R=͛ww4`5̶ Zy:3,Sd9~|3|ģb-QHEro}?}f,E{yYKΝ}#dՊ0n)+c4`dXi&Mw>ي|1WjW \",+Vt 8H_Qf_$;ƂN>ܬ!6 ;ўE`EQ;oC,<{ oAK,Mm i8re)ݜokz_r>c0;<e= TsW- [].O=~=D7z vrOI EN PV0fC!9(RK"Ҍl% 4ReZmN8Q$r^,N@ho<+ĉ*ͬJIό3~֥9LCCk 5k-x%%ge[){M] 1$wzhWVS^RtZDl+E]A !"䮉'~xJm7~|'6X2Xe8e -*k@G*9pĜΙ1|3f醊54]uoag%3 $4\S0aHš֎k?ZIP})IVIciu!IR ѷ1D&}O!am-M]cI)'łJ[7>yr[c.M7'̹X7.noB |`=wc/GC+\M?ӒT&M Vt4dJcǚwe"ٕytvDwbz5Fy\[7X8z}6ᵋܺy#_ ?F^[1(o;7 @ɋ!m,svZdћYS:!y^K]\mJWϳ`R`Vd0!-!Rg,bƽ+&.[Ѹ1܀:6)kpM8|q<3g 9Ax..2n[L{k7osN,ͣ]D7]m+78w$/~?_?'ΝEaW P'xW<]媴ߑF?oT H,a^X!TIKQdZ|! @#NñA?\+ACAp[ >S $a1 cx\-/6QMJclc ޱuM6%Hy-^ ZM12My`+ $æf-r:N!sPJVHd}_#4`@bQ=tHz8JTiZie8R2`A`w*3#B %48VnhJQ^54HTU-b4ULd-=oL} f@LdvmQjb%$]0gs6w~ˁGn,#Ĭ1$8JPtC@L;N0cM^+3Os_|sNL5E6<+ }nB7MdfQP8%=[Lhq9CjT(R=LU*BّY\{ٕ'#l]_^ytRK,{5;7{bf/I QuWO̸|6u~9A3( _ܼ!_mp/cBNYcf2ow-lRR͋͵AWTqeP:IJNkfv:˩fbX#)D8DZ3 H1%3 c:Wmޯ88͵rN|lL T7/K42N˭Uq[:9/Om>l/}=)^M6D荙K0\H/O { Z4@^kvT%aM! I08plc#zt@(҄i8y+@a)O*1] #f.@BPES) 4n yɭ!OgJt66sONmMkhƕw#G~X5g4 ZESDFF9~` ֓U V&CLbx2t#yPH> TSgD.VGohFb Y{!D5LiS [Ř[mbeRY]U~v}ְ4<>gyG{7EL IDATS(*ߴ|=)rګNjX` m2DP3,>{8VBBbt:nww vr2Bisjo|wؾu|g8Xz`r|ۑT*mj-ͷ~>h&[I_ ` AqE 4dM֯ 1ɫx/ؑ)#Sbd&Z-LwF>"7tL)zIڽ"[<݈ ia),Z0*%} -nF'?XM ck2ΖXo]vN/KaK|Z`_q)nuygi>9 >wR>@-*EdNfyemc?Z͏E5n]&,\%e@9ȕՂo(URz6uZ9\BuT](!1I*Xm`Ѿ_e$_TPRnܥy"p7_|E聘YC,߹;3Ǵ~>?96DG2Ҕ/کfZrR1drh~~PXIB}猫|_xB$d17 ȸØ0s5+G Y['d,*Ȑf xNg$*aIɋ<ɋF;Uucf;jSs=N_L b6Z4񸍿xnFX[NFL~vaIڽ ,)HN$c Z[b,XQ$o';IZu{!:ʿ Ο>Hb+u @иC_4&mN"zyk9TڪaDQp+W`j^|5z77(42F94yG8vQ` gOiϾ{ËҹӸy]@HR00Lgg5zÍ>8XMGJbeM9*ۚ]vVZ: m!^j Ql4K$לjޕZ!- 9((9O=uK֖ڷ`tBKLwx>uاל絫*t~Y6m.scl ErIcno$ս,<8Krem}6Wȧ;Ka <"G}Bӿ(?t|)3ąT>d%^iE΁Oa9dy]1ۜ|W/#6YZDn"p!!@itBl DXtqZe5 [itZ/TP 4[9HQEIU$ȏ"1뱨 $XXd/f䚬0}Ed؆'ΠF̳Vҧ"E8~p˷a|@HaH 2Å38"!$c\H!爩A} S ȆەD9l/Xb\ALu?aqAx~o+&^hxB'NIQr}͞<:@g$bXY#zDbZk2#/8B4{Qm7/UCt4p#za8LB줔)P_ m},^HLQp@nĚR/"aE8HI9b!/w_B!M>ȫgEiZʛiހg\CmG7|Lك셗[wЍSP <6AMgm $*l'Fѹ!^3M:d7v>m1s]4dZaw4ڷ4M909- h7!qm,6iZm uwc5Ɂ-Ӯ$)2p9G# T]4D?z8DPnSlŵ#W4TR À@HU֚baIHWn$`xwFAGOqkW$hwG$4-Ț\z*O^ph9Ngp:CЍ ] uwۅhUdwspcȻ>?$ pB7{9>_/ BfHƊ/`?%)?ZNO2!߱v8l" S.[䚲Т"bNy7 V): 8ATm1HSU]#)qt"[$0>NfZ ԪuVSѠ%68|aEgaǓ -\E&J[L[hY2C5'@ח- e M#}etC`r!*:ITwH^ W3d+EcsQ{q)}0A[jds#z:Kp,C9Ok`փ4bk&9oƹ ITG%w&%}SϞ糟8Gͫ=!dl؄ڍʕٸMnXF<_zB*>&7޸6[ܼF!{=M>8,#DOg E=[>(KFoےv9VVc(x׹&4^z6.{Ɛ̃+Jj4)|{roG|Cx or_TTn7Ei3w8]AR)ʯk/7GR"}#$CLPL ̹agE 8 2NCJoύ E/F`AIP#m%̚*#4 3D }v 1D7F4d(NW] C NF#L͊)LC9wa #oɌOYUn -${x, GxqSҧ8qe {@uLFULAJbg0gt5f,6 +BQ^l6BoM[_>--@-!U R ͤ)諒 ,z!^G^ ԏ!z`u.88vMkg|OP}J@_] ~VW*}Ԣ؂tJъv2l?+W:"XȂPdTቨ$@gsB@0pppO4Xķs,ƘR_muK%(>8*iB* 5/[G@5f* S0H.;>$C Q{_pɝq[i&NFE#(2U@p[2gٓAj(R' *Ry3L/Z?_![ *)SX$:2Xsd x• 0xo&XG,,Qh+ 2^aW*{&!'}mo:Ӆ}1 ݺ7ܡSqtF(ѻ^ *M&Nʛ9LO~|xYr@Qi&i$t޹B2斅dN͢\ps_4Js ?}q? >w}NOk.s˜>4+K,.+MA;ykBW` vѣUѴaf=yLʑw"{#N*8\h8DeB(Fq3 `rTZo^ Ďv ƅBN*&@.lFHL 7_ *GEk=Kkza I&MD%&R4€$էeЋ.7ƊVOiѦ$F=2.$h&A;GBWZU}UJsMIm$?$u2"a(9p Ac)dcW-hGrezpxly. 1d$%9F zKDD7:Q \ a{hM(c"ZRW - $ Vr^kB7m]+X%zulk FF=ϗbj>1^-_= Mgywʶ{!S)&?|扳_]!j޾ɭkhet>_z/?G}WlsCRTlcDтh oeQ>{]:'QluK 'ۼ%@{B性>uVNo p2C<Í5TD/hMyS'1pg[4}*!i2 H3,f yP)$a֜;uכd$>HB9]jCU`DJ7+$%]Dm+XS NB1'l)a ȐKrsM<1= !B)&\J+;c+''x hB^4ZdPH"-[n^QZl y?KO>m_MT Y/n 6}6nwi4 H3 K!kgݷ/qvC[O=/~?_gpU*~ ?pQLsƠR+7,V:tqۃ ol'RHawOI// /HE*gsBva-a #Ӥs3^M).p1;쏥VXH/_'zaFYbd v $֥iANVwc$T&^CÖOl#~\kNe9z*$|&~LM!z߹,Do ޽ ~t#8}I<|G8W^JaCXp_' @(iTn%ua P~7^Twrw"{ 9$4 ߱ i~C$ZKFXE4&mGЬ)K1@ t4@><CHI`hs'Zg-.J-1R>J)+əf!,{_BG%xdzAHlB|ۚ#|/pѳ4ݐ8"+"F=.y/^'8/oq?UZ9SEy#h8 ܺڥ3S~Ӭ]NnvK'!鱽a'\^>Xks̨h.20H| КA\斵$=S:?yvt ZA%J*ۥU0! "2A, 7.g!MepHeG 䠀,ZL $fT}`Xj>; D/oUUI]NӚwYf2eaW Yĝ qBX-0sa cxY] =;nӫzjrȮ$^޷OoewO;PTx>?w|/E.96ZvyhְkjjHVj/GO׾OGa[y܂l[Kr0K`;CZ?Xi/ "9!@]j 1i)UjJb+a u,hvS0*$`|+Q1D*GlFsh c^jFt- 6Ф?рEl2*@`!:>edb!2a- |hm1͙r##7G#sFVJKo7$TCV.jVMa* Ug5R.Ἧ$h +ӗ P!=pd8FX5Ǯ# DɏU:kD!J24|DdfA${hO_xW[H.{3}e_F◾7~6?g~&ݹތ[i28L:/x6O7 FnL5Owᔂ>ؾ%72{0OFͥݫ_H>q._%W,F%Lr|>BܾkWϗGK&\`%2W5}PբKe_N_O>>(DlE`z◪d@-)p#dRM'm IDAT_QI8?O{tBI1XOβffѣ>whx/ 8StcQ"Y6-g-c\pu PMq"yz t6_CgV\$޺:;;<-6zvfa%G jhoSDl $Y.>94z|-1=ҤmFV9GDҠ j,ׇA@Ic[4Xfо"4mͪ@ X5&H-MT2GB€q: =W-mJ./yL$i{3~mʥz^'pȾz9KQ6#lD>kj .R5(34ǂ_#-O= ~οQ8bv &/Ro¤\e#%aS:b*cM~7ͯN}˳3fKǁ;'syuݞpv)QPӰnq4_jD#Tj4$ )[ړuKM"z;ZHeu"wn7lEPb@"]1-B@5o]/v4ց&x 0ơFD{u44&iń@ećSId4~0zb$5Yr,r ʛvx1X+>*r; D`5SatZX5B?G!7*/HU'$91zٴ==*uBˤn1Jry\T$BVxRqg'7ߢFvHܞgi"?>/~D_U7˿y[* 3*|G{JμHdW;z"W7t n0"@݂̐[#wF"9`ȉtuƷ/Q]@svSo ]GZqG@>/8vR0?qw>Yrriڍ\}G' mk\Z0..\q]˕ N sZEHr3P2*|$O  L&P!m=XR]3{ ng>ݮ-wαś!s׿d740ݑڳyuHk~];Md^̡CҦLi1C2˂0,q'2wUMᐠ!ո0as;Ym O xոf4x"{;Cć8ƨ+ g #e? & *Ȫ(6 |%~~չ$C"E$O1J%҂-鋉0ڮ6"PY יH ي:HygdvtlL$%ec*o`,z*#iMIv ' `aXђz#c%伊Pf=9MΒșpHА6Zˤv@~J~H; AN2+u!)WbH|P68[Gt[WMDt!$l}y{B"'z }x^: \IjbK_ί7)̢,i6K;$$?v"zrw?{)7&;ÜcysT5z=[М #xBG/Ō={Ϧ?yc K4wxc^ZϢXt]61+)ffgq/syzӡ~Ve4 8ư.nCWPFC}Q2[O6Z?;, 2"jG-= 1aVT2UU58ihOih<{wO%#a۷~ -~7Ƀe$GpFpif*0}{3|sų0\C#bpowᆘ"j4y~=p¬e'9XVR@O>As<\QWB8rD0z5ߦ?h uE1DFDz k-D Y(zS t 7C/um hDm,LiR{? qPG!o`QYt },* #A1ER a_NH[l0D qUҕ9/48Z&v@ )Ǥ= 2O q(2 XǼֺ-GkN319w@_ВR $M]?OEƒ5$I EmrHvӹU =3o9P" E&зчpά[>X%%Rg'ID߆R |𨡻=7xm`{D,&w>ۣۼ4;bMl<9w⽕|y ׮;\, ,dNw>yM( <v#KqѮ,*a1#$  Bc<ďLCxDdC oi3/99Y r@m!q%[iEZV.c#dђӜ\|r33]HD>ȍ2S=S@''"}664bL/,Im)!]OT㌕5I\?|JH2PG`:b),1s>=++EOkIZ>Gop|9XNwyb|:_}M٩*_n}{ḰG )q-owz9'3n) ?W9%#nV#ҬX`&u|r4q^Oo|-~u:9,,(3<{J9L\nb՜{[ a 'T[&i]TJ`r +Wf&%kM@ F߆\#-JLB"}8AgU!AS3,,s/ U& r dn_/|Ԓ]ȋ7/%5~)ʛKXmdx㭛JԟMIɉ`X5  jȋCp57)HiCGv㵵=dqjA0ɱZ=oXy|}:Kg=C̄SZm=!((jh,?[DXg0…ʓ1"I0QH`ӹZܳk,ahzS!R%Zb.zωh@$5igdG駎 Ș4 G ڹ9#bKbБ)-GH41S!%AR`b#Lߟf .M%&]%c`~D)IvRRz{114zSwFeύΟ|8d:\l{Ts饋_7}cȨ0åBsY:s$O"G Q.NLkKϋo\ۿm#J5b?l=cgo+9l맗r@)h>"#;\u~Q{ rvR: Xi 5h G>+qvkBT=ct BPRCW jX7]rR "vDo>$WޟLY \?;x~p?A{'f;i󋁒aE9ϙpǽW}J0RSwdy9 %[)f\ZM:-kFx.ve0aNott+UY5Lx*DQخdo ULD*>&^B ٕgڠĩ31&Bc<NjP*-WX]ah HZ@r;ay 0;'z 4i7ح. n. ^>0uQSMjvXM\rg?०O1wLwڰT(vn2ރŢ^=ea֏ןPք0;rYG R `~D)FEZ ӓQBMY3W@91b&k:4v?G-➦mr,-zF"i ^)+z-mQd`eMDig@ۘ3tNC98+4zCS쳠SjW{GA5d2bnUWTܾSN]öqLÝ3m &J;Gu=[Q(>+$[~&&,}dDKz,9֊yu8MNiVcTӸ+pK?=f/SRz,z~ѡ>}[ιʸ̓`=p/lMv/Vi"Wз'a<L4BDp7,qX!yk0$ro<-4A*پ%#ˮ[>hɦ̕%wC(H;i8cy y9w=D/<3-I0X=COۆG1{0㬍YB2V@zԊ&c~PK)=9Hʗ6\IPtJbE}+9j/A%>F{rQf$b'Dr !-g\h2|9l-\J<̈Έlau"`SM^"O2|괽 zQ8 B3-V|DςGG_>5" ( 9dj~}!B? U\BʽG'ko H4!ϧAB*Y]FZͻ2[٠)QK_2|H{jhD8% s6`p$]ss_|7\v5_B.qV\x<}iYW|xLdW+_ps4YT6<9W.U|k,[Ԯ5uN}Nun6 [5+̴Yn6$͊>8cؾ oiEZ|y'`>;ɓϡ[ 3ތ鄝)׮lN=crcDsJR6mH/_]ӶwOsZ!o_ݳBvfaͭ&@XI<m⠟O+icz77ۣiD ΙѸW.7['ϋ ٯ7jUr_}OSk|%=5~ vN4^D0BJ3 }W+d(b"欄u-BjT( K죾uۥ P/c1CvW^m&7DɿI~.#WkGDп{c%!GB" IDATk4ٷ(KK%'K}vJTçv0a@eϿ1l',|m'|tpʿWz/И^TC,_?%yܨL.HjQޑbCW<Og;#fI.!ؕ_5WdI׀u^~f9}fsaEU _o@24*w&f͸^bk*ϗ>1>/iYUhF5f=pl/8[!`qM8k R q4F*[46?^hA_=uג}JWV(e ,$KR41BIl;DV92#燱t)sۼ|̚6 t?W 5w$cb41#G{c:%skLB'u~|z4墝rOpB,?G Y,Y\dhְCŅ=iZ%McH!Q^9 GϗB\ ~oIYdLEoV;Dܿ nL<_'I8/ 聶I-e~AҀȑ1tl7] (!J@F"F=Qjrz-[A7JgXR$M:D{#3K |kcl$b?7n0jŸ4eJT?->zKi!7/i~9Uaڳ:U0vE\rX '@4$%<7I1\%zN^]wigΔ N΄O;^ygwgשxweԞ#zmJ KeC=?_&x )VdWI=~QI*ArݮTH^ۅA+ZiudoOOʣݯ)߽$[ ]~&v/+*R.;@d.|`8ElNy<%wXmCoEvvk.H1D DO̷qd(MD+&8A6"!! >@6]," 7J'KK}*>*7I h Xp"x0X .R!D5qumĎe$'RMZ)-㣏I8 i,k:DDFڃU߆RaK*%F AXIϢKmCP>܍i? qIiֽBv$o@ B@M>H0E&%-# v6is'S4j:ᬼ3 y\>ŧpαJFS\ر my8ΝcKX>x=s'?\Kv4^OwwټICZ6`Ll\qЉPXنՐ I:XM8d=1bE\ީ;,ibK]OM7yWؙ%Wӭ)5>۷ u4 b_+g|n\hnG n}t̟_s&OHM_3{(O%d?s#xI6b%'/g}%.Z,BԿ/,<]h_*Sg')p$;fn4o~rD_g\I$ZxXÕ{L#fXx8R8n2IE#}jbY$ͭ3FBB㈞l;2'1^"}6 W%$ Č )# L)敩TH-%詏nQg+C=tK^ke ?s%"W55x#|ląD^f@2VTKvqJ t n5'KkL,uȱy0fxkx>pv "V@Eh4BW͖Y9ZpJ /`8M?o3N2`D;+iG"@~'ĩS#>M0%|7!,qx* lG7oq`T+|B"Q(TDu>emE"'Tnwx;֥5AM 8Mf^mȼ'FFZTX qڮDЙZKzROb8ٴ9DHañd#mrqa gd{QcMgKNS"a}ų`Y3_k8Z(p&G?)WJ`F0$M=4(Jn}V)+F\NK+"#f"D#5du!R#F /  @nOIߡ&Z  mHU?\}Q5uY\(묖_#mSFRXD5Q4!D&Z|ۻD VgILrH?/O\&cŃ@b1HCeK^% xI@"8ڶ~b1:KY=_r^;PM)^cY% Z g!y]h$%iϮ4 B}h?21Dcv]3w>)ǘmrOiO|y=}V֤|+H{2*&]NXFR9l38^=Ǎ͊<0ݮ ]i[blYˆn`!lE_{1t5}΀߻F5;1,!ttnLjMN c{ow8/Y6/7\J4~$/%$=6Sf hBW4p#o7O':G;̺D6 ZB$| yI+ͷa!iW/K.Go[sE@wݚ``Kqw@\qg~z!W>>V = lM, V*Q(۱26 ÙK-E]Nn_PSn\Mɔ0!_H/JI,6~\z*,>kڎjd9o+@M*+Iyot/)1-`˦r>#)I3T>к B"+whZumIRBA>!A0|#XE tt}#q}"itx-{^pc.$ cZACح&ֆݽ1gKbdr8@;Éď{زWќFe7IY'tJy|zY}a(s/ɓL`|"ћL,fGBG\לj9b XiNXɕ4nLg&_ŷ>/yW91;ݯ\ #\[\RireE5 |gj=R24غIW䠊h'm|Ů}]eTFr ŪɸAJ @GfÏ5)wB#T8WNn#% D? $0[% Cj\C"vJa[ώ'DavEoAJ9@Ls+lA k=1a8@4}tn'b'FWAk%9fdG!}FVlJ1B."f*#Y%>HYK !}Ax % V()χ>e 2JDfG%fA-@N9:C|uPyv&:z[cCu(*O?7't9wuh' ^#F/1@Եm=՟DM勁o/fLcV/j=t{"ڟ Dp߳, &Vr^ۭEƵ߸W{-VZu|,};ϖ螪ExNk}\e:ū8;Dcs}Ww+>^n<-4+|뭯;jfpFkz WvWr19 %~7&1oW=3w׼wu|l-5jҗiuqA{E/ *fP3FCPHBf|ӭESB{T+0?$R_oQ]7 4b967i@]hjxFӑ=KRcY~N9ܜòyBU2-'S7,s拓=mi~V Mo0  O_3ON+0$RLh9x6D4$ wJ,Y}yzIj2rQ'E{I (t^X#Q, fFA _#s=gPoP@|/A^R`U+P" W+$1-ggM7/eJ`PN[m&snxsO\y P5 i_$zMy)/ 5Ohsx/`-#2ѿrx~}[ T06f,E#8wcYs}?Z.8Rdq&I_ (}mT{>{"qA<[MyXD5ӎLY+cߡ[x;d~o]g _bKXZUcaLkӊV&mիSj0 f\# ō9LpZ{w֢5pJ*s2,4?jV44R`^}Lv29eN3\*N.]prp491X6a!9=ϗ_q{L9 p hB(KEZ,ZsP_#dT.]keU$" 914Z1 Tr <\{p+x,RLv;ܶ߶LZL($AQIݝ̱)YZ>N|/]7sb5ۯ{ĹTT nmx0Vߧ" Jjt*A,ET++ \|ӭ=^\,EȊnhiX pj=cQ<{v?ڗ^f2a>|x" %>Y޴_j<5 g vh`xNE;̹8'se2LaX@&ux>wu.*Mh>z,.qqTHt$Q\ "A]"yt|"FZ9.-&):`z +y"P)IL FKj\"sa4s)E}"Jz3Oj[L|@<u/N D;C +41`/J?f腴K ;4 P @ٷ OCbXb['\[}Q8~T!/N%agAmi]?u7dfD社Pd08EbI^ޓS$ZxtЏ%J*mױohĺjSGsPiZR(I—ݪ_MҮve!Ie[ˍoE'%=vvgGbXECNY[??|&5<:9]׿2<&,"L T1X7yqv6=k9wmBŒ>EB4 c)8;nrЭ7 ܰPY!t>:9,ch#hNk]b~om WqEƣqz\Iwks=%,7[$Ww+^» G ͂&M%op7 5OkbY )vO5GO|N :CD5VK&I#QBw IDATH"@}D+ h 'B5VS)6a3ˑ' zŹUO*sZ 5YKI  gEҀ~\6WPl߇(~xS+Bײ6"YG|aj}iE"fb:9z⿎= 'R>a[ٖr|A1筕%+O`ؒB jIegSKbϓwVwhT CYu'a9o"l_7'_|:oNN͛\a@YEcHyu[&TVtt>9 H]aou>x{K;9Ad^nf'U(QњEBW3fz{03c5{1Yl?ؘ}f1/bŬE/ fDjV]"o78yJ3/$yoD܈|w0noys;09ŝwTӚ{Z3]0+ v]LK8v`Sfw|#=|3k6ᲇ׈f}t+I0۴q9ΞGԶ-x!)1j6TͷuW)_x냈 f/YrGZeީ-ozMR: B=*~pcHJ !α-Y*G@@Hҹ,Q/hZR" W\yl, mjq@o. P_Tom#K9 H6E*k6)^e;]yf'M/>Irj A$ZBm]}tʺ*q.eo۶TO \ː]6M[O9J3w&/7W!9i(ӣi)zdxZ1niV78[! `mWz[<=%C|-~/g|_pz?mnuC]隸JCabaWܼ"qALjMd-OzJc^ITzv3Smɽ/ wͧ9Ztʃsշ }(zx~[ y%i,MG1.n]U:E w%˲m.B%hJPaX/ѳN,Zǁ? L%nJ}UJ; Y 1r  }Z,| qMkL+Os @ r&<>l#>=?xu+)}^%+=p3kH*4{tXsn\ |I]S-=\bu9[7,fL>7ėN9O9/~ż47gܿ'i[c} *ݳk_IZ3uxۜ&M=p V %aI׏"=jgvb7&~%v߶=ہgg-<썣]cƵTﯧ~Ux%0!rɂl *%rXQ"]]ƢB TXГ%IjApnH[%Ddu2e*X}4:էT;!$4ĘPsbEhzzWsǩ &H[Di1=_sNqEx3W8$Su0ctHoҫd,J\KDħkZ@R%HDZ2mz%R$6Kye$*V<~5[hیǁrG@*S1.o11sث`L>ls.KeS$ql, %I;ػÄ x]b~`^C=qos7i.KyO4odO]#K=tp5 $/zɗj$UЬw٣MMM6޹?o//r`ꣿGƌ/zz|̧9YXyKM;R\d3tImMrC^v(pŽ`  >q*>I3.o|zof#|OȎ9>9F:w765 &d!F#7Lm2 KBhҦ,&,.vb2F1HW H1y}fqy'/M$E1PlD .N% b@20lxrsd-@m?,|Uz¥i3eK/8pQ9789Ǧ2T&C"=|S㯔씒@K1zd#>c^9X2ۻC"_Ki O2(#JVQӎQKٷ%T*7QIbq(I]I 3׏eh*DZ=noG}'fD/an}8bm;)wY3OUY׶N 0z^HRϖ+G }lCKnF9FܘW9\J&z#<9=e/x1}<||ÏW?q)ۋQot=ppgMo`+>$^QM%}i* q}r< lKR=!v:6cf4,9|m7m6(уa\EֳưnZ~1Rc7]|͓SQ.KlABs o;pmBsd }ӍޜRՎ?mR-lp"Kg8zL5 2ٳJ=@DH<f%PcVלDG;M~+4X!.DV6-%|)L"U6qxH|A`:_ZAgɒOm76&4=l{wnXe3 CBW;BAJIp24"zւ*g'j&疛+k;H[ s;# Zg= =uueSGry "5l)+\ xs:le\"Q3)>"dCz.2 U/ؾ.{tNb=[Y־!4|J&I \|3[MY!D `†kSxihj{%|#N1k|8Okf̻Ըh[P<޺%C6mYe{= qPi_v|.خ R Ѳ7o<9uCO? lD AGLGĦeRY]`jetq8J\h)랝c}G_4v9_s|M=5c9ᄋ=Ml-vB)Mт;-Xjˁ1Ioa#i$…ghL3(8vS!q/  iך)$N%K:̔j|ePeL*{)չVu`؅*ӘT Zcev}OX8;r,>g[.dv#g EG~]U)-,Ʒ컚yGv"q5S!LI:Z_>uQt`L+2a@l6̈́θ|*+تd^ ,Rx$ ~V|)wYܺojwOa>6F΋[Cp*4=݈@K&sLkv"4a [ ݒUf_̀K=|YWVst>96tbƴmyoP3$il8#Grc1Tu`BCJz8u &I0-bƂDϮE~l XJƎz_?xm;ܸ/Etg9p%aTF 3 8B:|R [!J9l%~"L*q.6!6@\ZfE]͚()~`MQƕ$g<3&5wR!Q3-sx/&k=qЬ(}&aiQ cx "GW'{>mmMN]7< yanhlr݀:P"<33yeh[qLZӡjh$e׳R6UGQ=Ed0 .`.a%1j/y)O.*7OV/.W,ZVϮ 4mf:!pRCOHk&BtdPҹL`LVN\nKbR+5:gKb@t:3C}uqUT5RRse*.tWc;DIHezy+=e* p sw젡POg[çgIeZAq oW>ˊn1P27ΏU#g-.xMeq4lB=4KHGk&3bNs5 5_LhK!CR*NiCfwA[5T5G.-UdĨ'*V$O\v%*n] Cj7}UѬ§CUV-&[oc0;'eqo>Ɓ1D/Ҷ2=Q2$D)]$hG>DpDZc/}SaYT;_Iҡn?oRQ12aۅ}w 1=̫;XY*s{ںf^*4P6S cލmLHީ1ęc-7ٯ};6/rmD@{skQxSTE1b1:D f}Dē^d§-'7ßsU5UA(J:F)Q7€7S7rH$hCbIqaD|Vmsd5n)kNH^lԆdc.iBA*=!KqBz[!<-X}"5v@ R}}<Ua y:wJƓkCw9}F }iK IDATh{E#m ° mTa ]J^G"~>2qʸ}xJ,GU6&2oUYDŜbݒȍ Ӌˢ:R=ŋK7zIK9}G AߕЕ_r ߇{,?9^蹰gݟ_ lcvw2_{7>Źolq$c2%Xc8(շu?}p s-<_6"E$P ˃TisʧC-]!taaƵm4ޤh*FEïF !D@S'5&Ig仏bP{ ȑ;zzEtG=6vEZ:fmyv3D 茑wƎ$c2у&PZK>ғdp c &?6S'PR$-HVk ΋Sp+m+c،Up/(|0'Dr_$} Aj=Їp!w^-y!y} $S/%TC+d`޺6 (y}|#z~wƊ}djeiUFEFR"AOScx=;7<ї`oOxN>i`2kT{I_D!8%POʅSz\?f'gK0㍯mTq6ͥz\h-L XzuO\׳0S N~!uzq$VyzloVr;"~ l 38 "7`Q( 8fTu^%z$s#!D#{g.o%[1>{1[O~C8HZ"8 $ )AIk=]mfT{ creE/lYπH=Sb:rHnLpkAuܴ@5ȟw q-䱼E(ï{ uҘC?ē^~por w\+v+sDz6UD s`r JBGRXzH;sw!Aܣ!kX|ưYy\o@9&fAK֏ݐ .&Q%/;}xeGZ ;-$,^wx'2]^#iZd0.HfZ'V/+6/ܭa]q~GXy[)%^ԩV_@V1c̈́h%Ҿ.LuڀTD惸A=6+Fտ)#}'6ITD)=ZKTD%M*\(!Hdn }ϛ'ގ|9DC,#[\2!)Wsxu?:b"I!<29ܺ:k!\cki~^fq)$ND?|5Mj`x6?m9{gLvL#TtS,tA׏ܟ؍R}Q}6~ZaW [͞%)%G%9Ԍՙ0t4il;EG=M!>s 7VTz oaƍ(zvZrガre:sγmU_c8iaRv!ϸ]J7Lh-vvk)ϼ է'B̓scsnEx~LwɒVNzScq~Yg;aa=5 !zM%(\y3C}dϒmٖcW0֢#QTJ5,H L*gaQ.$gG!]\&5IC=uYm^KZ!CB*}LHQ7O6'iS ,[zˡCiTU7 GR@FJU%R%R+1۱y:PiH9jǐ*W}I\IBc`B?-7!FO<>ICHFHr)ս]~-$݄$ڢOr>h2&>)TXHJDoW_$ZLlq:O&eKO *whjOG@{,9L=C8 j|T-}ܽ=K׫^|MfV<=kx{|}i!Di SiW3*/{ظJ;n-nG|r]~0+YJ%NUQ1\"|E:PiKamjf3`ces!t;l+`)%{jgg}1ܤ/8eTT8㍡4Tï8 ɒtO75?9;-`yƃ=دD))`|&rƛwa!i؆^X擳svKg[Z0U>ixeglޔ!ňzz88@LƋxlГX L* 9 @ٴBTBf)K/7⼖ [밒;|Fl%k2D+%b(5Tay 'TCtR X7=lDo@aͤo[* ]8c9gHB˹opHf==>4=|EZr߷+$ڮFEnc *&i0n DIr&B OpN{` j?x%ur,2s{OT>7{$I, e{Vk7 32IJXcHacq`Z-ös;jrR!Ւ +ME~9G,pZh9'`}&y*ۅKoGdj7̚1F!+mwj/l;.,<_J_5(K{c.49c9Wvh0eš 8]XJCK:;<&IUP^2wC)\ T4[ۏtwnᴡ9_w {)4 Z} m[NL8%o_,C=ګbO}2@HZ!h󵐸xgG/k&޷96o{ܨ~7'V)Bs\9^?ڤ{Q怬MoߙZIJUtOb!)4 T*悐+4@y\"zD5^bMgǚ^ }T *mZ()`}br잍Q+vwZ/Ϫ?.ǵKҊLϽ<_sdەawg䏸w6{I6"=j+[kUۊTvJ^J^-Øt,X{-UV{= {̳,5IJI7'7t,(a/:v3f/4Z <=*=RܰyZ!ucJ\giUhE 9xWOpo7ꚓVk8^jmEK].c)vi+!q̀1)hg85;:p''q :\9#3DU t*V{DPz%/r1lc.g'"0K=pִ7fAi5!pbL6Ha;,y&l}m'<$A=Tsގd,٢3x<+8p-#MUbq(xW?Z\6H>Б! v%)p` ?w,]zj'v-W̏ͪ{lY~ fV)3T`~ gL'܉)Lqdf8"J;r(pcJԾOxz]߷4+]ٷtk珙ѝ-)mT*j%/MI>Մf<9c SE8= ٗq/P{= }&ۓ<݃~\[ԸMj"b cg ?=|e$djx AT;P;D}ldm:v[Bހp|71l*޳9hH2!Dϴe^w.& yd̔YK:0pbFbK]1 ]TUَVUeGuYBIb58$(;5U:_@,Ss`Mv:rA 0hlzm#Iij㠓 q ӘTR iË ixyvfou6:*hN1B40 sou_9H8BezXpvl9WNbPc׽GtPGV2ud\$mi}EXدpXE^s j(o#fA棛'`ӛpwSr;}A+tSmԞOyc;/ښIp&R-4&Llˎ5'UEl Ritʻg.mvB^9l6S8{e(YAƾ~hV-}e3Wmts;vzl```2-Uyڬ9rV2$ K/OX`R]H/!z ۙ} u ^aEWOޥA͒=HN@je@]O7l" w5Хh-|Ns;,5w͝Om)ƪ{Sۆl}bmǽ^]81c1]_^0E(L 0ssi],Oqf9rq,e~>:%pvHQ?kKi!wp"ra5[D&93+ߺ;Mk7NOO;{z}_z'|/GӺCNZ*ySlfp!͓j|-%7Y-n_HJ4֪)5=6<>^8{{)()ۮ3|8b~yR&+uZ'Kzocu=K99+nz`'Lk6g IDATЀةC9 Òh l6O]]o5sHyUV+>^8`,lũ1l{pnHيvC riO~#~5_N{Ͻ6S C(Nl;=aⶾd*%tmGU'pdk9LK=],=1J#,la^8U`$BP M!FTU(w;ʖ#!b oE">.:a8A|0ܼ3g)<"SNRNRUZÆHq!MJhEm4ha.f, Yzc,AMh2ɪƸn*X 罂iB"zOt9$mRM΀c1Cqy*%׏I '3B$3_z&3wsq,r_!m :V^)^%E{~uO<$z}!ubNpVxב͊٫ny*'f@@5D q;î'tQr0r^E .G:K@zf£O%B۱cHO8.0,39#NXV̎t|%u_הHX20pPtq r{VqS)7I3\޷YɔYBZj8j%l!ZnC\CpS;'k8,޺ b|hMߴ9jKE ~"8!kE6FrM'ī8jq61Bp5LKKSS"ma͹$"ءK+Ѓ1 p'^5YR"esD!N#0T(zd'qccGW(˥wW nצ-|:Ր%S;I+b=!^&e@> dp,5X,_?*B Ia g[>8̊x 0\K _"zSnWQOkw-K};w_穟?g'_[1$~c U>s uL9[3;Z0UuL>+JXl@ڕ2ƨTVvBܶ]"x^ǘX$DK4kU ;7Yܬ=/''0!Fͧ,M.Fm4 əEa͊_ F\?Jvp/o£o(M _ bz+M7$z[`˭H7I@RRJd0thl܁2R.̰9IS %pZj8}9+/lU'$j$%$4%p.k| DO+L"zE%A%c~!L ="(7ސn [Jru9Ax)Jx' pYv%v1aS)OI$lU!}%ɮDYb[y$qlαrL.UMV6V]Y>zHK'6=sWX)K{jP9{ITdWVi04] wpϰswcsᵱ1g^h#F;hh܌陦gjU*$2zxy"ʬꗱf3^x"yy@ 8炙qaF5LiPe׍z ~wOݽfm7Lm.g}_1\6C&$SIS2u՚ST~~yU>,&R'Q\[-OQC wgoNW:"O֠En˰vEU.=Ѷ~ a=P*TrUMt0ƖmLIF!I9wmhMqR Z1ع &9@3m_s]=8s:wOmG²^oS2N2~D~uʹw2L I v|DJSz}N]x'IYR ",ۑ68?CAPN@J֤rud>%sGAka^=bͣw'ZPYM@!iU8lM朱ucҩx( 7AdޞOAn/=Bs}pǎN)oψ h(KskNJEPfG^ `A'p +˙VGcK&/.9V>.\T9f=@&}F9–h{ U7 l)N;p4^qs)(<GýuXt'_|| FB?&U*^ {ޝMD%}ҰT)\̟1kC۪FX |%nv4K L>6@n꺸ѤR +9(}WA %+X6Mf-Adu+*;)1k z\t,)q~zYB˪d5 S{qGı1А- A'IaZ@Ƶkƫbx$'J7_gU@?b+_Q?,mP QsUظ0Ol# 'U4TJP!Ldzp!`H P .VQ?YQmE/ ]!݃rZӃ`Qt K#Z4W*l| "-/FM:XBI湴PRi'3`rV\Hqqj`.l8JJP^TY8~lZʵ1][yz/U1b~euyhۗ>{Q0ٛ/ uv 1 đ>-~tJ6]0'sDaJZ+] ƚGE'j+CR 8ӥYGyF-{~Qenp\m 2hҦ?,6Q?᥍T4!ז&S۞~ζ(\ʕY8'3 ýTߧڶZL~6Q9szO3i}g7愿@1)-w#]/q+˟;Ҫ=- H*tB M~.4b#Bf8.YY,R2.JM{R2TF$mbCM+uwzBNgL[$0r\45O"WJ0zVZbR,\O48E<;7%~y,JjWLۘ@U#4e2{SLk+PP^u*ciNɫFɔ"r]CQk(HSW!(ri(i/9pj{Jc(])W3Pvj`_1Wk9eWcq4'T.}kpe-+”u:Z^c LC~ƠՄ ˘3;r' tꠝa A?= ZvLr𻟻g+(svT1&[s=6$t"/"{k q5>ʢd3ut;hH `~0wrR2_ۡ᰸>1tSdi}jk۪bֱʃƣ\QPN)mqeJ= :HG y}OYY"}*Z(!@E69M>1ֶa9@psܜ!=VsC|4~@W< gYk]ܶjNXw@*ӽ材%ơp2z\5'UD/D>?6;BuR7Θ9Zi-ׁls3-rwlLg i^^LzLPƑ76j )yq5oԤs3,Os&:bښ.H̒YC>vRWɸԿT,5ۅ\ ~F'&v'eŨq$/R NH<6!J$ecPb-F5e*jԕmooR*~UT;߻˄ ZxF0ueЅF̠jY!H3w_y+UzM1F[kpgǞ84]y66 ]xyfd*ʣD\w]!F9aKLcv$8&u)$pNo>~;F0J io,ZGo-l);/hu c!܈F}{9&UjǍeKg2r?mtUXɲg0@"qXwz)u#zsUQ`">rC=4n4рNw ȗ.w9)S:p"sKMz=[yǷ,FbFZ[g!ngO\ {\A-C|i%mmOh#b|%ԕm=v*Ѥv{&yMfxELϜLфNU1mkTp=:!lAĩl.)!MH] 0֨ęP{הOdqTGź?bd{v/jc<͙?껕oiOoф{&9fl̓A:Ԓ~^"z7uQ'}W6w̢Kgj#uôjJ(_yBd]ؐ1]H6MepDm(Jy@*'B2XxV+roF [5ב!|SQ21LGδ! (9M’?2EI,vIx[Xvc bp+ߥwRd"1!e9+} ~Ik[E7q% nY_K|&2hZ ?ɟ5GN.z'Jgi#/S2-2AA{=ϓ8+xNml'Ɗh\NXOj̸`ɗr{BYZs|J8}wox9.ܢl*~-nH=1 O_p/2i\MH13p:vaʪ!8%z![kyƄ\,b~͗$k Qi0.w+}k:~C*c|"y6"ag{<;%ScK>Vw 'g4h3F jKK \;,Nenaճu@Zs{aNX m% 5@A-n?OoYT]__x:k`K[g~%#:d?j IDAT77Z. /,'GvD̹W"X ~wqm\5joA26 6+}p?#f69;mA4I\f]@o &\},./_|IJeU4sDͣ;O4p=#-RHwZzp㲫h2}m?U׻t> 0cq4@ ywXc&ަw?Ayu0*R]@;b@A "{P;S9ppxFJ )NsfT{t_q\M+КO ܍T:7}Vt+S|&p9V%$+E<~ym>E#@C=0^dj1:ݲkj]}u3wp} k6ͭ#3Z}?>ld]FYc-\OvM^}vMOXuݻyfհ%ڀbt\U˥el8bnh\pvј|o|aʍI_ z1Ѹ[y3dwNؙL\c"'zOBLެ.AZ⩵#6N[}{T%/$l+uY.L昢JFˌ( (rׯq )7Mx * t:k6ϑiF'Q<v:)H}Iޡ-ѳ|y2+6+W0nNƜܾV3˵tх#@ɺ1[n|&CQJ+F d4|t> ?#V2]]03r/I;6Ωpēm,DwIcp\4aٱ' hSU-sOkϓVPl$4H^|K]@Y!q@fHP6}D?' ?f%~Zkpٔ\P%+O@1;wxl9^@\6Awv:F |oz>#5I*3O 9u~~h%Sz s.ʕ蝁8/^;r s _ 1Oh&cԑXVF d2劔fw5hNKp, ;LE΄4|@h( [(I҄,ngFۺϑIFB$E). ^,x'cWi.AB4+"{\O;D-s$x6-1o2[{1m }k b%ī)v-ЪBdﻓMje0_!u!ƤSwX  _aK{;/H+"O}uVqqzˈY[|>, ^k ZnQ-ó'I_M#6{xtPll׀CO, B>9xR_{>)S1.s+>^-wߗIkG0-.o#!xs{×#u]n2 ww_rl-HҺJ뮠{A>T2:њOLX&ugTk<{eY?x&Cڲe5Tzʥ`T6 >{!*2FiVbd;}&@RL&CNQFJ2N6v8Fm홵yRA!t <9Y'!RPhJW'Yk;WP< ~z1dQhW὇j)I˫ πwNx ƮAyX& ňv 1[BN"E>?k`Ⱥ9PYOYⲍ,¨'ԇvpѨX}I\Xv? O: #UXErV-Վ'!nO|U\u _{U۷.ƪ}NR7c-kiSjj58HXw zɐfmptAd@RC  W0둈*N[XW\)xjIbSA"<v=}p =l^~W-"Ro^SUp2Rq- Pe7.OShCw=AP5Jڙ UG+ϚO"`˰r{y_Ӣݎ~aB?^F{煊<&ֳ1 8=..?Ӊ+c~۷pD?6!@(ٷ.fO g)벆 \rU^llY~w/#_xcD^nRE&wh!7o?G>yޘu,_t7G'}ۏF'8e&k.=pR8F@} ]`MՖTde $!¶oO} C]# V4B_ ,6;[ /}:QaJXLYʷGUж1/cz"\%j i$43ғdcݩ{ g/D܆Uo} ~Ҡ: d h%)%F' y0^T*8#۬S~T=595.e[8AiiKBJ᧯rve8:P ?62Xm+",h  m3~6gya`a,{x߂ޯ`;>߯č +}"{@?/+; vd1->f=Ut{]Ba3)]DX5! ˆɋ6,#N˖-YD"jުv,êuq[d_ia*$R;EٓUpP}M6²~w]P?J X>t \O {f;MhBJhW NܬReqQHDmjïdLJKTi{T$ν >Y&1//ĥ*K^. >|wD Zt1+5s ՠMk뒸R.aYpQ`-o—a>)ݻF7"him _OAlGBE' ԙ|t\BB휽ФNӊd9\].z\`sz3 J_;)+g$)ITi_jAe|HM'^P +4Qݭf\y7.+UOAIG"z ԘB[2~5ޙr[kߴ9e^+#^m*]ۏ"?t3^E1vbB%/pռ=\/x]hXGE%Y ڿKolI*VMa -H)6`mcp(i" ScqDXv5su *ʹ)d58\j[,hmaIuC5 ~ Q_L*͈PsjeAM5N|!#.'lS]/D~Y>A/oTD/VI%`w8=;)B&]h>\;PPA)Zؖ"[3fڢ8|]je/xB|[eHTkʥI7!VJ_]#"z1 #E?oҀ ~뉰wtT{nI\Iڥ'#0ѪAYh9s-Pe+u|Ɏj$SnG:C\A2u ÄLN+Z+rn.0ع4)&zxD(Gx5n3 FP>%` l^r/.Ie-#(\Jt ;]a^uAz2p UWPg!t $PJ4t.D= "%{4% Iދ$m^;nCP'-gӆv[jcK=1i!ާd(-/ѩT#08SDI~sR@i΀#ez ]suC;}x&~8eOa#}їyb&$>˪W!zc->(g)ҏ%>O9p` D~ .>hkmsueѶ1B^?o2=.<>j<3e v$sDD@"ƙ{!v'a-RyZ*W FY{ߨr^moίw첻7V\`uYno{>+Z-ĤF2t]V04"H!Wb%BEseTޏ8[\^ָ+5L3Xm +,e2U&ܴ{q zio餾+9h!Ѭ*A&`E 8T0ׅϨ9`HsD;m)ٲl)^SnNn/_֟A]('VN"0J%,x 9"9rpeg>WEiZ-S7uO]5Z}E&_->giO瓒"l` u|yK龫(RTzwD6{.7^PMBFwXÑ>b8Sl{SA(V>>&U49:<4FNY^-H7}x*W2 Gd`||_<ۚBn٭#yKQT$qLH4~e^6D)01ENc7 V?պbBjZw'Ǩ w?>n_(Sր^< EGd.9 ͙0Bp~%!$V-b0QuщquNոٳ$%|ԐVf9)tcSȪNϨ&sI\me/ D\8;s>˔l;!Ԑ%ݧpOߤ($hcEu:1h㪏(E_Mg Q-OS:Ւ>ьba0^0?ڦevB.$Ի$MP)en^tKf}bw^wwwq]we0%]Vf㍉eT;~JcH?. h4 Ήj.E,Ʋ9beD-#6-%وbqR3}n8Ei |Q>3qm!_Zw9ݟ9ݣ;Stb!9^Opf|PR,HmR%hXHohB8wNՋ` R&l2X| !oq n y=AN~^z2 Xv^8VBVqEua"1,&S*#? fXЈLU~{1s[GTuz!Ѫ ([P>-,GH/Iǔt$]ݯ9믐)ŢbP@_7H\hͪ/=]>ӄmH tʌZ,q66G33STc6\Є']ȏJ9ȃ#]bh*{m00%R)(zڒZAh|{4Q gܷ)ܴ^|jxm/N[& -JS\,y2–2udK[_XE\"U`܋U>*~ubBpYt eY?-CMHa}Z\ZW \kBs gq7Jw7jg .VҭO[nÕxoB3͸\\ᓗ3;qo6y*nL9Ut,%HI*ELYP=w/(;w&qa_N1̺~L3Rm|JO[*̴/^,)Hd1EN4'pS.1sCt_0X0dO^HR^v*.@furWU[fEI7! ox((O *ސEIɝ;w9R'Ϟ%bbٺ ^g>P 36o-FkW$QP IRw؅qۺ.#ρAM q#p1>X#)pC9>?~&P̐YMR?R IDAT-H!H4\`*6Q$D4PK(^Рl a3P6_ *4H!\yFi>TjK$(H􁫪j{@s@[)wlG/~SSJUyrԢ`&6,U pFEKo򲯖1ޭԡbnZJi-h}%0EYGY*ƙpK֦ݠL-处HzśƍUZ(1w VPbe(T2D^A߲5ʡ@h\MVl  udڐ&k^ >|Fl6g+֐uX $m&oϞY8@rb]ndJ,15BSo|Ƿ+R3|M$L hO%TIVm%.{Ի̈́@O` S>\%v٧0ܟT&~64N!Ҡҁ kt8R Ł~ +^*L1haHr"dIDn!ЕF c$(TtVPE+u@SϚM%(';*ÐG/&8Y@[O -A*'z|Kȶ&$@ o˜Sq "u_!x"*$ P "iۺכ֐`tٛ-8>pn:sOƞ6vdH$aʥ3my%Vh|x* CV/{ڤOB3M` [K&//)]P)ٹfhO<)ҥv ֠ܝIm%n2{} u{ ֛i7TbxOHG x^.KTZJW={\ZB806V2ݧ~F >s@)CFsV~tjy=I7Q$aI_6D..'JzaB s:ʓ|<2jNֿW筑 ٣m]f Iۂϵ>=do ,3WHHewRg]R7(t7jm닗 4`O e1%HJLSjIJ"8իAI|/)JI[TOrу<|O np Vt;ˎu/v vtib'ppԗ.V6e!{jW[ۨ{i<X&i~9hGt?<kɆez}煐-JD" ^ӊRɴ95_+PZWnh#MWIXL|(zVc,S-$.Ap{v˗zmW݋rf g"3L[\ n<˽z*h>#ID֢Մ' `w|ןpN!ȁr&fJ޴)ORUD箮d{\O($Ɇ׮UP*-NpwIҚ _$K̂ʄ5 )O/&tRNt!(Hj8M86ų͵> J*RM uF\kʨ֬kcmiqmD>{ɚCw7zS&@@S `ٖzRLay(} :H!|~}5 (L1:$H ҩAƑ`UBP9?Kᳯ^ 4L׮eC1 nM;ف#(IN',#uL z[ꪔ.\bRcٲvbSN?/${jD]D'>"S|[2&'V#U).^hK>v$<$uu_ꗱSCdnF J[>H(^ !F9/]d $"k}p(', xS8߽*c6;қ|mL;Usa]1ع®P<| ?yb DOrn|«hKчSf%Ze0}ޝyN0 N;`z V!* 8. 2˧iE#Fg,@,;Pu:f'68bꊊ!?G! m-g #ywk8a*Sl N{nR*K-%=aHSƏɌ#\rcWb,#t1bE؛rÒ'{ď.HV\@R!Pv"2.<,,^\n~A?5}Lj`4*O‰+Υ Ǫ"BoJ,G\?W%(PYHX K=[w.~999Op9lҒhCH?#RH,8/z,Qq )j2 z[˙pKt(yy8`kkc_pJ FK"5W& d#y-$"f8Dts'0]06xW{"ٕ}8r# 2;$UE!Ll3z]^?,6bd'bl<,c,^@ۋnD !nQEJELBDf{p~sedUjzKDq{~J] sL*<ыh.m$*Ea1pfѯ Ymb^?~1̅õzP)uoPP9,U/HSbڕt3KZLƘ敪W)3'97U>2|N)k83Dywrsbm>c_ IԹb5Wji)Љ MaIdS`)L]5\,wM]tG$}5Ӳ?s<5a h7FpxwiSk 2oۋh,9PuU\SKk 鐋'Fn *jP;jfkrfAj4:8WWxW_kE"*+V5̲PT?owVh]=h3>={7fbZ_ p挔5LJ`@+XV A˂Q/{7Epo /a=+k?>ׯ pytPa&iw/_4"|.L8/P(ԈaW$ZqV]]$ު(fem IXTQ364?,H-@qXҍ+ 7_ s]ZeaY}>m>?~(KmLǓ1$?zJi2hs3}8*4sf ,ix)XRuu ܓ)<=k".r cG@bn]p`Ue 1Kxsb-K"sSrMlbqAUftJZ{&%D?\{X#6kSb6kUh&X>{vݪ݅u{?%7/$j8ϱ+,EyUʖ :f[LPR(>#>a^-s=zX!%$;p:꛻?G [^TV]{cZmRƇsF2pp<@Y>h7X_`9rN;6}e}xdV14[^r+yDӆ*w/+Pz0#Ai@ke,cbG<9WNQS9bg]jfa}ginW8j doOogyp~hMw%xn0U*!cT(Sv%!|swOϖdhL)-"6e@[N> $%2 ےP='l#UL^}L>X*R"#RRvJ+M q &يYO>aq C!|3Nɀg(c*%*s6Z͊EH:i ȝOg,f9QH}\x4,1 #?u$i.*ufǪOdb)S.py!z WaEwTA+U2p}0~.=[4#oVeLEө'_6}o96gZvز$K<>GG}v.r,pm)y<B(j䴉^i2R]TIwfk }fjxK2|$lswB"1][dwջѝ]7=O|2ݓv(UOCs199~ԛ7Xs?G4IʳB|( JS%4/[6S,F}z&y~h*^CJ+v<:}U4V2^yOQ8I!q!됨1B%FTtb m |]w!z`HRy*-(ems>}y.% O,6w ~};>ځQ͍>_[lkJk[P0Ccx=Zҭv^ug{>.`îy b fx>iT."zW\QMM4#tXr> ɼVO_ɌNIpX\E3۳hB`BM^Y^şg,KGg]$tNQCT|grWVKEѝo34Ha O}Pg18 _13<&)g_x4r`^ tb{+M6;2QEモ`vTLwnfޠ q||bQ8I!{o !L"YN jM+L97ݩjާ ƆGo{s,W3M|XD.U&ܺD/)O=)'co"i9G@I?.E< QrED蒑h(CSsTU{jme>.?0ЗyM xOlO/IM/waTeѢbKҵ+U?<>QƚEZkAC9rW*fkY:[P# DoiPbʃS֢9ֶU.bTҮ %@ I5xQ)PYevgn-ņLn-ŕ?4=@50u9M46QOC峺ϼ4biҴ M~,πuõѷmd','x)cs(dVܫ[JX(WPkZRN|6iWMRD{M6w<x/DP]|$$FUlga x|ZǷ B_s+"~4( Άԉ(R3iébqvX0 0T dw/R)5dvލ!Il+\=vT@`UPro['72vMg݆-qHjVn||l<{0 uoD _hU}l^^0a*9u3"gR!٬V%rd>fMQ>y_[trrAAE6,aIx{w7 FjWϓ "PIe@SU /"R08uvssD)AV6>)4GZv+K8}Aa1ay c2zɂu] -gQF,.PȾz >lJ}VkUl-U;%MewB;ŕ܂s~=#U04U-f8בqï-&ܕQbpJ}/eQЄ jC,+ nisX@"gvzRQ2"Ӂ-mhFxĚM=+ʐMJ iV֦ YeA=Eqz2;$ @6xohP)=^ 5KwȍWܽ;& y'ګLU`t+G!Xo,t*m;l0FĺUʝTƘ-`.r mQL :yi' !&KyI~8}-*=s&==N3L4#Tii'RN46_1ۿwST@ ˽arq6`q\3H2H)vI_5١uRw YGʧ/[dᚍCh|f4<|ح홪̻"̊k9(o0GH' Pò:4*$Gh[?h`uzN0|24a,ŘP9dr:c6ors-MM1-*6΋sp%yQW8lDP\#:vUl(,={\J ,hqeɕӀN]Ʒdm>+;b?Uc9B.0Vvdeݕ2P`[&c :U8w,KȈ@K== gR:kC\.Q(,A>OY8%RXIcU#;xlquԵWPBQ)]OQ)RMJH|[9;xyٴT#U#]bDԚuUn𹴒L:: hR)e2WCzai| ×i>⃟}ۯsp\%TruLzØfUߺV3U)wȗ^H^E %rOs&1JoIvbDL:U}5>(e5zaTH٬ Lk& .x5TkW(g?/>+}n^1t:LVdh)xSȅm%@lCmaJʌC_?k:Q8#se@i2syRddJD)I;Q8n 5K8Ў*_ *sV:bZ /xė>Ki Z/:n%I6f B=l,УnRLf2RL9{ҙOP*laF)R뚳I!/_q=\!;h{!:=GA{H&u+̀O9;[G^ A Mltmku`*hMRzCݐo՗M¤a)~^Л #ݖ/^9d|ayG?a("|!|-?8믳UnFȵ̸ ] cVd6tbV[yez|[C#TАRi5Iȧs N渵N8?2^TJF^U:8V#ыeֺE}R=Pze ُΝܸ͛}N36~x7#c@y1384((Prإ8 dq}D !hp^먢q)7-K$0 >x~Y Y20F8c>#@|Aڲ̮i?Ǘ.r}S$RXsz;R#-etd {Iǜ<(e.s{p4M)lAP̠A"9Oqb-$ wE9ALo5s鴕n64 0({M?IFӱ2 kj0 T0YRȵɐu)nJYd ̕rkS%af[^ĕM˧(&cT>ȸqAs~8`J 1TfH$`#V̷>˸C=yDQr:qѤo]2jG.V-;'.MX躭Ra^CE[LavFaڊeɄ4C56 ɣ"r"$Rt^[/^p_rcHpWEnWLokν~^nTb:%m]KS3Yh 7bhY\AȝIN><E<^_ٜ:!rLoet|.rNs ?0+oe<ҍ:Ij!X>&ڄ Ȉ27?H_+_Ra+cOrf,U *$hois Q{^&saYybE7}1v E2mK<8ZC$]XEUj ʛ'&#:^hJ!U^BtsUi0F OatcSRc u҆6[i$ʰ֓L'=})u}΍4#VxܶQ=HOY ۹:"Iƹ}+$=+!V9Ltk*ԓie;zʞ3Εn]y44 LZ"/zJ/[=ʰN/[ `yuߛ-`>2iX^#[)/dǿ1rmkhtJk2N+o0}P=[+r[m \{{\9;WI#P A'gs:}~(ӫ-U/%AUÈPY mD/:JSoylnӱҮ(0Ww #9{?%>xJm>6GCn0FDzhyE2+l3s,34$5^) oa0ZH֘,7gwqݘ%1;Ak10=Vl;Kl Pxn9`{8BmZ:lӈ!tL @#\G#0- CsS%I_{!'ӌ?/k)XPVXuu}xMzʶ_Tbm\m&!Sd^Qj4Y,0/IX2'꧐Pݔf%&eJ Z0!V>ϯ_A>>i1AќLI,$"w~dfB$b_&rPjmc+wƟ.>8Z0i?>&? W $ik]ր]m p8.3YRMIz ƈ4TəAH+׀BFͯvށA_Ã_o~qb*ML홂ٸ 戈r FKѧqU QճG-KWYZ/.9Îib!_AM[Y.,DHr908 z~/ Wu ZK[Qi]fL%@+͚̎O]\Ӕ"^Arq4,e.`ol{]kسQ!97Կ_ب7&=}}O\O %xϏ' ybp|oW f)?Psj+qݟ}c7-SA堙{~@4MGFp[CF7maMҜ'dI5əR:-"*u~6i_G>+߽B&vI+&Ϗ+͝ɿo f `{ʍ߾5p^G/L0ӣʼkF\ei86"e"Нh"LPha ۸8UM0uk,63k1P[^#W˜^̏,@ RHtӐ:(gK kT&dyRTSo4b&Z%a )庽hx!,Fnr<%/sW@78π?(j@$%3z9z)˫;ULQs8 U8Ur݃~^ٻΝBh dkc2-MϘl*z)if6:^dlg9doIq@yаw*CZ8\c'm p㩔BCz<ɛΤ˪2F6g7Spe{|& 7-&ӨŴл/spt7x:&JPGᶪdv]{㖺gGղuNR⯧sѷ~х6YWSkA3)Y|): D:O}͞.%}&{Μ+!TNa-]e@KLI%լ!Aʲtz3/\]: Ziήγ4:]<.P/"T"jY^+ ~=G:Ecf)๪` E%^[jiX8׭yc[x~{03h>>Zpvʒ>āB5/G:(ݒ1b͏Il Uo4`lO>߾ ?JS iQL.G% KZ]̑(TѻVۻy{M]Sj #逎ʒ8Q=ܽW{FqmwAbcstƷ_`S:"zQ|d>Rp߿G M\M] 3NgD,Yi"4?+a~ B"kfDDO !W6-Yx*n[ Խ)7MpE%A0UR{cGrᶘ2xS&{ }v!rLik$^x=M"=%ܻ41S!")!u%bt0.F;tHik(!JfE؍XtkU lU65[,uyK-x@P+) <{(d IDATk8_{沈qZV@.^A\"_x"a*Ns)5k;(.J q!@f{'v1WMJ4& &J1Բ_5,5C 륤no(Ƣ=F ~ܛ,v=˒-R}p^ٻFU^{uXU;<@ 5bBE;>$ևqɯsw7%ǍF}am+Lh#vFE=̯LC 3f[˙hoxOOɣaeZ[G پsƟp@\3u21 O\KsG`.{,)0O*҇Qe3*,TK_ WI:GaXg9{a>0,ku #q}#I1"&֤yIyȆ"kiWqk)Hpy ZWʮ+|ڜ_eߖ1]d%ʔmQfi l\;܆SJʕ8'9i_qē>U_w+~^ y6)]d!_E4.,x;¼xգ=/\<(%FžskAkDRtu6(}+̫Rj4ΈJM q`m "gJx؀[5;:Mie|(tI4/,4lHv/th,Q)#\ۊzOK-3CE^]Bae-8-e5 F?THWT%hB&J}`<Ey+ HX72W*H[ $Lc _Ր ˃_|a*{_ծRlp02 M ln<`Kǿ!88,o ۛ횹y*'-A͔ hw*|62ܱ?J2MgNCK(KsƐ/>k FrP8ֹ!($[=TK$n pDbH4F@"I?Ŏ%&/0auTѰ)kq`~,?}ᄑ{+cTo -eiNm/Gx~ak*ce]Da;^.kl;BGF }#m8yZ_bT5jUkApJ:wx/&1j_P, 6a\FW#P4^փFS5gW9v xݲ^|.Q>}^BtyN?Ōj{om[}>t,Cgt^>lZvH/ iۇ `dvFhUlbf ,1dSa왌ݳ8Q:QՋ&ݛ䃞cNг6;( m%^xu"EA2bX&26q}pl{фKGR!9"kak8n?d'ܴ=\9N-fj^^8Il0n^s@s&?7[pЪuONk TVb-tU, #d=ufϻR$FSJD*HK\o3, OgiNoT3RdokBZ-Bf9]&(|&YX骐*eA9 X#DuPS-wpW;IKߚ /(.(41= \ h\_T$b1p.C- & 0qL/5bRF>("*]1`;J )?m҂2||<(Ne}U:"Dn^qܞ ^޷w93g9!s! ';%gƲGE C6KsOgB8uZqZyr>f+Jפ TQ0(B홴)$yf2RuRkL49O %g_?G>"zԨG`gZK ɢL0BME$y~5#0:UHWčrdVEݞNg5U)!Qq[}13n1ފ)u`-#c#j\qcRAN岼5Q)"q`a()bWs7.E"RL^W wVRiIAؼ⛇sH$]Bj:+N b%5##O~p*A^h [8sƵx}@ҋv"em&toAsb~"KT'FwNToà= K03\AZCF(4*XQDl렆/Q2`NV;D&ln+V?񣻇Y%u+TXK[ TUpխ!O1ą1l{?y?i챍H&30 {͠m…H@N4oJK2Y8Go/R#~}qz*۠EnUl~K!!Jĭ1p*K`D3U.灘ה_mКE0C}Ux׷y=ǽusQ(׋dOu j^\nC'UbT}AMv_xNW/IWi?|~y"l1bmp:sz;3|l`J[)lZ+xڟ@8"1(JQHE$z9Pe%`ЃOP1$6ge91fv`*PpV̶iqpo,ѷ{Ru4Hpo]c~zu6_`k#֕OP|)uPli!r:ꢺ 4EB1q{1kk ^BY.ٰY%wldM0Κ 0 ;0aAxЎ"$ njTP]S3Ҍ 9u^Aے`%I@E${) !}:CLZlܵI"4$Pwu@= {_8t`wF/Doe7nls(c(v `̝A5X p]:x;8s%w[nHwT|9\&܏X#0DC8[80 z~^( K)X 3%QJ1DO@|yDbnMLag!$0aF@{5U+\¯' X5)C2,|T5}X7g~zi$|ÿDJ2NrǥK$b: 5;Tk׃@+=qFƏsD5->(b儡m*gkd!;R2-˅([H[@gM"ڋMX1{s5D/ tT4POY *d] G1E|~D?$JxMgqRn~Q;.}pn0 ұh-ij.9n6kRԹ9ON?&My/\>7(Ȍ*V0?6Cd4f0ܨMߺqx'u!zQ_t[{n]3Q[Z^Dm]!MHND,}ާqTTh>yi'hq_dϔ ֋\]&-U0_/:%S,cs6К*]0CHcD#8L[ $P:@RvZك>sw(EAj%H<| ˞<矮hf|V{&zrmzTN@Z$H:*@ Z)t6EC&d^Qc' FW`pnދ4h uO_/V%)=;+攅.EV7ܠqVUq5I!pӡlE6:AY';=.Z(Jbq+l|hBqc2s$ũ.QߗA=?}!JnvWR̕ږ,h2<P{nMĵȝj^n+¯dh `@o'RNH&܎߳̂_(4jvO*$I1M'ʡ7Ok~ֈuQbx[fb u7yVu3I-‚1<>ZG^+` QSx%ޓ|#RDa=js:4AJdR$Hc)e93YAͷ_P'e##_?Oy=^ ?1 -(: v Jm~kO|,^f6s\afk I|yīaI'ETS5RZ4 UEb`YWN ïvﹽCQSM?MѸM)[D"mp-ybW>$#&u`ӡ,'[E^OW}eDȋIv1p`l=֐m!^C2ց>ڸs(B*F<ͩCDĒ*aj_ 0X&{{źsj\8 iR I^Iwn!?+Zˢ!sVɈ4?~pdZi{M-zٙ%+tFLCYdIJa`Ml7C?@ O NS' u[9l}Xh F&4VjÒ롹vj- HSnç.0+1*7ѩ81'x&tS)Irܗ@sΕ֔\AWw%k:q'7Vt,d]qb;f $Ӫ֒#0{:ZoaT5br\@;Bs?) K1PYp>ͳ&^1GHdBV AٻH({ FP]~%T{RkWg--Sn$YNرR.Vs}d7` tR < '@{C@Ƀ}=w?8 \dyFfEv4M13jȫu #z+]t==$/1sP[^6?JAP`ă&E̓x2=#C[@CbrH\vΔWJn 3]݌AfT& Pf\x\ec AiCGՒL~k1^ej&?2'zd5Śl*vO۲luJ5^5MLiqj&Lm* NF{S 3q{=z u:Lc[}ވۯ+V1u*e~e\(xhܔo g"刔bs`ŧ/֟v>ػT=z>VhZCW#zݗ̙s+-fjmo$J#~{ZvRJQNǤ+)@#?3مNe[VLCgyBۜR,2Rh#z I-bR~kU8}9p 6F}p)@mg@8 =?'/Wyp6ѵZt?#ʞ3+8z[giE{*J̷Yf`T>Ϭn3:"B[`Бp{,N?R)Ev9 >+8dyl}x 5;VUPaENop_>, m'l)bF>A"^5cJO2q%t?LD%0SX}1Z:݅0Neєr67rFS!g?k@Gn<I2ÑU$y 3W97*f'b pӵ@kѫ/0Jj˝m^=.LGhm ޞ0-0ZNH:7\BtN8@+r 3ȳ+fa󕫫m6j˜Q'#[E%Tcj'@Rdcc/l('3^|6} \ps\T :tU59'G QD^^dt1#X?hΘXFH":G`(/o! MU {<QDG"{5*0X{ a'"5d3/fT,d2jxd؎o4<ѽZ3aҠRϑA h&tt]7.q\0Ў4lL22?MX^9is<mU!p^-bI>2[UE =ޱl+6܀{}s~Db1ZI"M##qZN x 9L?{VVf_9 ޅ-x2F)dICF6sVV>}47v<s;B$XWЊ}F ]Mt-?ˍW<4v1La;nPi,W_INՋIhP{1+  nܘ"z'K>{gaEh&O Yދ7PN\Yp+hV{,) d/xIKѹ4 JG|GC%V&:@#t[6zqa}v4&z I6Rh~>.F)VNUh[LكFC)`K3y3(2y<3{0|L<Taa*?0h,Ƈ'{B"@eu/otce9BWtGA HD,:j̬OSjq!I ړ!(IJzQK= _OKa)ZaXf IDAT9( apA1* .pE^!9BGqɠvT~(aM zYB^vdž G*C}^ #&.HZiԷ8lq0_vM>>l< VPhvg3u9ѥ[cwG<>8++}-_{T%(Co85ԙlm#Gw ~7/xGIM/g!]F,AD3>'hgr*fLGc&γ&B~Q``g~0Z*:9 Uu]LIJrPP8uD?րSgO2`UN~\7Kl ʀHёZ#?='+9k吪 &=i/R|c] 3Qj$b+)嵿1v>3c1*>?0Q1.?aQy!ik |hep8 90 (Ż@2882%!4jQSƼNq_dV"*D;&6o $ihHhhQsZ֩Z.7^Հ1W{q @l_底 )> VWQ*ާt@8y E+yG'rŶ 5aYCxm"oYݞAsQ*x+m{qP\wKt. 蔶hKR2v|'b02ԦWTda1/-yK\Cnyݷ[ Y*0FR)@9ȔTŘbJ# >S152r?Ň"{e5E tTݢ[@WdQ@a=БfZ>,Rv;Q]4Tʥ6ƾt03Z/[~ys$/@%<9R\z [p뵩i1CR?O؍ᷜ)~5`ߒd`S;lK?~~4COgɿ<_}3Ɔϱg?ŋ8uB+r#)CO1qu^7L&A"{`;J{Ҩ0;oυYɬܮKsIs\unˉ(7ݛw1ç$ x('3or.fm:=_̅< d`#j[lX)!|sDM` w/Mn\b6r9{}MF&dB*+U4\Ub|,`Y՗ކBRF&R,n 1rThŪypBg?mS2gHi%W< mMBTgiFaMρ6lD*kI\>dةuJx4&W23_zY'Ö,9=$KmǛt9^Zc`U|eKzOup=*xd2,OxN?Óa*MZRhNV>(VaS9Ɇ_Uq.MJĿM!p&V I[<Ϥp<>v;<I)\(BGѮc'_;1}L qrjGɾ$n۞w~'Oެ?߿ ~&y)^B[ꅆn};C+_3珵1rDzƀӃ>Ϝ=/__>2\';/gܒws)efrn;/QboFܸzk!Pc+oPNyE6NdbfN2_R+b<Пv<1ګ/a/O_8"]~i+⳷-aSR婅^u15ӕ;W[b>l*̼źJjJ ) 7EY;XDK+'wqc9mrBkl(1Z!#)2㽗oًa*9I^PF)Cc, ҉V#CBAi GP|TIlA[F!zJar%lXeɌ({"x4 KT/ZG؞e7k?{}Gxq^ݐRbw' Q1 kO>Ϟ 1IٷYX2I*,C55 lO~>in—^x󭷸yS`bzG,Xf"҅F;Rʕe[v4ywu2zhI}߇PzkYz}CUL oeTT  9IId9\ӧ[!-N|qu$@8!D&dTmh3O e)|j@H^(uzMҽ*|Omѫ}󒸐)bLa!zИsA <8f &`d:x/1h@B#W iY0?s- m'8O3t#;ɏ9_WxH'skyC Ga:3R u̥|O]}BƵ8j [#p0VF^:*xp ۭKqDNΑ],#xk@5$O(T\y"qT^ ~7~OC`/?? +9REs-I*ɭ |?&Njf!wx!YܩSܺ=^IP'OΓ'`woVb\DWޞOo 8)t9\/?׾,kIK >EM@¥[c]p{P-m>L$`bT@bfF#Ise7Ym՝M=M][DLdSeg20P4,΃Е*&Ng ܛgdm T!7G$x{ݞuiZSR s~|m5or\KKj4rn6\%>{gPDeؾ5"P) rՉiLR}=SVT=gh v I3O_ˆS 3! t fSR4y^h]'jzG|AU2,6M:1zxl wȃ ^{# yp PI c{:ۄA^ 1 U6#p+(V \P@2!~]y+@ )~,kVp\ DIԻ4ۨw>Sߏߣ)chR2nUoW+|؞867rO ԋѺP DρDt J*xtQ1xÀzI J84aˤzxps?J$EgD Q^߽ҬB[rdsBt "f 9`8xT2; #icP9x$2C%j+u2d_ Sh+r@EQvM86'w C8G2 hfws ^,cyKe~buYU2A'r4-7gg9dMc2f T,~WYK_ؽ_,3+JGnD܀gr._FyprpӁ\e<}de+k"qW-ur."KK|0WW"A@§rd:ѐgd=#D/&V2+1^ h5 Wߙ)O^[9( C69 B4bp`E?rL i&hEl"2AM)BJsƵ)My^.nRAp|_~ϜqLK/C_Wη'JW~ꗞoLKI_L SɘWAhx -*|r^՘Nq$m{.JJ \E莙})^ZxKMV :Щ;! ,/}C ϥnɼM$)1uJD-Cnh=[^~yDZMCDUCx$CA$a68rczW&2Xy`9pȸ#gg>/w4(a, P 'UEk̳+s`b߀#r`v7)ψ.9s訤yB,?8ͿO]T55= Jx%Tibŗ\IcJiR6H5c1%]'|4l\HדWtA`z1 %!۷F1v LβF!5]I!D^`6t6c57엎NrL*MyP S*r YNFeK޶B$u~3%f &"EEԐG| bْ4+h ^4e֜@쥧ht${[ foɢdj'225ʮ0$2S0G¶}G^JP mGr|@3Z[>Jh j[IUؿv8.|_o>ƒHU7"sQw_c~&D#㑯`bC]Њe<%`= @ BGMP)' G(cxoEAv 8?㮎Ɂ: ĹsxgWnf$tzИpAwyNW|1Ku;BS},Ik_QHdޤ(9ޤ+vu5`dN0t%u+fN-P2Nm&Sk'Jnس&'_$m,K-TqAaPگҼ"Qj0S`Y-u(ݧ-ܰ- 2"{ kE͚&##b郵g4.]&'Km-%d9wwx';PpطS0})'R 2Uxg;tbڒ{9ιOx%m]J{ %CJ h%5tez_s&xu k67Ϯk q#epWA"G0EFU`S9D W؏q992~ʫ-Qsri Q xsmנϞO^mE$ÄvDn(㉂.8x)zlH^=E[De,&UcJ5?1_IIʭ%:{kǸ#ѻ|Lp;!J)8YQmi;&_KrŽF k%K `b> IDAT<ݾUc,EQ4)Xt"e* ycb7%g:^eN"U!mZ5zRސPv-@aPH1DaGH}=pɴK&dsUD3g:[;h,o\;cф#J0@CQq@unꚁРc%?^:Cjf#? yfg.lܭ)/uB~*[O,>R$c\Jpϋ

b҅*Nf[vuƏ+t{ Μļz0 6-`1| 6{\4pa1 ì۷ojfl<7kT<(A%܈~g.Z7)OE!s k5n ISD4Ŕ`JA_ӔHO"zI[DRRjl}{wTQ;<.b|x^z :[D®TN9><-9ЮQxP>@9QfK $},w,| 2X~(yHȱ:૊!?P92L.VH Y0g 6]$t޽xXJ @3?)]qdo2 Y~sL#D,`lʱgAw3fd&{@|ъ _nWtS2IdgG}Nu$1,yi} :s|:>6K;oZ x5syx睻N+|ԁ VfuB7MEZy`w!x=$⩤M,GO{Y #p|M]!+t'%gq?3x障3LGcW%n}Z`w.!+r'_%Z![ Gҫ|WpĐiB-b`E+$,&^4PNWn!׬pz)n\ 1juP9MڦTNR5fw#z+3^z7.![Fge@|Q/l."A6=['01 dնlY[qG9.N>OTzӏ T\R%, doj x(y1ϷЋuuq$SD4-XXնU4Dcx<!QJxJ[NS+U?L7[IL&-@D'f$xLhe&*1n[ N7ܺT\KپCS9ǀGYQ^g1= VL)63ٙ : St P)͋er 0}Pa@+@j+-j'z۲>1'F,K@ \qWuI_ uLc# ftfMnzs"]=g )%V :w-DU}i(+ZI&V1[hDs@{x9KdIHo##aY@x%VЈ%īǁEcļ27=fw3|gnFͷ<W%o1̢nǶqI\TXK}Աd a9 5t쩌͓@9}`qSr.0h42/&zuNF+`c1xQbٽVrN_#n^kx\rB^Dla;(@n^q,!NNo )s1N=Y>7\t l}sl ?v.7'; \SEH@'K}ƠWZk3K E􀺚P]5Bp(ep/NQF;%#YĜOTă d76TW{Mt-} o @E 9 .XYkHP׬+ Қ+!O2>)EaxǗgOYF8^pnJ{I5_kG D>d؝?7p>Vܢw{7K(_JX6Ax(P dVNG.P+Y;{eveщAgRY52ox^)s160ܞ0Mؖb}qw|sU3Edw&ܼO&.oۼ g83] J؅<ϱٛ'b-p%Q% ze՞7!Aq"ŹcfN#ʬ?$—̺i3J |pLA|qEK:nOh EL9OwV(pB4b5Qڄ'z5JuM]6LԦjxџD AL}liYe4z#YKm!N$Yɑ~{a-NmI3),gԀ޲g:#]77?&BOD )2pJt~I[WfPfx4enTh| *x VbJ+)-?YcP&ch^K$/Ќ&[9]RӴzNW}o,WK^rW.퐯v(p;I~p }P"1;J^~oM a_/~}8 􁅕y.?N9W_go6dEnFGQ|q-Pz" HUq]vh] ~w-r_d9kgzqߝV(䆍)ΐ= Dd5CnrDNLHumT_ Y&-y@  λVWgh1^:$HGee'((u~Mwۦkiu \tBj/m6dB"6'=39"TZ ?wŒ-o!MʮS h*sen0K,cjJvW1*^=K"TV'}-_sM79L"C96B{O;>ofDB42U+b *_x-gˈ=1=kOv,\s14̕_ҕ&26VRN_h@6د?E'9ur吪QBb~;1<¼SڇcƒY|JrߚIk/M.A&1]2BB";ԻvRzrћXWF|gxu>.MJ'7<6N ={vYlFˤuyNox 5r9Uvc޹rc>ήryOoq_y# V)O?ugN rQAcUD@ܳlzŔɉ|wInTW+bS×94T=cGf%~ĦPKJ$CFjAa\ВVEuw } qʿ 2%Z5慓09wx#L" VR-I.A9yLhAnYLZɷx*0ݻam)g mۤz9?]G6t4{}(fX &ڌ*6֡wCI 63rԈhD:ѫ6 5*#U%jW$g<D0)Mx].nV.}KWY_\v|"lgme/b<痾|۪.5to__?n?.i^lyu֞ s#c[\} ?໗[ypI |uE$KTͺb r&%*p{<) n' "ww0᠝)NSaQn1hҭd8,z*%ӚI#~z-y}/xC[rO]6N ?WZ/}>< ʗ@yֵfU&0 M*ˤ0af$=xɓ}]3 i6Wxa5𪁇 \2VIp*K"L?eYq.h!duIFk̸w@9෧Ei}m79 )}E!+@A q3}c]ke+4~LQ0@z߮^(l8?wr~:k8T?.jL.D6T_i돋} Hp{jjws*YLJ!R"ϟ♮=@Ђn!,F!Pu&1cFw lVN2T=rᙧ%fL&~_KW%Q<}o3ϧt;dML\U~׾ιՔIG^ƹ'=_{M^O;߸Ư}_D]Ѭz:N\?HY72P'&~T"Yg 0kpzU=y_b>8|dBʡ2xr71sKr[+oqU+\pLX@pd8b!IC\%VV5g+w,8|7=}_r}Tp!`$oW&@ykkr8Vӄ&^%jv5I^sO'Pn3. S\ąm7ܝ!cnfa D/KgH:ν(dZC!*AfULE!n#;d$l@@~ }7`V 9Kӓy~cX8:{=|e Z3~{|0Ett t+N <\"W!ߪlS+,IT&݈8FMDQia(CᥬG& P$ɕv*__zK̿_&O\|A](O5QMM3J*ZAkf`z&I%T_j$=?ƿ? 3q}thK^1G9X IDATQbp\ȸ`y-epa[0'8h{daP/b@Õq$ 0Z&+.gC'YzI_g9֞vO}43h[Ҳ4PxU& tO`윍qzғ|z*DoMyQs|aTD2e1Sx9VV>/p ?ȿg&]Q:kb%j`,6Jc>2{˄SS@Mj;$|EN>$Ā _-Փ9NKo{+es=I.Wn7!-:yS/եSx pߨp̵˗5ŌG\I`-fvnygsjΝLH񪟃sV BLr !pXng(ps'EoѳxIewHT <vOISm<"]Ѓ}=⭬TѸ9bu*10P%ipy.8m!wQ9Eer'o/tkD'Rwi[̒wX_=w[:9TیZ*2gt"*D[2$h$3A$ 7 E4KBN!mlsLRȂ@!Xp6^L\4eO`qW~K7UHϝ3&nd+{s eg8q.aW߫"9nb Ffh5ޑ},U '*8pѶ% R;9@6EiC W]\ 'oT4Y5eh|V>'~Cʱ wD~l:6cdһ'tB ݈S +1WG`H)y~+ۼH7zBM<+ !q*)b']9v=\hGc#V}^mHi9$hJS IPj=ao11OJHcQRr{'#|N>eԉU ja;,́&ڕg!p]4L:.Vr9/B晇c`w5y?j1X~,dPn59iN!z y6qYEMeӠ?ΙŪ|x{gD~}L!&Y 8ur} b=.ZY(6InElerW U gΘ7seD@T:VH;2 34X˱]2i6XK{7%КB̩WS[XkH9m1D?}AQk;h (29^4߲ky.VnN:3\[ߕmuo}olh'OGW⟽\!It@COgl:+]>9&QS/Q/jp,e@&9>y [!IDiF^kh[ʫٳm̵$Mڊ0iM4v4s<<;n@1RNcBV5U)([4a@J2q<kiݥ)X2WC`< KC&=OHtjsOb $-v{tkdE FM2Th5YHYCfKXDqH027 ;9Xk` qI<ceR7t 6~4da"nVP<8 z ϿTauFQ0(~+Zq $F<Et+ ª1l-cZ`eXPU؀usk[eY?87+ù<_W.Mѝ(Xd67\ǻc2ᎶrȝT9!߂@&EóqjʌkGܷ$-S"شC`9IUW'lE.{^]S`V0wL #願H){z0vR(c863߾J#H,W"ѫ܏~__x/>;W3K%g}Y&%%AqnCR2D^4+2hg+n- `PvSv,qAAa {s>A˱Z p_dD9j݆Z1jnc#TЭEX+QyT,A dU[@cJu.*I'U1+ orbH)ZQ"ћY0N?83C{TEU%f?=ނ XcyZD (  %s@ @-*{hUhk nkEwer*XaVh0>Ǡ˪' bi)|m(ra& +k4tX<:]EQ>ve +k=-;7dPdx/a7eE4R 3eKR1BhRډr9I mIvP!FbT.4}A `Tt2/Jz {lfQC3ln %у*wy(8#@Qv[޲ cd32ݩZH-ӠϢp(iE6>nMUʦgwmK rE4= !y%k-ͳ$ d3v'IbքMSA+,mpDHt*)ղmI2Oz=\+A٬@q13\L]#L< ٸMQ5 T+'&tH*`>𰔧'(i!ί5};E9o۵!P6Qs9^(]G~RbɴEHܾ Yd41b`0͑J0bm*w~z8$ZZTL x1E9{Hh Zrf@sҰ!p3#K63#8ns{vel5/(qe*U1&V]! Fkp6[`xkx™DR4tS&{ex^.y e<+=/?'ٺ}]pZOGLw7WТEӰs㰟2(V5=)tRQ-!MF)&y /JM.![+~|I%Ӆ7*mG߼Zzz{\VL`mQF:@\1"dDc3K&g`ǹ||j OϱA$t!ϊ1ND/p W8 Gycx;o^[7kpǛOZHu"&ގt[lP1͋sngRЂ2Xmj|TbՎ&i`,mf?i[U$312t9 * W`=%mn|׭*!w1R7A*!>@~7,]*y;f.X)T¶w|OH(gHKMKS[}JUzuyܫ #wHcۊBn$h=QL9񹋪]DYvdei#ϩaL*xuׅůnRН@ Z">z\ϵxf#ϥC|Fm󎇫HX >PU i٣gDT#=m R:32' _Pw/tJ3nC0)'#'ńk%!r|uU;{oaEIP&bJVqJoᆋ#k~? u=QY:1Zhٷg i|(Ie= 7$NHES~jKcr-}b>a fi>eKHgb aQ KTD6[EQ*~;Jy1/b(l5HXj-"UJ=M-[Cll@eʝ*HT-6ʗNrOC^‘qG ory ܢg [̼B("zkn;=FON7MJF";tSkyR IDAT-P{ӛ%I ε$"V(Ũڑ2]kvWWvK¨|ߚaFd{ l.b$ˡhw4I& @.DU[$>N3Zi2KչL:#r#P[h{q_ 1po*zpo`UVpdezZ"&Jh(wܗxM%# TpQC̺B]8{ablS)h XҔ/4$؜ {Ht<k?DXJ2Ex*Ay//縬i*mp$:$ǢC\nL]wW;NU .,JlHto|-!Ww MYVnWrs+ @lyK{i&o]} J& *4O;Jw QD{v ``-b#q3>g3:'*&*7ڽlXrᙯ;@"t5|Y~gHR\~@d$zQ+`?J2!w[`u?eQW 1ҭ#rMI1VUf ?{1,v* 0pSh/Vʈo2|ʀVS1M߻"$t!*̂:zvѿM^mL؛ꌄolZȃ`e=Yѫ֒Lީ2{xA۝ekWs6jմ"wQ%w_$<_śɼܲ|])ί4eqL &DQMSr2vo HtVT;?m8L INN{cS".$1r6H[R{@>&H\'ڢhp!:#`4.9浔.A%4.5/KyNW )T!1Ҧ 4M[uTN`0Z 70rjC5g뤯16q(x{ްrjUȁE}ўViWbT)T wvXts}pE.~wQC_7̋9(j\ Hsc?U?vM!ZRgR. hai'Z"=Ss5fkؚʪ+fMLwN ɬ\+suo#IIX,H D/e_D$ Fb8x65k__+CYJXSS~nG,oq!#IԃO6/a5{ =VfDU1˦ydJ Zʣ$Vh>|Y޼ÍwƸBkeD=3ɧYY^eVdA$PQ?`ww ̥5Y(>ĿWy}wΓ ^ſIEG|շ&>7{AD݃Jѫ'yo`w{ˠ?HUXYöAyq*Ii[c<=Acp RD+lUb-^楩vM4s:uvnAUK9+N>vf!^=מpO9U|V,k3~|f͸1qQ&eg=1F|'ބnk ֓Gu9xzI Q6H}{ |{SBB!PKM`dEI9k܄3)'$ RQ` %dL*eT<$^.ᜐG @O`":iIW D1"mH:IҲnLhhUpKΤƉ1,̶M+yH8랦@$aq_TeU@Fъ9^M@aJ4E}x4Re(rTf9!k$I `Fa6I&)Otc+oHB@"L6ay&X~JkLpy.#|wk돋eJߚ *tXJ~R(-9֝P'jpDpA%Ԋ:r9NUwgG)> >{u·ܭ6 =VAobAqf#Aa))7[UVsƿkAk)W,t1\.JuуIt/Iu9& Aow@y>]MG&xe m$7Fk:ʆ*DD/Sf?"t҇qx20@Z gܱ wVRN\IX³:́9vR90 $ d4yi#^8K+y3{m|qQp_#*|Hԥ[|믛D\ϛ {|PR􊜇ۆ'){P|7H[/QY<'6jj]UTf-6"sgdpu~w^'=GT;\C%}bhm ?gtdS?xʙ>:Y)7|˛<2$_t"|ꭜZe{g$do-d[91T}: !,E&=pZ*ԱH\:9Vf+W}w7(2 ce O@rS6p:' wb%_<7#pʨH⢟^> ^GVmh2~r-*{z7-5-oضG؈c46gIbCr!KuCEsltk#ߖbK7 X/׌q7 3ȈTJ +dTΜ'㈃m0@&ѧݎhwm<h'Vjz tUı9/+tP:Rw<@h\C%$%$_\>vLNOF'yOt5}2w|(5Jgq0 2X߼ R{y私n=Zˁz%)NbɫLV`*+)P]T!9tcɦFˁ q?yog/=; M^J:U)/+ E喢O%Ev-2*Q&V.Qào B!˓} zq|eV 'BĆp>R:˩AsAHfXW|3's0cp+Mn߹#)W}q&O1s0#> {p|%īް替v:[wջE͢BGl*"yK|L 6DiMDPǗj(H^RAa%&\ %fWR,CMlVN8Vѧ X-1x[Vj'(\*"$oޭ;7s2T iցM>oUsl$T|WZ?b~߱xʑ=Uv9V딄;}Ʀ6<0;PL:m=ޫbƉ߾%, LP %W5l+s <Іw/"{żp sRUx*eZYub`c-4;!Vc`OKZ>zb0U=";1H)uཌྷ`Ǻ#3:ѷzD΃S6 9Rq6-yϸYGiot^L|}K:话/~vw2;8swb[hk^nE"JYFzaģXk<{wWߚr~;YO([noc26y"JQDs;u t]F#$^&wY5tE龹Y#o8dV+mOHnײ>6!_ix6hf8 l{r)WG)'J Iko3В<~4CMEkl JLzxU|@U]=78"jBKVM#9}wCbIϿhc])hԖ}`xzέH  #De1ToN3(|=FwLƭZ"vf֒ҧ/d"k*:6B}sxbXm gqѥ>u*RR.!D{JsNڕRY\Di-k_êA#q[m`6oj8rpž 6i7T //$HcڤDJx %hkdjrFp"WKXk"8(f8rre1*:Jk !oh()TB̰bd=?xГqT",OƟ|ʇ`4UZ1 ?mNb˱ Gvn N]&x52kS=$+dD&WE4c"~fҞ*Eg//VJ|$ :8\(P*3P)acJRKtQ^w)GG ϑɞIM`|4zov}$n@pӫ< Ʉw}_ۼ̑fͳ7j:Fe˔;βᗸe`rs D >d)TdzWL+ϵoTJ3nCO{$y;iby2gFrmu[.'Wn6Hh7O>|JS`Ak~/RՆ4ML5ʕɑ̻&DҾA`mR'~iYJS.+Hb^q:wO0Ú!7=_3, h.®  `6Ep.M,rf#n)`%q)7\WTUCJP֌`n4k !8 .NL5x/d)֟:FhFӊ\dMuPLm`|HXZ\TϢ259 |\qg ׾EvOF[afbs xH3& W4o蕄{]<%!&xMhmpEeUN::SWFE#yh\fHp.sM)Ǚ1R`[~xmJxͣW(o`!Cϥ'c3Ap vѾE2pk?q+  '?M|s< {+С{ݻp>yq`dL52>gε5GOĢ@9eEdL}|s (BoWR t`vCZ,bj^rn91a|Df9ul)~sT_CjU5>;}/8[)x*xC.DBh03ՌSE@1-pMR%U (IY"7NmD۸\-@lڈz:7 ,HHؠj,>t5h+jk ?}.q-3 #?D^ʉf'mAks F"y]"n[+UTo/S>>SfO6OsǴ6<{TϸJů+˕C`E[~>n_"%##'| v 4'yQ5V |)"Z[Ӏ8  i:y p1 IDAT[>җX4?rlŕ8eަbj,n^E?ͻ Q#wuE>5KwͪHzo=.@*U-u*-Nv㨔b>\˸/i=#L/-=r`4{1) H6@&= =?.'E9ދV/6|2atdv&q$xqnݷ=~Hv5?g ,jm=Hfs.v]^FjT, j/;HSmaCJge &HcԠ!!X (!RjwTbrG(zѽFi TuOJRpHbC^Vy9? q'!<Oxܿ9>,y#Y6` %pQ$n2k`xL8[Ly{[a#)~o\,qMI`p+mFBe1&DB*C#&l" us\$CoW._6MbV<[F:Wa7`&6N8z:Ɣo|]|6'Oy]8| 6yr)NJs4'߭83dŸJ?.p`g0>`1.qi \]Kk YVAݫ GVfJ5<Ԃ1\H>M0l6=ѹXҞԳ&-*$,=&V tb:N̊Ӫ3x;%X)@Ch 9T<.d(^F2Gzځ z+#_yiYB'=Jc˦eE[ȮU$ul}jUhD++x+&`fXdT\8ݯQlE\ fnq$xKdbrkh}DT4jɥɺi $W {ay50,-q7(QNʻFTZU" *9*xb4{qҳ)`extr p|88*WOn-d G*{"'v}J ۤ[yRleJu.a\[NO_oD1jcvs"zv)͞L8y.cmvkorޚ-߽>9Ɋ=cfI {}g NM~F y?˵W \JlfugێiX@L4QhX4j8cl=_e΂s|f̃܈˄%RKu[蕘O9O96uc#{Wnr?ɻwPy2vw0+ͻ<~Cg ONN6p_I ׄ`>(C;'x'=`jܤʮq2&TqlQ^> r.=:>%-EEa~1@q2b^*Q\kX.T2y(M]B HeJad 38q3o]pu̶[DOk85x]Ѹi Mq޿<aTK=%x+w2uH ^*ϾˆDUKh*Fc1eZb%ape"/&NﳗҦ\GX.QIA1I!LyR 56V:| 8[3𖰲(-z8NC]8ꑢL5qRش$"}nޘ' c[۷q(k?~wx|x's^Gksk0!/P(T=3//~48iP`;lTKyKY%@:[YJTuol^^(U]׽D// igR!xdZN)^ˀ_%Ҵ%,xD)'d$^kIE#fe;d9kżŤu/R ~A~XNªb܉ {Q\ 0Thb$ ė &YD cl.iJfڄ]aeE[,r髌FU{8&ZkrB |#ѺD&ɼiA9$S}:ExC]" B&VYEtF('|uwg\ZĮRa)>O!7a$(zSjv]u6 2qC6u!]A!/:[{$ni;}?A2Y=A]fzp}G Ù=Zr0Zspð\\:n^ bKJ{ːӫUDpNրOFL\j,6&9 j]JM Uiˤt qW1dZq9mlE2o{/Xg TmW@̷ :0Zs4b4`P||?|_p5ߜqK7O_mSϾ}&{~nv%f\<N%;E=/=ܗy@e90A>;RxURdo3L>P14&&VL-y7a+]aM;Y'$ZWLTqki[չ9Yq5A|05? 2PUC ns\@EuC^w怌2.ExEnA!>H%ڀQ{hOgB㑇IL4-y_ɾd@Nvu /ƈu8, 4l"aC*Y: KWyKƀP(zU;RM/L + K2h\=T{8tjVY 8(د̥V(Fӊuϣܚm=N8%y 龾}f-IL](Q,?/y~||;-{]DsTc'$1W^~zDZT+&X;#͐x/R `ONnj }z,jHwmޞ\q#]r*w>(KL&>^'0;3XCg`T #~]4:$0*@XOԋQk5&4Lʯoz-#WbIXSOGBeϒ^mp|Oo9;|w R kv%{2s2_xS'G}G?]Gr0^v<,}x3_-N\Gk6:6xR#s~(IEdoV8]@vee(*gbOK7T[$QKQL&e`Lܶ2&m}o;sL58t,}`}TSчFhВf}?-/2+^'fAE u@}RqILB+y! ˒dt,F` x^EwH^e-ZJbsM2&◞Ib(U6[CQ*GR P 9=)-Z/u!5N\;X?jLB*PW閊W$zo;5t!y{C$rz'"?>h{ %KЀ6 k0=#A&y}Yn%Јa{}*(%tD N>:%~Up|t[TUV͋IK$/nwuFʱ\I']^濈 &1ɜ[ymE4+eirFFͶdu,oP) AJ- AVRw1?a?dXl}z֦t݅R wyj~dxt4+I].K|mO{|?_ ؅]dp<Χp2Rfy}2P q;XUr94sRd4s1v YAZ*=3C}J;.Qv-F7`h9ib dFص]V.) .QB}d>sqJE?<-yɁL_7eǜ>Dž*"S+k)\.,q L菗Ҳ$wa@ѨTH̀I}[:=h*8u` '* %Q\$ ULl|7v݋^vb[~ ;ZGbU.3@iMeAGPNB8⦈N鐾|.r9#|tnyY| Ia;wrD E3,o~~N>'w d>g V 2f;ワ62.QCjW49Oڈ/f2|Xm a/ E}%\XƯ9>G'ʘVb~Y@r4A1_"j)?oV̭}iO1k8,m)_9"}2Qm0X 8۴x(C{y֠@!y K;jl%Q|Q)R& JM93ZDY+)BYkl3!zJFD4U$ GJHI-D50Zp&4:{T tTQ=`|hH-_6Ngu$q~0n ט9$8/.yVv 1(`'zG%6/|w?݂2-3/v. ,π@*M$Pu"qoik,ga鬗Vj?h `Xz#/CHJD*>(yS<6(iɜO#!rLK^f6D ᷿|[~^HZR8&4J~%A{t1ܾwQ:JX"nw`6O0qތR'Y@ lT})ofH;L?7eP*H$%OO zR:~d.6rsB"zG US{Uu9JIEU/D]26vڳBYK}b`UHSs#iͳwyv|⠈*Դ2m5AO45THXۭ+ҙLa%agVAGy/:[{&/OHFQ9uJIV_m_W!@ް>m`{S~ܕ%`Gw_z KZRIj=(J3o; YR:1 3h4bxZ"X"s r5OLRFH_wB *l/)WFޑY{Su/F,xt-BڄA,;jm3ǿW_7`9Ej}̦ԔO~ ˘% K;eKNKt$H_.E:˥ķS4DDr6ӝZDѢpiu6O_BV7EXH:-*Rr qٜvl=/z%"oZ@d—H[?{f{&\*'V'^pi>Pg7ުyG+QKV˧n3Jz@&v35nI$̥6T[ &ܺ^7=w~>J!]pkrkrhmW2'&܆<)CCGf*|ʰ=rpy7\r qf! Yzqݯ`ڍXZeƢϺD ƐP9!'K޳ϥZkDӒy^ ƈj^ƞf42]ƍĒը޳DÕl3/)ޛ O *BrwSMwY+y'лWfEodT iTh aSOMא>!l!]#x G'Wp˄}u= '| )8#!elR0I:`mE SӲ\C$J%V6&̞kW/"Q'm"Ɖ>E W$t^w|'+Ƈ-"-ILU(m5\ܺ?xT,%ddYrgS5,m5(UA9cqWK=WI,FlcEJDT2VUKtJU.RR!)wVU4, 0jEA´Ғb"#p漘J&̈́8VCы $cpU<=vJ 3 * Iy(2$/BW"TJR}ן$#$G20),\!hg{|Pk,EU,^b])l%)I IXGڎOTT0:m65xIZC!E2gV@FBPόƩ)I^^?~;SAwصf IE:{fXyb%Q2c>}x_\k<6D/b6N&*eYQ@훀V`#j]Ը@5 ٔ^'m%UTH߶/Xy$WDFԶƞؠԅIK( Q0J*85`ZfyBq|Sn>0H& $Ձ $ˢsxI% Cahd."w AS+D/!pTֽc&;\5c4aG=adrviK$߻1e.DG5 _l=nRr0%tL 0,=8&DEᯧF̹E\LS )Ys 40I),p ` >9 2sdmj eLV>?llE_a~UCH_"yܼ1Χ٬ vOTBΩxydrCH~ I|*SI+$mӿ]n_..E늕NոQA!H fp ǫƑ!jJe* y7^Ye1TCaR4 [20I,],WZQ" ta%) /jh6 jG,5aB߄&x~uyN7,~?V8̓1|)7KÅO(tU1ASi\{6UB\R7T(v@Dq6LSU2p v ,j*DfZ5(x dߥߟ|VsR c$XI nmx3RU݈ڎicKvS):wA>8)%o>JKj qQjQS!JWlۤBwmdzD"BWMCe h$-w.˥i|V֧d5fTHRBڇTa^:&NP 0Rr,S rʖ-xhUq`#,G͆g zoݝpo,wFli9S,Ho9E mZO`%7F]ُwhӄ&4"q+gG3 j-7؁:;IN I4a!HJ:&}&VN*L^r]f@u;1J2xE`=U(*HU 7~b$K&dA"Nו/1CDp@V5.Yy1JpFz%ȗ ruX32)4yy>|ɔ h e%DIrN QBΨ'mrh)֞1-R)arl37ijl m/4 n fr>!gynÏ:^'?Yp,]ID4(Mn+̦ê㧝`DSư|"v|cՇTr9j 'ziYm(.}KϞmy*/cq&ھS _.MԡLbY³ ͓Ue NZЦ8@zP3g}f?7O>]ɭ}`2du005g:<@&)/y@ZaVDj٧T5(QҽHJhVPXDқv\IUKTʳM"~bbE 8h(2BŲ>{F:ByKxRKnqjș tY7T%YZ)4Bv)w&>?SyD+߸Ϭ(~)J"oq'QrXY[OǨZ|L;ec)''WaR<'L$ )p}@g ;dTZ8brW/Wk3&>72H$y :iN _bHjjK6Jmjzw-c6휱k{wERdo3콐ڢCeibuSe0ϡ4zK*Fl9Kʢ l`͟kN֧xׯPetNfg$PHG` V[8ђu4ܮZ=`{296\mXI!֤-|0nl`3 3g5邐"RI`eTD*.tHߗļ*yU5ZQMI&mپ$}MqicJurdD5in?s}%w$k˖+ <^%Xn 1\D^ߛ[ȋ (pu\R>z&ksd34\x~IJtE5qJ&gDE#myR |+ѓ"E#Quz"7o>Te_]NnCwsi-Ugߗ`٧ǔU.VcTGD1;*GSдiW`8f_=6/os6ܿ |6dB:I1L>-!bV!E>Z*sj|6 JjH⛿E?~|?$ze2>49i{|[ͯ>鳓6f4&Vf%S@W@G# eoGbbZ#^$KDfpNTQ<Fs7{cT Ap]?&fT]m˻ L' }dt|ieߓ>j;XL|4fҒP%MW2dl"6Ӥ!M~:)iy,J_٥"Ƀ|] W͈״&h;Z)ڊbex.f!\z(m Ҵ pGyJj6%WiZH_MmӶȲk"uї09/瑮#K m9RqƭuMչǎj|d{g82{ί~-Q:<α~(dl{UepFi->ÝoDz p Dעx f5O̽Ϙ8= gX ~X?"{>15XX+޾whz[|_"jg6Ke3>p/}:,#qAĬP)q~Yt-D278k@2*[#EXJa]0-џ'?F~]+*^Eߥ0GTDDL 4 %L XtBWFt6lb;Kd*^ ā ViJ] ?T~FMIA* 5|yp]% bV8 k=Nj[ҐRcϻJ١ּ>+lA]2BZbԜ،6SԆ6m`;?6pqVD Ko7JG9:T2Y+`e"z=)_g*6U Nϡf%e]$>e#do`p,@ *ye|`NPLHB5(j(J&S 9BꂵR$0eяC,bW(>l}4 $RiQ7 GMmWu|7N'lM.2*;_?Y0)> _Լy|ԮOەLyW4uU˙TH RSݐ߯(|^ }+<B_q{=Gj!4`c4o6FsqRuÐIZUͭ)Op}2 .~b4R_ox'|2ܿwk+J;g2 r6g~60^rwE( A&n Bȕ0ʼJA(׈J:]y5믤'Qi&WМKrO.F6")Xy)} i"ѓ"00&^ [\M{E&5Qi VV02o9`)B Kϊ\YV됝}* t9n^ʕ =sARa$:#(|DϚtCM?m+v6>[XC8'Ѻ[P"IjY RB5(9NɍĭT3 4XI'ٗ5|NƶC;+zF*,B|&HZ.~EttTѾN~^@)y:M!ڷtM.!ɗ/tN5xCv x-$s[˞j}*moY&gX2vY_j]lby:yS^A(B ǟeޣ, %TDj_.+}za } hXdp1{7$3nBKD-+*jr< IDAT{ab RˑݠE<#=ڛٵ^뫹Re߽+=x9}v&s)Jlz~o,5d؄lbEvMxz1)f1̱imڹon'!f2uz0e6Ar &lq0Tccs>IKK(㺹Z8ff+t6㨹g$^AVb G1& FVa/>Tv`\`ءY- b0z`hQ\ A^ qYrnK"\tyAJ} E:dPHg;oX: ;4BJMiԎj(4ALn M[1oT }x%bE=!^$ NҤr0ƣ|E^ քedZ[t}B.s^X95GvHnU%U5d3ŗIA"ye=lw/cRZMDvDLoVppr"~eB"wpsG쟈|r<3.dp}trh|VC9xG_>;}!zytœ{mzZ3b;>y&)y1}bD_l-YM5b(鿦Bz"H3S\&@@&n%KDqx0%W )Y1RzLDZBetӝYZP/dHI"|܇NtLxjYK@E&_숌hCI4i}H+ЮԮEG,}/mF#x`1@TɺHeJdi``^cL%uAV8-UшK6LΛJzPϗ*X0 ˋVfPf1>Gш#k9NfiVU"2 QrQ[(qʹ̷fƆ{f{ZҖX@H70-mbږ'$9/_T0[6r;g#d2&IYYdX6 oE]S+ԹZel'^\y'>:o` |$sQ_S:گ~C/V>ޮPURw3>R./޳>4LG3eU2HbY@\Zk=1Ɨi[-sJ6FCRFB 9OSPJf],ש5ըb=;Bpռag{| Z6BiWh5Vk\I[-kXBVW`πs s _ 8BglS FF"vi\`b_q1y0xt,*YȀss[ǃjaBg*d&|w=S"&<˿qT BceZfI+@Q7 : PH%rY1s#dlIIJ@kK'AwW ^XWK&WbYE&e3vB^zͲ#iy 7uFAY +{JKەh.hҫ79<:XVW*g[8sL[9nR&#Q[܌k<]^w7J=2uQC1(2'Ο13͹i+M[d(붲p-fB0t|ϩΦxc1TD9x{+&.+a+C{&|`٧yמo߽{?8ؗZY04\r $yqwͪ."_<{!%ߋEǰDʀfLcY9v>zPU= ZcG0aq6@$\ +')+R\4=i>X-K!O'uD/x޷eh#$lG__b}`YlV4<-I} xs`{jĿK% M oV@IJ\KD7FW/2yu&O*_ $)v_)G]IR0G׏V7j\!>'j Kf*(⿹|{GX6=鎰AƱf!vy &h /5Z+_ <ԑw] hk'^ůr%)r<='Wr/g?xBUV9{;:nhLÂ9.Xׯo*x T_א]׬^Og"5l Lr}\|L]&@67(zѯeTlj@NN>|0sL"$Vy);q'.;}vwh 4?]~|z?RʂQ\)h$\fw0}LN+D^,3~rŋr+{dw[6U/}.&`;A q@D~[9+\zK*Z"ZZ+Q,8sK>zRͲyX D/&uOX{Bj hFڃ3Zʔ560Ϧ {'"ebv%/Tꀺ9S:jʄiKEsX\mHzY@cXp~Sif AI{ DYT"ssx||yY79;v=ɗKB[86 b4q%Y%׸kB~(qByĎ+1(tvIɯڎ}f|Ѻ~%']z @+b\ *`M[RnLi6%kOo >`0B1ϧ1&5 mZm-Me0CWKx'̟Kv?I];үwT= _Kw5oޝdNVB@q_/>H:2 ##u 7$D3mS)/ևblDٌ*O4s|+ʍ(G v:wd-2n!<ϩn89f!O?_pƐwxI#i "$%/hؖ{b$֌m,'C)16 C)sH1jH>–9&{o-ܖ j-#6֗nMlׂcXо 8,qَIZC}dopz1[bI:m. 7V J#&z{@SAu$9(k,N/ ]Y)[M ^ i9P퀒$o1m}Mye2bypցTUh %>kӦ]IyhJǓLڵ4~LLAER$29 =ez` :UeoG8~ ʸ6?2YrI(Cy6U"txwo<52h;G{88ˀu. ۟܅GO hBT ^]n,9i~bfm pn.$=I\C6}1Flīr=чBMN$u.276oHF26 ^Ϧ o9KF8_F=!hͲkؕYыT8ĦVmSCX,5MI?_fa*B~N&uxzԡ-ӝ`k?-j~y;^{4sVOydsMzgxX&&ǯ_m]mMy#m{А"qäBﯤ^7hyD;~WV`_%{ F!Z ϸw;iBYX˧s@#=9Ur]y:`M ɣj4bLbkU%x4>zmV@HiT44|iJ tNR;(%ĶbZs?} رWz-|ϴ$yV0meHQyOM;'ѫHXnh+p0m22yDZo+!y K*SAR\ [CLgDnx!k"""脙.LHGh>xyYBg2LwgPHu4>m߁g)|39!^ P q<^_nH^@g6GDP &亶r 2%S}ChhmRyp<(-Y ߣ6+xo}[qct IX$bo(/!:^]ZIlF5Jf'}J%\Q9[9N㹹qJZV)[ QNBl/*l|-?|mڙH1r ;rTlk!zy2ф~#% - 8 ZIJ9w$hy.% RQ-[_zxӀY_%wA}zb(M5#oo8)ZׇZAT+ɝ4>'7m)Cӻb[5 θnyBl:%0֐z|ɛx#/L/D"*Ty=h ^ⱼdcQ#*5'0AS-fRiqy|{6 sLϑejPDճ[ttYYaR?mUDs܋\]r_@e2Or 8{B߾w;;[0ՌBNqCj{L%|A0~a$z[/-l'J ')\ eMe/襼w%649b]9], $aOcc<}X=Ͼ!~{dŖ?.$^w _ zeB5˺]XuO'k\3f`M0E!]YQ[o`31KDoPEWiN||f27YQqaCXXja#v!oL|s>D^`S?j< ,k`C,o̪\(eyyg=[|jv7^s-ΣձѴ;Q$oR!e%0 >hJs'- ~jK{n6Ȅ$yжƪnWF:$izsDH񮭘?]p-~ {^sS``4%)dMu\7AJ:)X5  D/S69{w~*D0ҷo4|$޹گ$$_}u꾨E`4Ԁ=qTk!`v}7F2&L%)uxhlǖTkrtzfJ {:f9T5ebp=o+8߅܏9[QW6W qkי1j h@_1К졯-dH5]Œ{Jq &-.aײ.#񿊟ekVoaRL.]mhPA; bQh+CّܓVG?l:eDܕ ВDкKt'y % R74oW>ÂP70s$^Y<ٽ=dz2KT_+$nE A7蠨oY8M׽db9:8{z{q؟GU.],O?{fɉ?> GϠv}\>9_<%u͛wO췋[a[L?>JsqvưI T5yVn^e|syWRUȞzce,?PI*h_LD/!HU\v!nn%]"cVI]z#[?'b͈*<m>|ZBg%Q8׷Y#gwP{PX=3Ϣ5fe6UZ5&H8e_09I$p Wȝ$-E6AyR\#eP5Ojܚ\pVuN`nɛ:yzB'DeX%5b/q=شZQñm3.|{q M1<}`g3>.y+i8Wplٛ5آlk^% tY_4rLcjdhk?wgC^5zNj ۅy~1tȜ\GZ,|Jo{c[iO>ωA9RrwC]I^Ǭ[dt͹TC19gt%Kgeԭ`@b7Lo?fYcxWr?2|ޑ<<e8vK.3yQMgX9~3IΉwym2:1JL(r)( ā5tgPO/KB,.o4N\D.yi 1Ѧd,лltA$czITJ.rDճPŬ]@#.Hbs,m~²"W~٣%ut~ ׃}# .UM4::ʵsw|Hy;F'c U07m<~hŽ}-D=2U_@e_DɶX̖xFks9Uu BsO/%x̫@X{ZPTVkjB|C]-a݃Un'y }sԿloА+f~CV/#hڟl'`,}6dU,'E>PKJaJs7m"z~7oq3a<;[0/VJ.Tw?@ʤ)0x㴲.* -52hI>g؊_9/Kʅv[B:Eh7߰6ضQ,e$_||I aKh;=ӪyVUj䅁oh;H1FMPή.aU.5.̳Gm;摉amjV=%L^^]ZWԳl*TB I33G[!ߥ lpGu(%,\Nxfb N%3"يZQmceC>v+[]wTsS;gj&فe\kvFz8`.)g1BT;wH HJuXNkχ? M+0N"ٵJU”%պ&[}=!-W љA<~~OРMZ>:Vh<'_]Do`3/j|w#L'>e`.w/"I?z|oВ;&xN|4NhZ9]t7&tzKJ`3'$UD@Ioa[r[vkΚtEDk LЪ,σQxu qRQC'#XHp=^oR1&ߕu%i^x0xrQ:\wixAq6?n߮ ^"0ImJAN"JDc[jPϹ&\1ǖ$KJ— [$Ҍ¨7nv ]]oyC볘ge/9*@/ rR$2Vz)or="vkZV0}G%  Br꠱I=p !.%% i>lZ b4< ?ڳG[~mėu޵R97%cM۪PK7o6OMHJuPAwcfMCʱj9I ٟ <\t6 7I{1jʰ?nr]s0G9_~ ̚;wXՒrT;uѺTj<pܖlÜ-̗ Gc ؒ{CyI.辑u7bMBrN ы[DuUeU/aTҪj#- ,o+qF09.WzВ>ќ=#O5|!7ƣ-̷ O~q27O?}8['\Lgx6e=o~s+f.]B77!-)݃2OfGJ'=HCn+Wh䅷b~_kؑHcXz̏"{끃՘OB޲9v1]nWj|%{~R>;ra ׈Y7K븕# $3j&X7tW`\OkoT:axN[ wwɑ8cB|fXvV,Cg'69UQ( ),"IyzA:!´^OIJ5[Ŵ%~|.iN歩4v~F]vvh|߻w {?$ů+A6:s}F ߻wq)(ci](;;[07: s }oQ&*9Vރ "E8#^+jx-1U/$z[/*IX4lD@ˠFrgu_Q:]u*r`R=|6OޘO?cEoCn;{'HJԌ珡dzl Q/%V2{y=l'3ԗm:w%副q$85[Iy2'=:7??FwdDvoLO33Yv$Ӱ%, |1c4%GSh*4𧧇ؚ,ۓI$~R:!Y}KD/-I[i2Z˪>IX%dKLk,_|qXyKܜDPlbti$z|pIū_>7߽_ @V?* Q^^x`?PMD ѴF"1[e"s2Q:+u= gyyV$߿&Sd{9u^ !aǰ\Tj-Ƌ2~V/h|UnvSG?o;з䘩f9jMFڟ|V}<y-2{~;i!ur v@kjgoJhN ^mDar^c&RFDCezJ# kį6qmp"0hQA|+ߥm@ȝTSU3ij_d^uI}~3'g8 D_ dIѨMiOLipyعagj{SZ\]fF?m- F%"c5.7ٲN;/xC*uXvt'.aUN_2L=n.i=M,dY!A63 9vhX+Ho[j޼Uϐ4eW\؋"yzEibFpC{`hxB)r/j֋6+jSwWuWWG2+5|ɱ`Yν딨paj*foLE(Ӓ*4[X a @FR>zБG^7&n*ܫٵQO[Œ=L8|rX1O-jaSY<:XHbt9T48k IDATZ&cxzλ<ݖ=gGFOU@e0onjX?5! 8)+j1y03i90^L\gtm=, 48ɐ:#wUsoa8:G܀f/DjМ#|at [b&yPЏGY,qM 4,)9푊=D~q'ERg cr?^4o Cޭܹ,2IX,sAx~Bolr'ʱe.IF_j%+ݢ;#7/U'?||FF1}TIf7ps}z8;PX%Hs͵ӛgy8=w"Ptwh+{k?$5A-zK7M } j U@LF\M2U ,m>_: H)-Y(kX6ԦRW?-bDg^}LMoXLO'ԋ`~xמ}tb\r6$/ j !sNfJpN}qȄt5$MtFyC&?w $L59ZcjtMAj˻M =%!) /`0:D'pF}+-\n>^9,V.<q]S|^3fWDJ%T4Β~hR-qׂ ijt37.xlRf>iJDPL<򅒣=|ôJ+[UcoJd~$);ڜu'V]u1Y (!J5řɩzf_yfVƀs/:+?wpE$ ' bgZ9"K UXvC d'DR@;hOؒ4< u2 ̿ {yd t\1"| 4RW&6BMsT0#2~=Dk `@@TFܲy S3:"sv<2h 1y V7CAǫ-ô8`\=N yNs ]y4*{jɭD&`fw41u"[PBHr\t),(8"({iїZbAo}8~M!tDe1jt+bq,D:[#bQOq d 3|,TM$T:cpoj_YCM>˖5= 2We˘ܰ>[m˻tå-CF7mP3 &Y˽ՀzYyY76Bi@j2P%餞!S]SIߴ<=K5,6vBoOx*?@26AFx-G$Q.0B*$b ?)[S%ƦfQ=.=B]xVZJmAԒ8X &,O ^AJLh2C^}~ =]VqtZRkR}5 ԉND<_u(z5v|Veٰ5Q$1oFXyfDGA"e%Bu r=?y=ۖ!Zau[9DT۪rm{|]b/N-}\* Y\ZV}gQ)eJh˘~m@uM!\Rg+B1,dD}HgG8q- P ]YHv5_nbwi#7S>}0zpO8*c\SiK@ѡ HԶI Z*IIBe(m_L,_\K/\ˏo6lYWc 58FT 3#mєeKF2Buw4S|wM:/1~AN?Dž;Uw4LX5evR#ƣ[7ʮ*=\S}E%y sgˋ:'axZ? H06A&مaDpl="l( (@ |SR>3V'B phΣ$޸-/=%ܻyٮx$.z΄Ā1\ 5O;Q\^bj7զخ#K;4̫_-ZLק & 3͎2)KL ;}eO[ϓ/ݖ+Tቈ 4f2TԙXpʉZ 8گ.:[F~+JSF| 'ΦԶf8JJ}k-~"56!n뎡3ISM47P7ޜUVQ A<}|u$ 90.ٰ,q Jܽ.ݱg.gIkn'Ib4'QnSG =g/~nl"b .y@Z완T} ;iF[Z4Jŭ=>W6xK$c\GRf_vF+wcמZ *zܟ}?][6զU;%LV#EVKR}< l[]rpgVvEoTM^b+W`ll!&-;+JwN0#D rjTeV\Ίͬ{DV$p_?Lq|'PPB- [TB j;{]."\qTxظxBp!|mS~D!4#†]HӆH@i*KV:0dHtX)Hg@4;FnOocjT9ygB<\p9V=_[c_ِFpThA_Q: sb@%}%Ф~v| "3޴ylx{ڡeE1'ce#8h)ED2QǻW!^̈́^zt-\RA&7܂Ap2uӘVny>Cٌzy!V"Ťm:j( 7 K;;E.s?BԴ)$If;I/$яsE"WȚh#&-3nsfe f!JP/|>N#iEu }P" uL\&74h,mH$+G/'gvseڑ[YR6t0"j>lw3]jg-Wo݆*ÒHFοb_B/wOt >#V:&ޗ,_,8c5TDHBE?5&HY7tt2"Գo0JJHuqoOn\[a~H-锑,:.aw6?:#.^̊Gݹ~-CCe&7TajKi5i[j y>k^j HXbՀʀ!H]BͳD.A!rsۼԔ{ݮ[_tǺ"0&g>1 [E&[-j!'hfw:+@P%3JM䑥D$sW[7TS!b`G3~*1(ܪJ xY!`)%J^cI=N'iƜ2QChuΦ-h^T}pJ֩nTC_6Ht!'8R?Rf뽛X:y18ᓖ2P)d.m4B/o3 X@:<8}1QDh0V&59~EWpcD\RϏB 8 *^OD\թp]^ W+ 2iظ]s\"m{"B/s=gJ:o|>W!enHPYH7{370j>gI/'H#e1)li0uާ`\$HE^u4z!;A%"Q=C*!/Ab|"`_^ Qy,!,?9#O\RZ8D%p'lQ*2@ u=;`J8q!64NY~\ĝ |-.qM2^[sHp3PV<(}|/mbs=g@Yd6ADEk=h'`#kd/c+5qT`MHaUKqL/RˢLMPB?#rsg x>N9. Dكp=tP7tdy7{8\[Jpxv`dž߀뺓&Ⱶ:X$HC?@pGⱙZJNjU|EW"UTP4ɩ:pU?Wow;TTs|&&Q*9AV;\/`iZJuׇߞ gHiAk3.|T=\#1]y-646gmRpti5.KRп-n]4 w_rhQ1 ]Lyہ2~As Ժߎn(kqD*C =e./i8>4^BSU,JR߱N>tCwTy^b"'< Hj;L/@YFؽa1.Q"K& B8Z #Ew}~֫yϼ$ Ȋ=o&qbT,R2,]~^RW/[jhDzd>ZRT=uPŜ 'U"not[8AJ%؊yZKҗ9@%(mU96`R&v{;TDoεDz@ ZRe獕cr|ulJw,\Yˌ(-\_=SH&S x|ŗvK,引/%Tw~107$!UiX_r. ha0=fH7{~*URܻ. fz.Ku㠄0$W)^uss z;nC&5).iX?97̽6 ZdapK)Ȇݢ0+KԖ!Id i+ޢ,ye0J3NtZ}2@O1qع6%f37!F?ԚQ*ɯ_}vTi"X=t8^I i {u,^Λ alw|;ͷmc2M 'A%lNf.\IQgAv41@a1q|| n)gaU imtP6m z4wF*EZMs`QKozꘙLpi >FGNgmbP ܲ n{{h Y5J9M1MeoVE-$ZFU~dSZJR  0t3zϡ+u.bK@.Ϡ\Mf+N*H3-rש #wڴ6@fm|ԅt/JŎ7T5H͝3tAIh)(xia3P{xQCR4eZ.b 1dBI@CAs<)eepFͬ!6E2E+œnX .RCf~U}/bHG&idG'1 ZбȽ$Lbh&U\ثAga j9+'׈3HxŅ+X~o YQ%pٸh6s Q-J1ފT*b eq~̙8<3&3%gɋ+#L<7x/%UPR#߰^UOϽ<^[Qݧa(qgpo}#bZbq R(!ǫBChPmy"%\sWͧ0|d{$il^,Qd5fZ#\9dώ =޲JZ1L ^w /su?ۉX| ,;չA҆*Cm<.'!BօӖsFK  IpLM8' ΂s2rsRTNV+D0,LNxR ps1t{m |PS=?24D78ğ ^ / ?pqi1;}a}6M0dBO,V[M&1TV6)qW?acb|e:~Y@HܠDL6gDW=j NQ /8J.&v\tg!Ϫ^g^G2Q-mFuHwfJ>rZBs3S*3UYVN'g[  םZPJ|#k`y%xFp98CEB)v Π(iD*d41)%Qz$/5$޼Rj,uz|y,(qב xU-fLW+p EEN>1=F3Hbu|Gjn d6n|?#[t.TRp!Ҹ(ɉ~j'p3%n>pR,ǰvh*,Jhʩd5,`ܧF1rʪXLjSe$5p65U=-{D2-=[1AQԊzuY# "6=\f%J^q],9GxMS=BaYdwl~ZÐ(sH!rLp3,F%\fHSQD71b5GP&AqA몖i,Xn jxGRЈyܹsJ <# 6%*ncQBd[ +ew$pڔ, UP7°PsWx[{w6üdgdsk罻P,?mgP4 VqcU%.xx.k^^siė(& J BYKw<)p|9+ L@/6%_k-g'w)B]\yd /Ƹܭ\'-h{ݒ-o^~>ISuK(;3v;%v"TH&[{ n  D;#"A`NK2w7.bz9w+!/DX#< 6ղJJ-$XQYTRU&,L&f%-L\zݚ{"#x4dTIv1nd7ȰzȬgIOiy*h>ajzf1j(jm" ~@&xچQIA!3-\ѓ{p=tGKJ@Ej ?Y7!SnjkWTT>¥#'TYCiƾ4ˇkGtzZ\#(8SLoMkܶݟ!|yr~|w\_LN_-hC))U+WB 9$DQBŋw܂Yjz.\^zb?D-( aOUX(R ~=bX,gUv2puqw/g݃y_-bX, VY,bXgX,rcŞbX,a{bX,1VY,bXgX,rcŞbX,a{bX,1VY,bXgX,rcŞbX,a{bX,1VY,bXgX,rcŞbX,a{bX,1VY,bT.on;wEnXbX,*RfgvwX,b94b/wWbX,bNɒ3!`,zN%}W_ [,bFqқ?>y.߿ཬtWl 2%9?`$?Xw??^u;{,^'r#sg*N*g&7o㽟8.[p@G]nܾlo>U䍈ni/ٍkၯ>n=>~Õ<}I61"ΔR[y!W:g{?4^>鈣/^WkWu}oX,*H'|N郞ʧ?})-`UIsD՞Kf[kNY/=퟼x|_Gnc^_r|}_bX8{ons uQlrm|쾶Oo >zSYtď{GY; ~A=:PʋyIzу1 2|2q[;d\|^xI}.rx\wOzLj w~} ~{c{ڿ6͉IfZ4j<}]+{}oX,kv;U/<_wǝK>>xǷn? y_=:jpq&1d73lTp=aȧ_ǻz+S_[oڻy'B\9W~v>`<5=( /gbX,]<;yťvpϷ6V__{yJ/_]J>!q#Dnx]1bN8RZ2$(OH!_e_^~1qj_1n:ўqa~xy7^§KtSm OgbX,_m~H {6e@qQ t=Xt&/?W

/H=֭>\%>q 7<5s̑oS{?q>t]|klߜ3ܸ}-_|08bk~Ǯy3ڷC?/+*0>+G  ,o^FŁy龻OQ-?⥷O]lX,C \#}mTXE] /Um7_>gv2=i'|E\=bW7z_\g۞.xd==Yy].w} o{.8>vmbX,cwƿS7l%HfҺJ4~='s/0h\ܳ>vԣ#[c 17^޶m{4:8z{~Ưٻ؃45/"Oџ]VY,bXv#1(e/=9DdGtڙGkh*ܦ$WvI]Jۏ#z@k6\E6.ћ=='X,bbylQBw1o"z8(%;)yNdH$Q}EGuJ2#x '%՗?I-bX=knNg/z:΃z(>O:'cJi"W

à X/Ά$c|_<0;ikU\qn" <}?Kyl'kǔu>"^E?Fņ3m'-bX!ebX,,<\wu\݃Yzu`wƵX,bnbbX,0Ɗ=bX,+,bXcسX,b9bbX,0栋=35ܸۼp\1!̫>ǧnXS͍g7ᆵg2 IDAT_1[,bX2N7~k?G+yߜNEW|+^mG?0xճu}߃cpg/{(\t6bX, W%EO]ӻ|W3н_.>]ΗoDR>Mox.O,nkx7;ߩ7ClDG bX,e\G/o64_{9~'NUi[UnQqZyh>nՔ56nhFpo ֥wOQHXow2j/sR⃺(z6f<6}[z{ٳH7glSՕG[>%6biRo{L]{fgtű:v>cWooǭ|Nm:ucW*A̘%z_9Yevvow8GNӻo;ߪo}E#R$ɥ[Fӛ3}|ORNx_iXw5yNYONs^9S(yS u >^3~(IT5giWyu聩iղk?:͹zosOim_@H47`M=k뛂$?c ?zPbcI;ήޮ^ۗW{Tw]qEQ˫#5ҕ-Fp]ӡ:k(s#vg_n[~U|aj]6DInIrcDUrtpGZӧȎ1aij״˹+ޥ-;gٺSVȔW/T{TκC?-ۑ=ӦjLo~=ڻul߫*Sy?oۭoϯ12ի+[wk4WZ_g/g<|,[Y" TwѱzvԮuFsUE 0@ R&+vv Zؾ\J9~&{JsȌQÏz7I5 gWo_rnal"llku gӖ]ߏz7Rwߗ5|f&m͎կ6 +t-3:+LUn6ۯ}_o]O9 WU"Qj}/Hs۩WsTUuny;?8~~̏UG˴wBuGq~v;Mٹ,|W,*0A {R~0_>\5QQM?ʓKS}YP3+G0;FMZۦZk]$7.z{c}qhߞOh+oeƓVA]g%/5Qm_nUnWǯ՗ջ!K]=}Z5K{zתWTԿK uP3yۃH|*:16g~M |f_1A {fqrrc+a^syc;-UެLz=tŸ8\8GlJ.ѝQO]rD}Zqu'KcW[_gk 8BFHY:nZ.ŝ?J=Q|6wi :DuvIFqbF{ikGhkc;tUɖK.uv֦-Mh7vcǮ>8 5A {ۦ쒒izqQ־\\=]]oÛ0 }ɵoʵzTPOΞ֗7+r-ZJZdvZz+\ݮ~XmJ^xTo5ԷJW$tFh_ꂱCw5:if$IW/1O9v%I-QeU}P;||$z]:~K;uC cqfrd?G͏-zMlI JP߫*aݖyhOi@w޹[ge?vcEyfIҼy󔞞nm$͜9SfͲ6K NW7_=j7Ez8(AO|2LziD؃yU[QޠBp0`=#8a{Fp0`=#8a<]iZ5 k@be{Fp0`=#8axF#<8 +{Fp0`܍H6V`=#8a{Fp0'hæimjaXN=#8a{Fp0Gݍms7kY+ tXp0`=#8a{Fp0`=#8a<ֆ3YjMrjYmmj#B+{Fp0`i+{Fp0`=#8aP`P_ +{Fp0`=#8a{fH28(?/,I7oӭ͒3gj֬YfI8a<ֆԩ{" k1޲|)65uG5<#I^y&2g]h\p*l3z+zv^ct4E=SOzf(_b$]62nbYTVkizꩧA=ԇ|}c+:a໬=^h]ƝMWn/AY{zꩧނzR+{.RO=So頞zCX3=%}kYI[=4o{6Oȳ4{`=#8a{ܯ^$OOMjRLmDfhI W?nIf.5{}#QN^+• @ƿO.Lߩ{o_s9)'IfLۋMI|5G8 rKhhoG7ȔȮ4\=+^QF siLFTMHu߼V}ں.Cuj}zw%:Iu0*)R^kºC"{tji$ɭFM@Kl0tްAJp_}V婺IEz]+DŽ+%YaJ xϷc `9~$ ~g+,Wnf۔r?Rǜk pJOؓ25 S;З;=IWrS;Q^tήTdk਑p^>Xfr-7\a>$M/Qɢ*IuՕqƯ_7+oݗgm$u+7.ԩݪjS@jID(ڡhHZoS]);4$lXzv?#N'LPW~Ne]5d8MB>dhq'=N8)읜\<6[ݛWPHR2 VJR\P|_zN *6`(sWbnULmX]yՒ7XM믢/>Cu u:g'6n"ξH?:\j?+6𣏕GB}o*By}y7BMJeT3UWu*O'za1JLWCԵfV&HE*uEJUK`7g$E%_]ǟ eZ@>oߩNLTDjjr(NtҰG}fKI8vžҬZYvleȬQEC44L;~U_I2 ZY^n^ ?)c[U\uW{t~W;Ԅۊߛuժ3Tm^RziPJe=lPϐYSva?=uyU*k:)s ӣ{1ͫݟehoWR6f=iS M&TɕϺjUչiKv_(_`; aOVˀ蚦UnbŬ5˴OVmřCS8EGx$-yTuy*}u:UeopR>mYKkP!0|j˚ulCŐ咼v^jlղJ]57@m.jMurD# Rm*Q#2J(}}[.o" I3NFD"$Uo4򸽪u/@pwrz1TT_*k媋O5%*.i]$4ґܳV TQk*"u.ih4lFtRQW*i<qQٿUrjuD˔1$էSkJ)БROXTgx[S+y(=@U$QUJshNV Wm~|ۋUnUTh7ݟfQSϥpInEF8 j!.Pն5ڒ/E).&Kzmc wKFX^? 1Z*=Z'wL7 <;٩JSF+V6ۗduPTTnEEuPnOW;bMH8FG*c[So(-/R:)@NKQDCidDiddo%uSBPӑ܂Vvɬ5FFʐdSv~z h\O8?~o~m<&M)ъI*@qԶWS}G|'%sz_>ǟ>mZ+{D;b=b.Rcfv}a61X1?in ^hm׈їGa>UTnQpJ ۭ/Cq'iy ){~Ro>@|}>S>GG)W,}.gsjK:2 , cUޮlu?\M)WMXY]µYcu3,-WdixUqd{$ |Yhead = NULL; return cache; } void addToCache(Cache *cache, const char *filePath) { CacheNode *newNode = (CacheNode *)malloc(sizeof(CacheNode)); newNode->filePath = strdup(filePath); newNode->next = cache->head; cache->head = newNode; } void deleteCache(Cache *cache) { if (cache) { CacheNode *current = cache->head; while (current != NULL) { CacheNode *temp = current; current = current->next; free(temp->filePath); free(temp); } free(cache); } } bool existsInCache(Cache *cache, char *filePath) { CacheNode *current = cache->head; while (current != NULL) { if (strcmp(filePath, current->filePath) == 0) { return true; } current = current->next; } return false; } kew-2.4.2/src/cache.h000066400000000000000000000006711457005474400143260ustar00rootroot00000000000000#ifndef CACHE_H #define CACHE_H #include #include #include #include typedef struct CacheNode { char *filePath; struct CacheNode *next; } CacheNode; typedef struct Cache { CacheNode *head; } Cache; Cache *createCache(void); void addToCache(Cache *cache, const char *filePath); void deleteCache(Cache *cache); bool existsInCache(Cache *cache, char *filePath); #endif kew-2.4.2/src/chafafunc.c000066400000000000000000000373271457005474400152040ustar00rootroot00000000000000#include "chafafunc.h" /* chafafunc.c Functions related to printing images to the terminal with chafa. */ /* Include after chafa.h for G_OS_WIN32 */ #ifdef G_OS_WIN32 #ifdef HAVE_WINDOWS_H #include #endif #include #else #include /* ioctl */ #endif typedef struct { gint width_cells, height_cells; gint width_pixels, height_pixels; } TermSize; static void detect_terminal(ChafaTermInfo **term_info_out, ChafaCanvasMode *mode_out, ChafaPixelMode *pixel_mode_out) { ChafaCanvasMode mode; ChafaPixelMode pixel_mode; ChafaTermInfo *term_info; gchar **envp; /* Examine the environment variables and guess what the terminal can do */ envp = g_get_environ(); term_info = chafa_term_db_detect(chafa_term_db_get_default(), envp); /* See which control sequences were defined, and use that to pick the most * high-quality rendering possible */ if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_BEGIN_KITTY_IMMEDIATE_IMAGE_V1)) { pixel_mode = CHAFA_PIXEL_MODE_KITTY; mode = CHAFA_CANVAS_MODE_TRUECOLOR; } else if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_BEGIN_SIXELS)) { pixel_mode = CHAFA_PIXEL_MODE_SIXELS; mode = CHAFA_CANVAS_MODE_TRUECOLOR; } else { pixel_mode = CHAFA_PIXEL_MODE_SYMBOLS; if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FGBG_DIRECT) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FG_DIRECT) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_BG_DIRECT)) mode = CHAFA_CANVAS_MODE_TRUECOLOR; else if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FGBG_256) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FG_256) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_BG_256)) mode = CHAFA_CANVAS_MODE_INDEXED_240; else if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FGBG_16) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_FG_16) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_SET_COLOR_BG_16)) mode = CHAFA_CANVAS_MODE_INDEXED_16; else if (chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_INVERT_COLORS) && chafa_term_info_have_seq(term_info, CHAFA_TERM_SEQ_RESET_ATTRIBUTES)) mode = CHAFA_CANVAS_MODE_FGBG_BGFG; else mode = CHAFA_CANVAS_MODE_FGBG; } /* Hand over the information to caller */ *term_info_out = term_info; *mode_out = mode; *pixel_mode_out = pixel_mode; /* Cleanup */ g_strfreev(envp); } static void get_tty_size(TermSize *term_size_out) { TermSize term_size; term_size.width_cells = term_size.height_cells = term_size.width_pixels = term_size.height_pixels = -1; #ifdef G_OS_WIN32 { HANDLE chd = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFO csb_info; if (chd != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(chd, &csb_info)) { term_size.width_cells = csb_info.srWindow.Right - csb_info.srWindow.Left + 1; term_size.height_cells = csb_info.srWindow.Bottom - csb_info.srWindow.Top + 1; } } #else { struct winsize w; gboolean have_winsz = FALSE; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) >= 0 || ioctl(STDERR_FILENO, TIOCGWINSZ, &w) >= 0 || ioctl(STDIN_FILENO, TIOCGWINSZ, &w) >= 0) have_winsz = TRUE; if (have_winsz) { term_size.width_cells = w.ws_col; term_size.height_cells = w.ws_row; term_size.width_pixels = w.ws_xpixel; term_size.height_pixels = w.ws_ypixel; } } #endif if (term_size.width_cells <= 0) term_size.width_cells = -1; if (term_size.height_cells <= 2) term_size.height_cells = -1; /* If .ws_xpixel and .ws_ypixel are filled out, we can calculate * aspect information for the font used. Sixel-capable terminals * like mlterm set these fields, but most others do not. */ if (term_size.width_pixels <= 0 || term_size.height_pixels <= 0) { term_size.width_pixels = -1; term_size.height_pixels = -1; } *term_size_out = term_size; } static void tty_init(void) { #ifdef G_OS_WIN32 { HANDLE chd = GetStdHandle(STD_OUTPUT_HANDLE); saved_console_output_cp = GetConsoleOutputCP(); saved_console_input_cp = GetConsoleCP(); /* Enable ANSI escape sequence parsing etc. on MS Windows command prompt */ if (chd != INVALID_HANDLE_VALUE) { if (!SetConsoleMode(chd, ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) win32_stdout_is_file = TRUE; } /* Set UTF-8 code page I/O */ SetConsoleOutputCP(65001); SetConsoleCP(65001); } #endif } static GString * convert_image(const void *pixels, gint pix_width, gint pix_height, gint pix_rowstride, ChafaPixelType pixel_type, gint width_cells, gint height_cells, gint cell_width, gint cell_height) { ChafaTermInfo *term_info; ChafaCanvasMode mode; ChafaPixelMode pixel_mode; ChafaSymbolMap *symbol_map; ChafaCanvasConfig *config; ChafaCanvas *canvas; GString *printable; detect_terminal(&term_info, &mode, &pixel_mode); /* Specify the symbols we want */ symbol_map = chafa_symbol_map_new(); chafa_symbol_map_add_by_tags(symbol_map, CHAFA_SYMBOL_TAG_BLOCK); /* Set up a configuration with the symbols and the canvas size in characters */ config = chafa_canvas_config_new(); chafa_canvas_config_set_canvas_mode(config, mode); chafa_canvas_config_set_pixel_mode(config, pixel_mode); chafa_canvas_config_set_geometry(config, width_cells, height_cells); chafa_canvas_config_set_symbol_map(config, symbol_map); if (cell_width > 0 && cell_height > 0) { /* We know the pixel dimensions of each cell. Store it in the config. */ chafa_canvas_config_set_cell_geometry(config, cell_width, cell_height); } /* Create canvas */ canvas = chafa_canvas_new(config); /* Draw pixels to the canvas */ chafa_canvas_draw_all_pixels(canvas, pixel_type, pixels, pix_width, pix_height, pix_rowstride); /* Build printable string */ printable = chafa_canvas_print(canvas, term_info); /* Clean up and return */ chafa_canvas_unref(canvas); chafa_canvas_config_unref(config); chafa_symbol_map_unref(symbol_map); chafa_term_info_unref(term_info); canvas = NULL; config = NULL; symbol_map = NULL; term_info = NULL; return printable; } void printImage(const char *image_path, int width, int height) { FreeImage_Initialise(false); // Detect the image type FREE_IMAGE_FORMAT image_format = FreeImage_GetFileType(image_path, 0); if (image_format == FIF_UNKNOWN) { // Failed to detect the image type printf("Unknown image format: %s\n", image_path); return; } // Load the image FIBITMAP *image = FreeImage_Load(image_format, image_path, 0); if (!image) { // Failed to load the image printf("Failed to load image: %s\n", image_path); return; } // Convert the image to a bitmap FIBITMAP *bitmap = FreeImage_ConvertTo32Bits(image); if (!bitmap) { // Failed to convert the image to a bitmap printf("Failed to convert image to bitmap.\n"); FreeImage_Unload(image); return; } int pix_width = FreeImage_GetWidth(bitmap); int pix_height = FreeImage_GetHeight(bitmap); int n_channels = FreeImage_GetBPP(bitmap) / 8; unsigned char *pixels = (unsigned char *)FreeImage_GetBits(bitmap); FreeImage_FlipVertical(bitmap); TermSize term_size; GString *printable; gfloat font_ratio = 0.5; gint cell_width = -1, cell_height = -1; gint width_cells, height_cells; tty_init(); get_tty_size(&term_size); if (term_size.width_cells > 0 && term_size.height_cells > 0 && term_size.width_pixels > 0 && term_size.height_pixels > 0) { cell_width = term_size.width_pixels / term_size.width_cells; cell_height = term_size.height_pixels / term_size.height_cells; font_ratio = (gdouble)cell_width / (gdouble)cell_height; } width_cells = term_size.width_cells; height_cells = term_size.height_cells; chafa_calc_canvas_geometry(pix_width, pix_height, &width_cells, &height_cells, font_ratio, TRUE, FALSE); /* Convert the image to a printable string */ printable = convert_image(pixels, pix_width, pix_height, pix_width * n_channels, CHAFA_PIXEL_BGRA8_UNASSOCIATED, width, height, cell_width, cell_height); /* Print the string */ fwrite(printable->str, sizeof(char), printable->len, stdout); fputc('\n', stdout); // Free resources FreeImage_Unload(bitmap); FreeImage_Unload(image); g_string_free(printable, TRUE); } FIBITMAP *getBitmap(const char *image_path) { if (image_path == NULL) return NULL; FreeImage_Initialise(false); FREE_IMAGE_FORMAT image_format = FreeImage_GetFileType(image_path, 0); if (image_format == FIF_UNKNOWN) { return NULL; } FIBITMAP *image = FreeImage_Load(image_format, image_path, 0); if (!image) { return NULL; } FIBITMAP *bitmap = FreeImage_ConvertTo32Bits(image); FreeImage_FlipVertical(bitmap); FreeImage_Unload(image); if (!bitmap) { return NULL; } return bitmap; } void printBitmap(FIBITMAP *bitmap, int width, int height) { if (bitmap == NULL) { return; } int pix_width = FreeImage_GetWidth(bitmap); int pix_height = FreeImage_GetHeight(bitmap); int n_channels = FreeImage_GetBPP(bitmap) / 8; unsigned char *pixels = (unsigned char *)FreeImage_GetBits(bitmap); TermSize term_size; GString *printable; gfloat font_ratio = 0.5; gint cell_width = -1, cell_height = -1; gint width_cells, height_cells; tty_init(); get_tty_size(&term_size); if (term_size.width_cells > 0 && term_size.height_cells > 0 && term_size.width_pixels > 0 && term_size.height_pixels > 0) { cell_width = term_size.width_pixels / term_size.width_cells; cell_height = term_size.height_pixels / term_size.height_cells; font_ratio = (gdouble)cell_width / (gdouble)cell_height; } width_cells = term_size.width_cells; height_cells = term_size.height_cells; chafa_calc_canvas_geometry(pix_width, pix_height, &width_cells, &height_cells, font_ratio, TRUE, FALSE); /* Convert the image to a printable string */ printable = convert_image(pixels, pix_width, pix_height, pix_width * n_channels, CHAFA_PIXEL_BGRA8_UNASSOCIATED, width, height, cell_width, cell_height); /* Print the string */ fwrite(printable->str, sizeof(char), printable->len, stdout); g_string_free(printable, TRUE); } void printBitmapCentered(FIBITMAP *bitmap, int width, int height) { if (bitmap == NULL) { return; } int pix_width = FreeImage_GetWidth(bitmap); int pix_height = FreeImage_GetHeight(bitmap); int n_channels = FreeImage_GetBPP(bitmap) / 8; unsigned char *pixels = (unsigned char *)FreeImage_GetBits(bitmap); TermSize term_size; GString *printable; gint cell_width = -1, cell_height = -1; tty_init(); get_tty_size(&term_size); if (term_size.width_cells > 0 && term_size.height_cells > 0 && term_size.width_pixels > 0 && term_size.height_pixels > 0) { cell_width = term_size.width_pixels / term_size.width_cells; cell_height = term_size.height_pixels / term_size.height_cells; } printable = convert_image(pixels, pix_width, pix_height, pix_width * n_channels, CHAFA_PIXEL_BGRA8_UNASSOCIATED, width, height, cell_width, cell_height); g_string_append_c(printable, '\0'); const gchar *delimiters = "\n"; gchar **lines = g_strsplit(printable->str, delimiters, -1); int indentation = ((term_size.width_cells - width) / 2) + 1; for (int i = 0; lines[i] != NULL; i++) { printf("\n%*s%s", indentation, "", lines[i]); } g_strfreev(lines); g_string_free(printable, TRUE); } unsigned char luminance(unsigned char r, unsigned char g, unsigned char b) { return (unsigned char)(0.2126 * r + 0.7152 * g + 0.0722 * b); } void checkIfBrightPixel(unsigned char r, unsigned char g, unsigned char b, bool *found) { // Calc luminace and use to find Ascii char. unsigned char ch = luminance(r, g, b); if (ch > 80 && !(r < g + 20 && r > g - 20 && g < b + 20 && g > b - 20) && !(r > 150 && g > 150 && b > 150)) { *found = true; } } int getCoverColor(FIBITMAP *bitmap, unsigned char *r, unsigned char *g, unsigned char *b) { int rwidth = FreeImage_GetWidth(bitmap); int rheight = FreeImage_GetHeight(bitmap); int rchannels = FreeImage_GetBPP(bitmap) / 8; unsigned char *read_data = (unsigned char *)FreeImage_GetBits(bitmap); if (read_data == NULL) { return -1; } bool found = false; int numPixels = rwidth * rheight; for (int i = 0; i < numPixels; i++) { int index = i * rchannels; unsigned char blue = 0; unsigned char green = 0; unsigned char red = 0; if (rchannels >= 3) { blue = read_data[index + 0]; green = read_data[index + 1]; red = read_data[index + 2]; } else if (rchannels >= 1) { blue = green = red = read_data[index]; } checkIfBrightPixel(red, green, blue, &found); if (found) { *(r) = red; *(g) = green; *(b) = blue; break; } } return 0; } kew-2.4.2/src/chafafunc.h000066400000000000000000000007661457005474400152060ustar00rootroot00000000000000#include #include #include #include #include #include #include #include void printImage(const char *image_path, int width, int height); FIBITMAP *getBitmap(const char *image_path); void printBitmap(FIBITMAP *bitmap, int width, int height); void printBitmapCentered(FIBITMAP *bitmap, int width, int height); int getCoverColor(FIBITMAP *bitmap, unsigned char *r, unsigned char *g, unsigned char *b); kew-2.4.2/src/directorytree.c000066400000000000000000000324541457005474400161460ustar00rootroot00000000000000#include "directorytree.h" static int lastUsedId = 0; typedef void (*TimeoutCallback)(void); FileSystemEntry *createEntry(const char *name, int isDirectory, FileSystemEntry *parent) { FileSystemEntry *newEntry = (FileSystemEntry *)malloc(sizeof(FileSystemEntry)); if (newEntry != NULL) { newEntry->name = strdup(name); newEntry->isDirectory = isDirectory; newEntry->isEnqueued = 0; newEntry->parent = parent; newEntry->children = NULL; newEntry->next = NULL; newEntry->id = ++lastUsedId; if (parent != NULL) { newEntry->parentId = parent->id; } else { newEntry->parentId = -1; } } return newEntry; } void addChild(FileSystemEntry *parent, FileSystemEntry *child) { if (parent != NULL) { child->next = parent->children; parent->children = child; } } void setFullPath(FileSystemEntry *entry, const char *parentPath, const char *entryName) { if (entry == NULL || parentPath == NULL || entryName == NULL) { return; } size_t fullPathLength = strlen(parentPath) + strlen(entryName) + 2; // +2 for '/' and '\0' entry->fullPath = (char *)malloc(fullPathLength); if (entry->fullPath == NULL) { return; } snprintf(entry->fullPath, fullPathLength, "%s/%s", parentPath, entryName); } void displayTreeSimple(FileSystemEntry *root, int depth) { for (int i = 0; i < depth; ++i) { printf(" "); } printf("%s", root->name); if (root->isDirectory) { printf(" (Directory)\n"); FileSystemEntry *child = root->children; while (child != NULL) { displayTreeSimple(child, depth + 1); child = child->next; } } else { printf(" (File)\n"); } } void freeTree(FileSystemEntry *root) { if (root == NULL) { return; } FileSystemEntry *child = root->children; while (child != NULL) { FileSystemEntry *next = child->next; freeTree(child); child = next; } free(root->name); free(root->fullPath); free(root); } int removeEmptyDirectories(FileSystemEntry *node) { if (node == NULL) { return 0; } FileSystemEntry *currentChild = node->children; FileSystemEntry *prevChild = NULL; int numEntries = 0; while (currentChild != NULL) { if (currentChild->isDirectory) { numEntries += removeEmptyDirectories(currentChild); if (currentChild->children == NULL) { if (prevChild == NULL) { node->children = currentChild->next; } else { prevChild->next = currentChild->next; } FileSystemEntry *toFree = currentChild; currentChild = currentChild->next; free(toFree->name); free(toFree->fullPath); free(toFree); numEntries++; continue; } } prevChild = currentChild; currentChild = currentChild->next; } return numEntries; } char *stringToUpperWithoutSpaces(const char *str) { if (str == NULL) { return NULL; } size_t len = strlen(str); char *result = (char *)malloc(len + 1); if (result == NULL) { return NULL; } size_t resultIndex = 0; for (size_t i = 0; i < len; ++i) { if (!isspace((unsigned char)str[i])) { result[resultIndex++] = toupper((unsigned char)str[i]); } } result[resultIndex] = '\0'; return result; } int compareLibEntries(const struct dirent **a, const struct dirent **b) { char *nameA = stringToUpperWithoutSpaces((*a)->d_name); char *nameB = stringToUpperWithoutSpaces((*b)->d_name); if (nameA[0] == '_' && nameB[0] != '_') { free(nameA); free(nameB); return 1; } else if (nameA[0] != '_' && nameB[0] == '_') { free(nameA); free(nameB); return -1; } int result = strcmp(nameB, nameA); free(nameA); free(nameB); return result; } int readDirectory(const char *path, FileSystemEntry *parent) { DIR *directory = opendir(path); if (directory == NULL) { perror("Error opening directory"); return 0; } struct dirent **entries; int dirEntries = scandir(path, &entries, NULL, compareLibEntries); if (dirEntries < 0) { perror("Error scanning directory entries"); closedir(directory); return 0; } regex_t regex; regcomp(®ex, AUDIO_EXTENSIONS, REG_EXTENDED); int numEntries = 0; for (int i = 0; i < dirEntries; ++i) { struct dirent *entry = entries[i]; if (entry->d_name[0] != '.' && strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) { char childPath[MAXPATHLEN]; snprintf(childPath, sizeof(childPath), "%s/%s", path, entry->d_name); struct stat fileStats; if (stat(childPath, &fileStats) == -1) { continue; } int isDirectory = true; if (S_ISREG(fileStats.st_mode)) { isDirectory = false; } char exto[6]; extractExtension(entry->d_name, sizeof(exto) - 1, exto); int isAudio = match_regex(®ex, exto); if (isAudio == 0 || isDirectory) { FileSystemEntry *child = createEntry(entry->d_name, isDirectory, parent); if (entry != NULL) { setFullPath(child, path, entry->d_name); } addChild(parent, child); if (isDirectory) { numEntries++; numEntries += readDirectory(childPath, child); } } } free(entry); } free(entries); regfree(®ex); closedir(directory); return numEntries; } void writeTreeToFile(FileSystemEntry *node, FILE *file, int parentId) { if (node == NULL) { return; } fprintf(file, "%d\t%s\t%d\t%d\n", node->id, node->name, node->isDirectory, parentId); FileSystemEntry *child = node->children; FileSystemEntry *tmp = NULL; while (child) { tmp = child->next; writeTreeToFile(child, file, node->id); child = tmp; } free(node->name); free(node->fullPath); free(node); } void freeAndWriteTree(FileSystemEntry *root, const char *filename) { FILE *file = fopen(filename, "w"); if (!file) { perror("Failed to open file"); return; } writeTreeToFile(root, file, -1); fclose(file); } FileSystemEntry *createDirectoryTree(const char *startPath, int *numEntries) { FileSystemEntry *root = createEntry("root", 1, NULL); setFullPath(root, "", ""); *numEntries = readDirectory(startPath, root); *numEntries -= removeEmptyDirectories(root); lastUsedId = 0; return root; } FileSystemEntry **resizeNodesArray(FileSystemEntry **nodes, int oldSize, int newSize) { FileSystemEntry **newNodes = realloc(nodes, newSize * sizeof(FileSystemEntry *)); if (newNodes) { for (int i = oldSize; i < newSize; i++) { newNodes[i] = NULL; } } return newNodes; } FileSystemEntry *reconstructTreeFromFile(const char *filename, const char *startMusicPath, int *numDirectoryEntries) { FILE *file = fopen(filename, "r"); if (!file) { return NULL; } char line[1024]; int nodesCount = 0, nodesCapacity = 1000, oldCapacity = 0; FileSystemEntry **nodes = calloc(nodesCapacity, sizeof(FileSystemEntry *)); if (!nodes) { fclose(file); return NULL; } FileSystemEntry *root = NULL; while (fgets(line, sizeof(line), file)) { int id, parentId, isDirectory; char name[256]; if (sscanf(line, "%d\t%255[^\t]\t%d\t%d", &id, name, &isDirectory, &parentId) == 4) { if (id >= nodesCapacity) { oldCapacity = nodesCapacity; nodesCapacity = id + 100; FileSystemEntry **tempNodes = resizeNodesArray(nodes, oldCapacity, nodesCapacity); if (!tempNodes) { perror("Failed to resize nodes array"); for (int i = 0; i < nodesCount; i++) { if (nodes[i]) { free(nodes[i]->name); free(nodes[i]->fullPath); free(nodes[i]); } } free(nodes); fclose(file); exit(EXIT_FAILURE); } nodes = tempNodes; } FileSystemEntry *node = malloc(sizeof(FileSystemEntry)); if (!node) { perror("Failed to allocate node"); fclose(file); exit(EXIT_FAILURE); } node->id = id; node->name = strdup(name); node->isDirectory = isDirectory; node->isEnqueued = 0; node->children = node->next = node->parent = NULL; nodes[id] = node; nodesCount++; if (parentId >= 0 && nodes[parentId]) { node->parent = nodes[parentId]; if (nodes[parentId]->children) { FileSystemEntry *child = nodes[parentId]->children; while (child->next) { child = child->next; } child->next = node; } else { nodes[parentId]->children = node; } setFullPath(node, nodes[parentId]->fullPath, node->name); if (isDirectory) *numDirectoryEntries = *numDirectoryEntries + 1; } else { root = node; setFullPath(node, startMusicPath, ""); } } } fclose(file); free(nodes); return root; } kew-2.4.2/src/directorytree.h000066400000000000000000000020361457005474400161440ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include "file.h" #include "utils.h" #ifndef FILE_SYSTEM_ENTRY #define FILE_SYSTEM_ENTRY typedef struct FileSystemEntry { int id; char *name; char *fullPath; int isDirectory; // 1 for directory, 0 for file int isEnqueued; int parentId; struct FileSystemEntry *parent; struct FileSystemEntry *children; struct FileSystemEntry *next; // For siblings (next node in the same directory) } FileSystemEntry; #endif #ifndef SLOWLOADING_CALLBACK #define SLOWLOADING_CALLBACK typedef void (*SlowloadingCallback)(void); #endif FileSystemEntry *createDirectoryTree(const char *startPath, int *numEntries); void freeTree(FileSystemEntry *root); void freeAndWriteTree(FileSystemEntry *root, const char *filename); FileSystemEntry *reconstructTreeFromFile(const char *filename, const char *startMusicPath, int *numDirectoryEntries); kew-2.4.2/src/events.h000066400000000000000000000020151457005474400145610ustar00rootroot00000000000000enum EventType { EVENT_NONE, EVENT_PLAY_PAUSE, EVENT_VOLUME_UP, EVENT_VOLUME_DOWN, EVENT_NEXT, EVENT_PREV, EVENT_QUIT, EVENT_TOGGLECOVERS, EVENT_TOGGLEREPEAT, EVENT_TOGGLEVISUALIZER, EVENT_TOGGLEBLOCKS, EVENT_ADDTOMAINPLAYLIST, EVENT_DELETEFROMMAINPLAYLIST, EVENT_EXPORTPLAYLIST, EVENT_UPDATELIBRARY, EVENT_SHUFFLE, EVENT_KEY_PRESS, EVENT_SHOWKEYBINDINGS, EVENT_SHOWINFO, EVENT_GOTOSONG, EVENT_GOTOBEGINNINGOFPLAYLIST, EVENT_GOTOENDOFPLAYLIST, EVENT_TOGGLE_PROFILE_COLORS, EVENT_SCROLLNEXT, EVENT_SCROLLPREV, EVENT_SEEKBACK, EVENT_SEEKFORWARD, EVENT_SHOWLIBRARY, EVENT_SHOWTRACK, EVENT_NEXTPAGE, EVENT_PREVPAGE, EVENT_REMOVE }; struct Event { enum EventType type; char key; }; typedef struct { char *seq; enum EventType eventType; } EventMapping; kew-2.4.2/src/file.c000066400000000000000000000320061457005474400141720ustar00rootroot00000000000000#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #include "file.h" /* file.c This file should contain only simple utility functions related to files and directories. They should work independently and be as decoupled from the rest of the application as possible. */ void getDirectoryFromPath(const char *path, char *directory) { size_t pathLength = strlen(path); // Find the last occurrence of the directory separator character ('/' or '\') const char *lastSeparator = strrchr(path, '/'); const char *lastBackslash = strrchr(path, '\\'); // Determine the last occurrence of the directory separator const char *lastDirectorySeparator = lastSeparator > lastBackslash ? lastSeparator : lastBackslash; if (lastDirectorySeparator != NULL) { // Calculate the length of the directory path size_t directoryLength = lastDirectorySeparator - path + 1; if (directoryLength < pathLength) { // Copy the directory path into the 'directory' buffer strncpy(directory, path, directoryLength); directory[directoryLength] = '\0'; } else { // The provided path is already a directory c_strcpy(directory, sizeof(directory), path); } } else { // No directory separator found, return an empty string directory[0] = '\0'; } } int existsFile(const char *fname) { FILE *file; if ((file = fopen(fname, "r"))) { fclose(file); return 1; } return -1; } int isDirectory(const char *path) { DIR *dir = opendir(path); if (dir) { closedir(dir); return 1; } else { if (errno == ENOENT) { return -1; } return 0; } } // Traverse a directory tree and search for a given file or directory int walker(const char *startPath, const char *searching, char *result, const char *allowedExtensions, enum SearchType searchType, bool exactSearch) { DIR *d; struct dirent *dir; struct stat file_stat; char ext[6]; // +1 for null-terminator regex_t regex; int ret = regcomp(®ex, allowedExtensions, REG_EXTENDED); if (ret != 0) { return -1; } bool copyresult = false; if (startPath != NULL) { d = opendir(startPath); if (d == NULL) { fprintf(stderr, "Failed to open directory.\n"); return 0; } int chdirResult = chdir(startPath); if (chdirResult != 0) { fprintf(stderr, "Failed to change directory: %s\n", startPath); return 0; } } else { d = opendir("."); if (d == NULL) { fprintf(stderr, "Failed to open current directory.\n"); return 0; } } while ((dir = readdir(d))) { if (strcmp(dir->d_name, ".") == 0 || strcmp(dir->d_name, "..") == 0) { continue; } char entryPath[MAXPATHLEN]; char *currentDir = getcwd(NULL, 0); snprintf(entryPath, sizeof(entryPath), "%s/%s", currentDir, dir->d_name); free(currentDir); if (stat(entryPath, &file_stat) != 0) { continue; } if (S_ISDIR(file_stat.st_mode)) { if (((exactSearch && (strcasecmp(dir->d_name, searching) == 0)) || (!exactSearch && c_strcasestr(dir->d_name, searching) != NULL)) && (searchType != FileOnly) && (searchType != SearchPlayList)) { char *curDir = getcwd(NULL, 0); snprintf(result, MAXPATHLEN, "%s/%s", curDir, dir->d_name); free(curDir); copyresult = true; break; } else { if (chdir(dir->d_name) == -1) { fprintf(stderr, "Failed to change directory: %s\n", dir->d_name); continue; } if (walker(NULL, searching, result, allowedExtensions, searchType, exactSearch) == 0) { copyresult = true; break; } if (chdir("..") == -1) { fprintf(stderr, "Failed to change directory to parent.\n"); break; } } } else { if (searchType == DirOnly) { continue; } char *filename = dir->d_name; if (strlen(filename) <= 4) { continue; } extractExtension(filename, sizeof(ext) - 1, ext); if (match_regex(®ex, ext) != 0) { continue; } if ((exactSearch && (strcasecmp(dir->d_name, searching) == 0)) || (!exactSearch && c_strcasestr(dir->d_name, searching) != NULL)) { char *curDir = getcwd(NULL, 0); snprintf(result, MAXPATHLEN, "%s/%s", curDir, dir->d_name); copyresult = true; free(curDir); break; } } } closedir(d); regfree(®ex); return copyresult ? 0 : 1; } int expandPath(const char *inputPath, char *expandedPath) { if (inputPath[0] == '\0' || inputPath[0] == '\r') return -1; if (inputPath[0] == '~') // Check if inputPath starts with '~' { const char *homeDir; if (inputPath[1] == '/' || inputPath[1] == '\0') // Handle "~/" { homeDir = getenv("HOME"); if (homeDir == NULL) { return -1; // Unable to retrieve home directory } inputPath++; // Skip '~' character } else // Handle "~username/" { const char *username = inputPath + 1; const char *slash = strchr(username, '/'); if (slash == NULL) { struct passwd *pw = getpwnam(username); if (pw == NULL) { return -1; // Unable to retrieve user directory } homeDir = pw->pw_dir; inputPath = ""; // Empty path component after '~username' } else { size_t usernameLen = slash - username; struct passwd *pw = getpwuid(getuid()); if (pw == NULL) { return -1; // Unable to retrieve user directory } homeDir = pw->pw_dir; inputPath += usernameLen + 1; // Skip '~username/' component } } size_t homeDirLen = strlen(homeDir); size_t inputPathLen = strlen(inputPath); if (homeDirLen + inputPathLen >= MAXPATHLEN) { return -1; // Expanded path exceeds maximum length } strcpy(expandedPath, homeDir); strcat(expandedPath, inputPath); } else // Handle if path is not prefixed with '~' { if (realpath(inputPath, expandedPath) == NULL) { return -1; // Unable to expand the path } } return 0; // Path expansion successful } int createDirectory(const char *path) { struct stat st; // Check if directory already exists if (stat(path, &st) == 0) { if (S_ISDIR(st.st_mode)) return 0; // Directory already exists else return -1; // Path exists but is not a directory } // Directory does not exist, so create it if (mkdir(path, 0700) == 0) return 1; // Directory created successfully return -1; // Failed to create directory } int removeDirectory(const char *path) { struct stat st; // Check if path exists if (stat(path, &st) != 0) return -1; // Path does not exist // Check if it is a directory if (!S_ISDIR(st.st_mode)) return -1; // Path exists but is not a directory DIR *dir = opendir(path); if (dir == NULL) return -1; // Failed to open directory struct dirent *entry; char filePath[MAXPATHLEN]; // Remove all entries in the directory while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; // Skip current and parent directories snprintf(filePath, sizeof(filePath), "%s/%s", path, entry->d_name); if (entry->d_type == DT_DIR) removeDirectory(filePath); // Recursively remove subdirectories else remove(filePath); // Remove regular file } closedir(dir); // Remove the directory itself if (rmdir(path) == 0) return 0; // Directory removed successfully return -1; // Failed to remove directory } int deleteFile(const char *filePath) { if (remove(filePath) == 0) { return 0; } else { return -1; } } int isInTempDir(const char *path) { const char *tempDir = getenv("TMPDIR"); if (tempDir == NULL) { #ifdef __APPLE__ tempDir = "/tmp"; #else tempDir = "/tmp"; #endif } return (startsWith(path, tempDir)); } void deleteTempDir() { const char *tempDir = getenv("TMPDIR"); if (tempDir == NULL) { #ifdef __APPLE__ tempDir = "/tmp"; #else tempDir = "/tmp"; #endif } else { } char dirPath[MAXPATHLEN]; struct passwd *pw = getpwuid(getuid()); const char *username = pw->pw_name; snprintf(dirPath, MAXPATHLEN, "%s/kew/%s", tempDir, username); removeDirectory(dirPath); } bool checkFileBelowMaxSize(const char *filePath, int maxSize) { struct stat st; if (stat(filePath, &st) == 0) { return (st.st_size <= maxSize); } perror("stat failed"); return false; } void generateTempFilePath(char *filePath, const char *prefix, const char *suffix) { const char *tempDir = getenv("TMPDIR"); if (tempDir == NULL) { tempDir = "/tmp"; } char dirPath[MAXPATHLEN]; struct passwd *pw = getpwuid(getuid()); const char *username = pw->pw_name; snprintf(dirPath, MAXPATHLEN, "%s/kew", tempDir); createDirectory(dirPath); snprintf(dirPath, MAXPATHLEN, "%s/kew/%s", tempDir, username); createDirectory(dirPath); char randomString[7]; for (int i = 0; i < 6; ++i) { randomString[i] = 'a' + rand() % 26; } randomString[6] = '\0'; snprintf(filePath, MAXPATHLEN + 7, "%s/%s%.6s%s", dirPath, prefix, randomString, suffix); } kew-2.4.2/src/file.h000066400000000000000000000024341457005474400142010ustar00rootroot00000000000000#ifndef FILE_H #define FILE_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils.h" #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef AUDIO_EXTENSIONS #define AUDIO_EXTENSIONS "\\.(m4a|aac|mp4|mp3|ogg|flac|wav|opus)$" #endif enum SearchType { SearchAny = 0, DirOnly = 1, FileOnly = 2, SearchPlayList = 3, ReturnAllSongs = 4 }; void getDirectoryFromPath(const char *path, char *directory); int isDirectory(const char *path); /* Traverse a directory tree and search for a given file or directory */ int walker(const char *startPath, const char *searching, char *result, const char *allowedExtensions, enum SearchType searchType, bool exactSearch); int expandPath(const char *inputPath, char *expandedPath); int createDirectory(const char *path); int removeDirectory(const char *path); int deleteFile(const char *filePath); void generateTempFilePath(char *filePath, const char *prefix, const char *suffix); void deleteTempDir(void); int isInTempDir(const char *path); int existsFile(const char *fname); #endif kew-2.4.2/src/kew.c000066400000000000000000001060121457005474400140400ustar00rootroot00000000000000/* kew - a command-line music player Copyright (C) 2022 Ravachol http://github.com/ravachol/kew $$\ $$ | $$ | $$\ $$$$$$\ $$\ $$\ $$\ $$ | $$ |$$ __$$\ $$ | $$ | $$ | $$$$$$ / $$$$$$$$ |$$ | $$ | $$ | $$ _$$< $$ ____|$$ | $$ | $$ | $$ | \$$\ \$$$$$$$\ \$$$$$\$$$$ | \__| \__| \_______| \_____\____/ 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, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L #endif #ifndef __USE_POSIX #define __USE_POSIX #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cache.h" #include "events.h" #include "file.h" #include "mpris.h" #include "player.h" #include "playerops.h" #include "playlist.h" #include "settings.h" #include "sound.h" #include "soundcommon.h" #include "songloader.h" #include "utils.h" // #define DEBUG 1 #define MAX_SEQ_LEN 1024 // Maximum length of sequence buffer #define MAX_TMP_SEQ_LEN 256 // Maximum length of temporary sequence buffer #define COOLDOWN_MS 500 #define COOLDOWN2_MS 100 FILE *logFile = NULL; struct winsize windowSize; static bool eventProcessed = false; char digitsPressed[MAX_SEQ_LEN]; int digitsPressedCount = 0; int maxDigitsPressedCount = 9; bool gPressed = false; bool loadingAudioData = false; bool goingToSong = false; bool startFromTop = false; bool exactSearch = false; AppSettings settings; bool isCooldownElapsed(int milliSeconds) { struct timespec currentTime; clock_gettime(CLOCK_MONOTONIC, ¤tTime); double elapsedMilliseconds = (currentTime.tv_sec - lastInputTime.tv_sec) * 1000.0 + (currentTime.tv_nsec - lastInputTime.tv_nsec) / 1000000.0; return elapsedMilliseconds >= milliSeconds; } struct Event processInput() { struct Event event; event.type = EVENT_NONE; event.key = '\0'; bool cooldownElapsed = false; bool cooldown2Elapsed = false; if (!isInputAvailable()) { flushSeek(); return event; } if (isCooldownElapsed(COOLDOWN_MS) && !eventProcessed) cooldownElapsed = true; if (isCooldownElapsed(COOLDOWN2_MS) && !eventProcessed) cooldown2Elapsed = true; int seqLength = 0; char seq[MAX_SEQ_LEN]; seq[0] = '\0'; // Set initial value int keyReleased = 0; // Find input while (isInputAvailable()) { char tmpSeq[MAX_TMP_SEQ_LEN]; seqLength = seqLength + readInputSequence(tmpSeq, sizeof(tmpSeq)); // Release most keys directly, seekbackward and seekforward can be read continuously if (seqLength <= 0 && strcmp(seq, settings.seekBackward) != 0 && strcmp(seq, settings.seekForward) != 0) { keyReleased = 1; break; } if (strlen(seq) + strlen(tmpSeq) >= MAX_SEQ_LEN) { break; } strcat(seq, tmpSeq); c_sleep(10); // This slows the continous reads down to not get a a too fast scrolling speed if (strcmp(seq, settings.hardScrollUp) == 0 || strcmp(seq, settings.hardScrollDown) == 0 || strcmp(seq, settings.scrollUpAlt) == 0 || strcmp(seq, settings.scrollDownAlt) == 0 || strcmp(seq, settings.seekBackward) == 0 || strcmp(seq, settings.seekForward) == 0 || strcmp(seq, settings.hardNextPage) == 0 || strcmp(seq, settings.hardPrevPage) == 0) { // Do dummy reads to prevent scrolling continuing after we release the key readInputSequence(tmpSeq, 3); readInputSequence(tmpSeq, 3); keyReleased = 0; break; } keyReleased = 0; } if (keyReleased) return event; eventProcessed = true; event.type = EVENT_NONE; event.key = seq[0]; // Map keys to events EventMapping keyMappings[] = {{settings.scrollUpAlt, EVENT_SCROLLPREV}, {settings.scrollDownAlt, EVENT_SCROLLNEXT}, {settings.nextTrackAlt, EVENT_NEXT}, {settings.previousTrackAlt, EVENT_PREV}, {settings.volumeUp, EVENT_VOLUME_UP}, {settings.volumeUpAlt, EVENT_VOLUME_UP}, {settings.volumeDown, EVENT_VOLUME_DOWN}, {settings.togglePause, EVENT_PLAY_PAUSE}, {settings.quit, EVENT_QUIT}, {settings.toggleShuffle, EVENT_SHUFFLE}, {settings.toggleCovers, EVENT_TOGGLECOVERS}, {settings.toggleVisualizer, EVENT_TOGGLEVISUALIZER}, {settings.toggleAscii, EVENT_TOGGLEBLOCKS}, {settings.switchNumberedSong, EVENT_GOTOSONG}, {settings.seekBackward, EVENT_SEEKBACK}, {settings.seekForward, EVENT_SEEKFORWARD}, {settings.toggleRepeat, EVENT_TOGGLEREPEAT}, {settings.savePlaylist, EVENT_EXPORTPLAYLIST}, {settings.toggleColorsDerivedFrom, EVENT_TOGGLE_PROFILE_COLORS}, {settings.addToMainPlaylist, EVENT_ADDTOMAINPLAYLIST}, {settings.updateLibrary, EVENT_UPDATELIBRARY}, {settings.hardPlayPause, EVENT_PLAY_PAUSE}, {settings.hardPrev, EVENT_PREV}, {settings.hardNext, EVENT_NEXT}, {settings.hardSwitchNumberedSong, EVENT_GOTOSONG}, {settings.hardScrollUp, EVENT_SCROLLPREV}, {settings.hardScrollDown, EVENT_SCROLLNEXT}, {settings.hardShowInfo, EVENT_SHOWINFO}, {settings.hardShowInfoAlt, EVENT_SHOWINFO}, {settings.hardShowKeys, EVENT_SHOWKEYBINDINGS}, {settings.hardShowKeysAlt, EVENT_SHOWKEYBINDINGS}, {settings.hardEndOfPlaylist, EVENT_GOTOENDOFPLAYLIST}, {settings.hardShowTrack, EVENT_SHOWTRACK}, {settings.hardShowTrackAlt, EVENT_SHOWTRACK}, {settings.hardShowLibrary, EVENT_SHOWLIBRARY}, {settings.hardShowLibraryAlt, EVENT_SHOWLIBRARY}, {settings.hardNextPage, EVENT_NEXTPAGE}, {settings.hardPrevPage, EVENT_PREVPAGE}, {settings.hardRemove, EVENT_REMOVE}}; int numKeyMappings = sizeof(keyMappings) / sizeof(EventMapping); // Set event for pressed key for (int i = 0; i < numKeyMappings; i++) { if (strcmp(seq, keyMappings[i].seq) == 0) { event.type = keyMappings[i].eventType; break; } } // Handle gg if (event.key == 'g' && event.type == EVENT_NONE) { if (gPressed) { event.type = EVENT_GOTOBEGINNINGOFPLAYLIST; gPressed = false; } else { gPressed = true; } } // Handle numbers if (isdigit(event.key)) { if (digitsPressedCount < maxDigitsPressedCount) digitsPressed[digitsPressedCount++] = event.key; } else { // Handle multiple digits, sometimes mixed with other keys for (int i = 0; i < MAX_SEQ_LEN; i++) { if (isdigit(seq[i])) { if (digitsPressedCount < maxDigitsPressedCount) digitsPressed[digitsPressedCount++] = seq[i]; } else { if (seq[i] == '\0') break; if (seq[i] != settings.switchNumberedSong[0] && seq[i] != settings.hardSwitchNumberedSong[0] && seq[i] != settings.hardEndOfPlaylist[0]) { memset(digitsPressed, '\0', sizeof(digitsPressed)); digitsPressedCount = 0; break; } else if (seq[i] == settings.hardEndOfPlaylist[0]) { event.type = EVENT_GOTOENDOFPLAYLIST; break; } else { event.type = EVENT_GOTOSONG; break; } } } } // Handle song prev/next cooldown if (!cooldownElapsed && (event.type == EVENT_NEXT || event.type == EVENT_PREV)) event.type = EVENT_NONE; else if (event.type == EVENT_NEXT || event.type == EVENT_PREV) updateLastInputTime(); // Handle seek/remove cooldown if (!cooldown2Elapsed && (event.type == EVENT_REMOVE || event.type == EVENT_SEEKBACK || event.type == EVENT_SEEKFORWARD)) event.type = EVENT_NONE; else if (event.type == EVENT_REMOVE || event.type == EVENT_SEEKBACK || event.type == EVENT_SEEKFORWARD) updateLastInputTime(); // Forget Numbers if (event.type != EVENT_GOTOSONG && event.type != EVENT_GOTOENDOFPLAYLIST && event.type != EVENT_NONE) { memset(digitsPressed, '\0', sizeof(digitsPressed)); digitsPressedCount = 0; } // Forget g pressed if (event.key != 'g') { gPressed = false; } return event; } void setEndOfListReached() { appState.currentView = LIBRARY_VIEW; loadedNextSong = false; audioData.endOfListReached = true; usingSongDataA = false; audioData.currentFileIndex = 0; audioData.restart = true; loadingdata.loadA = true; emitMetadataChanged("", "", "", "", "/org/mpris/MediaPlayer2/TrackList/NoTrack", NULL, 0); emitPlaybackStoppedMpris(); pthread_mutex_lock(&dataSourceMutex); cleanupPlaybackDevice(); pthread_mutex_unlock(&dataSourceMutex); refresh = true; chosenRow = playlist.count - 1; } void finishLoading() { int maxNumTries = 20; int numtries = 0; while (!loadedNextSong && !loadingFailed && numtries < maxNumTries) { c_sleep(100); numtries++; } loadedNextSong = true; } void resetTimeCount() { elapsedSeconds = 0.0; pauseSeconds = 0.0; totalPauseSeconds = 0.0; } void notifySongSwitch() { SongData *currentSongData = (audioData.currentFileIndex == 0) ? userData.songdataA : userData.songdataB; bool isDeleted = (audioData.currentFileIndex == 0) ? userData.songdataADeleted == true : userData.songdataBDeleted == true; if (isDeleted == false && currentSongData != NULL && currentSongData->hasErrors == 0 && currentSongData->metadata && strlen(currentSongData->metadata->title) > 0) displaySongNotification(currentSongData->metadata->artist, currentSongData->metadata->title, currentSongData->coverArtPath); } void prepareNextSong() { if (!skipOutOfOrder && !isRepeatEnabled()) { if (currentSong != NULL) lastPlayedId = currentSong->id; currentSong = getNextSong(); } else { skipOutOfOrder = false; } finishLoading(); resetTimeCount(); nextSong = NULL; refresh = true; if (!isRepeatEnabled() || currentSong == NULL) { unloadPreviousSong(); } if (currentSong == NULL) { if (quitAfterStopping) quit(); else setEndOfListReached(); } else { notifySongSwitch(); } clock_gettime(CLOCK_MONOTONIC, &start_time); } void refreshPlayer() { if (pthread_mutex_trylock(&switchMutex) != 0) { return; } SongData *songData = (audioData.currentFileIndex == 0) ? userData.songdataA : userData.songdataB; bool isDeleted = (audioData.currentFileIndex == 0) ? userData.songdataADeleted == true : userData.songdataBDeleted == true; if (!skipping && !isEOFReached() && !isImplSwitchReached()) { if (refresh && songData != NULL && isDeleted == false && songData->hasErrors == false && currentSong != NULL && songData->metadata != NULL) { gint64 length = llround(songData->duration * G_USEC_PER_SEC); // update mpris emitMetadataChanged( songData->metadata->title, songData->metadata->artist, songData->metadata->album, songData->coverArtPath, songData->trackId != NULL ? songData->trackId : "", currentSong, length); } if (isDeleted || songData->hasErrors) songData = NULL; printPlayer(songData, elapsedSeconds, &settings); } pthread_mutex_unlock(&switchMutex); } void handleGoToSong() { if (goingToSong) return; goingToSong = true; if (appState.currentView != LIBRARY_VIEW) { if (digitsPressedCount == 0) { loadedNextSong = true; playlistNeedsUpdate = false; nextSongNeedsRebuilding = false; skipToSong(chosenNodeId); audioData.endOfListReached = false; } else { resetPlaylistDisplay = true; int songNumber = atoi(digitsPressed); memset(digitsPressed, '\0', sizeof(digitsPressed)); digitsPressedCount = 0; playlistNeedsUpdate = false; nextSongNeedsRebuilding = false; skipToNumberedSong(songNumber); } } else { if (audioData.restart) { Node *lastSong = findSelectedEntryById(&playlist, lastPlayedId); startFromTop = false; if (lastSong == NULL) { if (playlist.tail != NULL) lastPlayedId = playlist.tail->id; else { lastPlayedId = -1; startFromTop = true; } } } pthread_mutex_lock(&(playlist.mutex)); enqueueSongs(); pthread_mutex_unlock(&(playlist.mutex)); } goingToSong = false; } void gotoBeginningOfPlaylist() { digitsPressed[0] = 1; digitsPressed[1] = '\0'; digitsPressedCount = 1; handleGoToSong(); } void gotoEndOfPlaylist() { if (digitsPressedCount > 0) { handleGoToSong(); } else { skipToLastSong(); } } void handleInput() { struct Event event = processInput(); switch (event.type) { case EVENT_GOTOBEGINNINGOFPLAYLIST: gotoBeginningOfPlaylist(); break; case EVENT_GOTOENDOFPLAYLIST: gotoEndOfPlaylist(); break; case EVENT_GOTOSONG: handleGoToSong(); break; case EVENT_PLAY_PAUSE: togglePause(&totalPauseSeconds, &pauseSeconds, &pause_time); break; case EVENT_TOGGLEVISUALIZER: toggleVisualizer(&settings); break; case EVENT_TOGGLEREPEAT: toggleRepeat(); break; case EVENT_TOGGLECOVERS: toggleCovers(&settings); break; case EVENT_TOGGLEBLOCKS: toggleBlocks(&settings); break; case EVENT_SHUFFLE: toggleShuffle(); break; case EVENT_TOGGLE_PROFILE_COLORS: toggleColors(&settings); break; case EVENT_QUIT: quit(); break; case EVENT_SCROLLNEXT: scrollNext(); break; case EVENT_SCROLLPREV: scrollPrev(); break; case EVENT_VOLUME_UP: adjustVolumePercent(5); break; case EVENT_VOLUME_DOWN: adjustVolumePercent(-5); break; case EVENT_NEXT: resetPlaylistDisplay = true; skipToNextSong(); break; case EVENT_PREV: resetPlaylistDisplay = true; skipToPrevSong(); break; case EVENT_SEEKBACK: seekBack(); break; case EVENT_SEEKFORWARD: seekForward(); break; case EVENT_ADDTOMAINPLAYLIST: addToSpecialPlaylist(); break; case EVENT_EXPORTPLAYLIST: savePlaylist(settings.path); break; case EVENT_UPDATELIBRARY: updateLibrary(settings.path); break; case EVENT_SHOWKEYBINDINGS: toggleShowKeyBindings(); break; case EVENT_SHOWINFO: toggleShowPlaylist(); break; case EVENT_SHOWLIBRARY: toggleShowLibrary(); break; case EVENT_NEXTPAGE: flipNextPage(); break; case EVENT_PREVPAGE: flipPrevPage(); break; case EVENT_REMOVE: handleRemove(); break; case EVENT_SHOWTRACK: showTrack(); break; default: fastForwarding = false; rewinding = false; break; } eventProcessed = false; } void resize() { alarm(1); // Timer while (resizeFlag) { resizeFlag = 0; c_sleep(100); } alarm(0); // Cancel timer printf("\033[1;1H"); clearScreen(); refresh = true; } void updatePlayer() { struct winsize ws; ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); // check if window has changed size if (ws.ws_col != windowSize.ws_col || ws.ws_row != windowSize.ws_row) { resizeFlag = 1; windowSize = ws; } // resizeFlag can also be set by handleResize if (resizeFlag) resize(); else { refreshPlayer(); } } void loadAudioData() { loadingAudioData = true; if (audioData.restart == true) { if (playlist.head != NULL && (waitingForPlaylist || waitingForNext)) { songLoading = true; if (waitingForPlaylist) { currentSong = playlist.head; } else if (waitingForNext) { if (lastPlayedId >= 0) { currentSong = findSelectedEntryById(&playlist, lastPlayedId); if (currentSong != NULL && currentSong->next != NULL) currentSong = currentSong->next; } if (currentSong == NULL) { if (startFromTop) { currentSong = playlist.head; startFromTop = false; } else currentSong = playlist.tail; } } audioData.restart = false; waitingForPlaylist = false; waitingForNext = false; int res = loadFirst(currentSong); if (res >= 0) { res = createAudioDevice(&userData); } if (res >= 0) { resumePlayback(); } else { setEndOfListReached(); } playlistNeedsUpdate = false; loadedNextSong = false; nextSong = NULL; refresh = true; clock_gettime(CLOCK_MONOTONIC, &start_time); } } else if (currentSong != NULL && (nextSongNeedsRebuilding || nextSong == NULL) && !songLoading) { songLoading = true; nextSongNeedsRebuilding = false; tryNextSong = currentSong->next; loadingdata.loadA = !usingSongDataA; nextSong = getListNext(currentSong); loadingdata.loadingFirstDecoder = false; loadSong(nextSong, &loadingdata); } loadingAudioData = false; } void tryLoadNext() { songHasErrors = false; clearingErrors = true; if (tryNextSong == NULL && currentSong != NULL) tryNextSong = currentSong->next; else if (tryNextSong != NULL) tryNextSong = tryNextSong->next; if (tryNextSong != NULL) { songLoading = true; loadingdata.loadA = !usingSongDataA; loadingdata.loadingFirstDecoder = false; loadSong(tryNextSong, &loadingdata); } else { clearingErrors = false; } } gboolean mainloop_callback(gpointer data) { (void)data; calcElapsedTime(); handleInput(); // Process GDBus events in the global_main_context while (g_main_context_pending(global_main_context)) { g_main_context_iteration(global_main_context, FALSE); } updatePlayer(); if (playlist.head != NULL) { if (loadingAudioData == false && (!loadedNextSong || nextSongNeedsRebuilding) && !audioData.endOfListReached) { loadAudioData(); } if (songHasErrors) tryLoadNext(); if (isPlaybackDone()) { updateLastSongSwitchTime(); prepareNextSong(); if (!doQuit) switchAudioImplementation(); } } if (doQuit) { g_main_loop_quit(main_loop); return FALSE; } return TRUE; } static gboolean on_sigint(gpointer user_data) { doQuit = true; GMainLoop *loop = (GMainLoop *)user_data; g_main_loop_quit(loop); return G_SOURCE_REMOVE; // Remove the signal source } void play(Node *song) { updateLastInputTime(); updateLastSongSwitchTime(); int res = 0; if (song != NULL) { audioData.currentFileIndex = 0; loadingdata.loadA = true; res = loadFirst(song); if (res >= 0) { res = createAudioDevice(&userData); } if (res < 0) setEndOfListReached(); } if (song == NULL || res < 0) { song = NULL; waitingForPlaylist = true; } loadedNextSong = false; nextSong = NULL; refresh = true; clock_gettime(CLOCK_MONOTONIC, &start_time); main_loop = g_main_loop_new(NULL, FALSE); g_unix_signal_add(SIGINT, on_sigint, main_loop); if (song != NULL) emitStartPlayingMpris(); else emitPlaybackStoppedMpris(); g_timeout_add(100, mainloop_callback, NULL); g_main_loop_run(main_loop); g_main_loop_unref(main_loop); } void cleanupOnExit() { pthread_mutex_lock(&dataSourceMutex); resetDecoders(); resetVorbisDecoders(); resetOpusDecoders(); resetM4aDecoders(); cleanupPlaybackDevice(); cleanupAudioContext(); emitPlaybackStoppedMpris(); resetConsole(); if (library == NULL || library->children == NULL) { printf("No Music found.\n"); printf("Please make sure the path is set correctly. \n"); printf("To set it type: kew path \"/path/to/Music\". \n"); } if (!userData.songdataADeleted) { userData.songdataADeleted = true; unloadSongData(&loadingdata.songdataA); } if (!userData.songdataBDeleted) { userData.songdataBDeleted = true; unloadSongData(&loadingdata.songdataB); } cleanupMpris(); restoreTerminalMode(); enableInputBuffering(); setConfig(&settings); saveSpecialPlaylist(settings.path, playingMainPlaylist); freeAudioBuffer(); deleteCache(tempCache); deleteTempDir(); freeMainDirectoryTree(); deletePlaylist(&playlist); deletePlaylist(originalPlaylist); deletePlaylist(specialPlaylist); free(specialPlaylist); free(originalPlaylist); setDefaultTextColor(); showCursor(); pthread_mutex_destroy(&(loadingdata.mutex)); pthread_mutex_destroy(&(playlist.mutex)); pthread_mutex_destroy(&(switchMutex)); pthread_mutex_unlock(&dataSourceMutex); pthread_mutex_destroy(&(dataSourceMutex)); #ifdef DEBUG fclose(logFile); #endif if (freopen("/dev/stderr", "w", stderr) == NULL) { perror("freopen error"); } } void run() { if (originalPlaylist == NULL) { originalPlaylist = malloc(sizeof(PlayList)); *originalPlaylist = deepCopyPlayList(&playlist); } if (playlist.head == NULL) { appState.currentView = LIBRARY_VIEW; } initMpris(); currentSong = playlist.head; play(currentSong); clearScreen(); fflush(stdout); } void init() { disableInputBuffering(); srand(time(NULL)); initResize(); ioctl(STDOUT_FILENO, TIOCGWINSZ, &windowSize); enableScrolling(); setNonblockingMode(); tempCache = createCache(); c_strcpy(loadingdata.filePath, sizeof(loadingdata.filePath), ""); loadingdata.songdataA = NULL; loadingdata.songdataB = NULL; loadingdata.loadA = true; loadingdata.loadingFirstDecoder = true; audioData.restart = true; userData.songdataADeleted = true; userData.songdataBDeleted = true; initAudioBuffer(); initVisuals(); pthread_mutex_init(&dataSourceMutex, NULL); pthread_mutex_init(&switchMutex, NULL); pthread_mutex_init(&(loadingdata.mutex), NULL); pthread_mutex_init(&(playlist.mutex), NULL); nerdFontsEnabled = hasNerdFonts(); fflush(stdout); createLibrary(&settings); #ifdef DEBUG g_setenv("G_MESSAGES_DEBUG", "all", TRUE); logFile = freopen("error.log", "w", stderr); if (logFile == NULL) { fprintf(stdout, "Failed to redirect stderr to error.log\n"); } #else FILE *nullStream = freopen("/dev/null", "w", stderr); (void)nullStream; #endif } void openLibrary() { appState.currentView = LIBRARY_VIEW; init(); playlist.head = NULL; run(); } void playSpecialPlaylist() { if (specialPlaylist->count == 0) { printf("Couldn't find any songs in the special playlist. Add a song by pressing '.' while it's playing. \n"); exit(0); } playingMainPlaylist = true; init(); deepCopyPlayListOntoList(specialPlaylist, &playlist); shufflePlaylist(&playlist); run(); } void playAll() { init(); createPlayListFromFileSystemEntry(library, &playlist, MAX_FILES); if (playlist.count == 0) { exit(0); } shufflePlaylist(&playlist); run(); } void removeArgElement(char *argv[], int index, int *argc) { if (index < 0 || index >= *argc) { // Invalid index return; } // Shift elements after the index for (int i = index; i < *argc - 1; i++) { argv[i] = argv[i + 1]; } // Update the argument count (*argc)--; } void handleOptions(int *argc, char *argv[]) { const char *noUiOption = "--noui"; const char *noCoverOption = "--nocover"; const char *quitOnStop = "--quitonstop"; const char *quitOnStop2 = "-q"; const char *exactOption = "--exact"; const char *exactOption2 = "-e"; int idx = -1; for (int i = 0; i < *argc; i++) { if (c_strcasestr(argv[i], noUiOption)) { uiEnabled = false; idx = i; } } if (idx >= 0) removeArgElement(argv, idx, argc); idx = -1; for (int i = 0; i < *argc; i++) { if (c_strcasestr(argv[i], noCoverOption)) { coverEnabled = false; idx = i; } } if (idx >= 0) removeArgElement(argv, idx, argc); idx = -1; for (int i = 0; i < *argc; i++) { if (c_strcasestr(argv[i], quitOnStop) || c_strcasestr(argv[i], quitOnStop2)) { quitAfterStopping = true; idx = i; } } if (idx >= 0) removeArgElement(argv, idx, argc); idx = -1; for (int i = 0; i < *argc; i++) { if (c_strcasestr(argv[i], exactOption) || c_strcasestr(argv[i], exactOption2)) { exactSearch = true; idx = i; } } if (idx >= 0) removeArgElement(argv, idx, argc); } int main(int argc, char *argv[]) { if ((argc == 2 && ((strcmp(argv[1], "--help") == 0) || (strcmp(argv[1], "-h") == 0) || (strcmp(argv[1], "-?") == 0)))) { showHelp(); exit(0); } else if (argc == 2 && (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-v") == 0)) { printAbout(NULL); exit(0); } getConfig(&settings); if (argc == 3 && (strcmp(argv[1], "path") == 0)) { c_strcpy(settings.path, sizeof(settings.path), argv[2]); setConfig(&settings); exit(0); } if (settings.path[0] == '\0') { printf("Please make sure the path is set correctly. \n"); printf("To set it type: kew path \"/path/to/Music\". \n"); exit(0); } atexit(cleanupOnExit); handleOptions(&argc, argv); loadSpecialPlaylist(settings.path); if (argc == 1) { openLibrary(); } else if (argc == 2 && strcmp(argv[1], "all") == 0) { playAll(); } else if (argc == 2 && strcmp(argv[1], ".") == 0) { playSpecialPlaylist(); } else if (argc >= 2) { init(); makePlaylist(argc, argv, exactSearch, settings.path); if (playlist.count == 0) exit(0); run(); } return 0; } kew-2.4.2/src/m4a.h000066400000000000000000000612141457005474400137440ustar00rootroot00000000000000 /* This implements a data source that decodes m4a streams via FFmpeg This object can be plugged into any `ma_data_source_*()` API and can also be used as a custom decoding backend. See the custom_decoder example. You need to include this file after miniaudio.h. */ #ifndef m4a_h #define m4a_h #ifdef __cplusplus extern "C" { #endif #include #include #include #include #include #include #include #include #include typedef struct { ma_data_source_base ds; /* The m4a decoder can be used independently as a data source. */ ma_read_proc onRead; ma_seek_proc onSeek; ma_tell_proc onTell; void *pReadSeekTellUserData; ma_format format; FILE *mf; // FFmpeg related fields... AVCodecContext *codec_context; SwrContext *swr_ctx; AVFormatContext *format_context; ma_uint64 cursor; ma_uint32 sampleSize; int bitDepth; } m4a_decoder; MA_API ma_result m4a_decoder_init(ma_read_proc onRead, ma_seek_proc onSeek, ma_tell_proc onTell, void *pReadSeekTellUserData, const ma_decoding_backend_config *pConfig, const ma_allocation_callbacks *pAllocationCallbacks, m4a_decoder *pM4a); MA_API ma_result m4a_decoder_init_file(const char *pFilePath, const ma_decoding_backend_config *pConfig, const ma_allocation_callbacks *pAllocationCallbacks, m4a_decoder *pM4a); MA_API void m4a_decoder_uninit(m4a_decoder *pM4a, const ma_allocation_callbacks *pAllocationCallbacks); MA_API ma_result m4a_decoder_read_pcm_frames(m4a_decoder *pM4a, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead); MA_API ma_result m4a_decoder_seek_to_pcm_frame(m4a_decoder *pM4a, ma_uint64 frameIndex); MA_API ma_result m4a_decoder_get_data_format(m4a_decoder *pM4a, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap); MA_API ma_result m4a_decoder_get_cursor_in_pcm_frames(m4a_decoder *pM4a, ma_uint64 *pCursor); MA_API ma_result m4a_decoder_get_length_in_pcm_frames(m4a_decoder *pM4a, ma_uint64 *pLength); #ifdef __cplusplus } #endif #if defined(MINIAUDIO_IMPLEMENTATION) || defined(MA_IMPLEMENTATION) #define MAX_CHANNELS 2 #define MAX_SAMPLES 4800 // Maximum expected frame size #define MAX_SAMPLE_SIZE 4 static uint8_t leftoverBuffer[MAX_SAMPLES * MAX_CHANNELS * MAX_SAMPLE_SIZE]; static ma_uint64 leftoverSampleCount = 0; extern ma_result m4a_decoder_ds_get_data_format(ma_data_source *pDataSource, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap); ma_result m4a_decoder_ds_read(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { return m4a_decoder_read_pcm_frames((m4a_decoder *)pDataSource, pFramesOut, frameCount, pFramesRead); } ma_result m4a_decoder_ds_seek(ma_data_source *pDataSource, ma_uint64 frameIndex) { return m4a_decoder_seek_to_pcm_frame((m4a_decoder *)pDataSource, frameIndex); } ma_result m4a_decoder_ds_get_data_format(ma_data_source *pDataSource, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap) { return m4a_decoder_get_data_format((m4a_decoder *)pDataSource, pFormat, pChannels, pSampleRate, pChannelMap, channelMapCap); } ma_result m4a_decoder_ds_get_cursor(ma_data_source *pDataSource, ma_uint64 *pCursor) { return m4a_decoder_get_cursor_in_pcm_frames((m4a_decoder *)pDataSource, pCursor); } ma_result m4a_decoder_ds_get_length(ma_data_source *pDataSource, ma_uint64 *pLength) { return m4a_decoder_get_length_in_pcm_frames((m4a_decoder *)pDataSource, pLength); } ma_data_source_vtable g_m4a_decoder_ds_vtable = { m4a_decoder_ds_read, m4a_decoder_ds_seek, m4a_decoder_ds_get_data_format, m4a_decoder_ds_get_cursor, m4a_decoder_ds_get_length, NULL, (ma_uint64)0}; // Custom FFmpeg read function wrapper int m4a_ffmpeg_read(void *opaque, uint8_t *buf, int buf_size) { m4a_decoder *pM4a = (m4a_decoder *)opaque; size_t bytesRead = 0; ma_result result = pM4a->onRead(pM4a->pReadSeekTellUserData, buf, buf_size, &bytesRead); if (result == MA_SUCCESS) { return bytesRead; } else { switch (result) { case MA_IO_ERROR: return AVERROR(EIO); case MA_INVALID_ARGS: return AVERROR(EINVAL); default: return AVERROR(EIO); } } } int64_t m4a_ffmpeg_seek(void *opaque, int64_t offset, int whence) { m4a_decoder *pM4a = (m4a_decoder *)opaque; if (whence == AVSEEK_SIZE) { if (pM4a->onTell) { ma_int64 fileSize; ma_result result = pM4a->onTell(pM4a->pReadSeekTellUserData, &fileSize); return result != MA_SUCCESS ? AVERROR(result) : fileSize; } else { return AVERROR(ENOSYS); } } ma_seek_origin origin = ma_seek_origin_start; switch (whence) { case SEEK_SET: origin = ma_seek_origin_start; break; case SEEK_CUR: origin = ma_seek_origin_current; break; case SEEK_END: origin = ma_seek_origin_end; break; default: return AVERROR(EINVAL); } if (pM4a->onSeek) { ma_result result = pM4a->onSeek(pM4a->pReadSeekTellUserData, offset, origin); if (result != MA_SUCCESS) { return AVERROR(result); } if (whence != SEEK_CUR || offset != 0) { // If the position really changed ma_int64 newPosition; result = pM4a->onTell(pM4a->pReadSeekTellUserData, &newPosition); return result != MA_SUCCESS ? AVERROR(result) : newPosition; } else { ma_int64 currentPosition; result = pM4a->onTell(pM4a->pReadSeekTellUserData, ¤tPosition); return result != MA_SUCCESS ? AVERROR(result) : currentPosition; } } else { return AVERROR(ENOSYS); } } static ma_result m4a_decoder_init_internal(const ma_decoding_backend_config *pConfig, m4a_decoder *pM4a) { if (pM4a == NULL) { return MA_INVALID_ARGS; } MA_ZERO_OBJECT(pM4a); pM4a->format = ma_format_f32; if (pConfig != NULL && (pConfig->preferredFormat == ma_format_f32 || pConfig->preferredFormat == ma_format_s16)) { pM4a->format = pConfig->preferredFormat; } ma_data_source_config dataSourceConfig = ma_data_source_config_init(); dataSourceConfig.vtable = &g_m4a_decoder_ds_vtable; ma_result result = ma_data_source_init(&dataSourceConfig, &pM4a->ds); if (result != MA_SUCCESS) { return result; } return MA_SUCCESS; } ma_format ffmpeg_to_mini_al_format(enum AVSampleFormat ffmpeg_sample_fmt) { switch (ffmpeg_sample_fmt) { case AV_SAMPLE_FMT_FLTP: case AV_SAMPLE_FMT_FLT: return ma_format_f32; case AV_SAMPLE_FMT_S16: case AV_SAMPLE_FMT_S16P: return ma_format_s16; default: return ma_format_unknown; } } // Note: This isn't used by kew and is untested MA_API ma_result m4a_decoder_init( ma_read_proc onRead, ma_seek_proc onSeek, ma_tell_proc onTell, void *pReadSeekTellUserData, const ma_decoding_backend_config *pConfig, const ma_allocation_callbacks *pAllocationCallbacks, m4a_decoder *pM4a) { (void)onTell; (void)pAllocationCallbacks; if (pM4a == NULL || onRead == NULL || onSeek == NULL) { return MA_INVALID_ARGS; } ma_result result = m4a_decoder_init_internal(pConfig, pM4a); if (result != MA_SUCCESS) { return result; } unsigned char *avio_ctx_buffer = NULL; size_t avio_ctx_buffer_size = 4096; AVIOContext *avio_ctx = avio_alloc_context( avio_ctx_buffer, avio_ctx_buffer_size, 0, pReadSeekTellUserData, m4a_ffmpeg_read, NULL, m4a_ffmpeg_seek); if (avio_ctx == NULL) { return MA_OUT_OF_MEMORY; } avio_ctx_buffer = (unsigned char *)av_malloc(avio_ctx_buffer_size); if (avio_ctx_buffer == NULL) { avio_context_free(&avio_ctx); return MA_OUT_OF_MEMORY; } // Initialize FFmpeg's AVFormatContext with the custom I/O context pM4a->format_context = avformat_alloc_context(); if (pM4a->format_context == NULL) { av_free(avio_ctx_buffer); avio_context_free(&avio_ctx); return MA_OUT_OF_MEMORY; } pM4a->format_context->pb = avio_ctx; return MA_SUCCESS; } MA_API ma_result m4a_decoder_init_file(const char *pFilePath, const ma_decoding_backend_config *pConfig, const ma_allocation_callbacks *pAllocationCallbacks, m4a_decoder *pM4a) { (void)pAllocationCallbacks; if (pFilePath == NULL || pM4a == NULL) { return MA_INVALID_ARGS; } ma_result result = m4a_decoder_init_internal(pConfig, pM4a); if (result != MA_SUCCESS) { return result; } // Initialize libavformat and libavcodec AVFormatContext *format_context = NULL; if (avformat_open_input(&format_context, pFilePath, NULL, NULL) != 0) { return MA_INVALID_FILE; } if (avformat_find_stream_info(format_context, NULL) < 0) { avformat_close_input(&format_context); return MA_ERROR; } const AVCodec *decoder = NULL; int stream_index = av_find_best_stream(format_context, AVMEDIA_TYPE_AUDIO, -1, -1, &decoder, 0); if (stream_index < 0) { avformat_close_input(&format_context); return MA_ERROR; } AVStream *audio_stream = format_context->streams[stream_index]; AVCodecContext *codec_context = avcodec_alloc_context3(decoder); if (!codec_context) { avformat_close_input(&format_context); return MA_OUT_OF_MEMORY; } if (avcodec_parameters_to_context(codec_context, audio_stream->codecpar) < 0) { avcodec_free_context(&codec_context); avformat_close_input(&format_context); return MA_ERROR; } if (avcodec_open2(codec_context, decoder, NULL) < 0) { avcodec_free_context(&codec_context); avformat_close_input(&format_context); return MA_ERROR; } pM4a->codec_context = codec_context; switch (pM4a->codec_context->sample_fmt) { case AV_SAMPLE_FMT_S16: case AV_SAMPLE_FMT_S16P: pM4a->sampleSize = sizeof(int16_t); // 16-bit samples break; case AV_SAMPLE_FMT_FLTP: case AV_SAMPLE_FMT_FLT: pM4a->sampleSize = sizeof(float); // 32-bit float samples break; default: pM4a->sampleSize = 0; break; } pM4a->format_context = format_context; pM4a->mf = NULL; pM4a->format = ffmpeg_to_mini_al_format(pM4a->codec_context->sample_fmt); return MA_SUCCESS; } MA_API void m4a_decoder_uninit(m4a_decoder *pM4a, const ma_allocation_callbacks *pAllocationCallbacks) { if (pM4a == NULL) { return; } (void)pAllocationCallbacks; if (pM4a->swr_ctx != NULL) { swr_free(&pM4a->swr_ctx); } if (pM4a->codec_context != NULL) { avcodec_free_context(&pM4a->codec_context); } if (pM4a->format_context != NULL) { avformat_close_input(&pM4a->format_context); } if (pM4a->mf != NULL) { fclose(pM4a->mf); pM4a->mf = NULL; } ma_data_source_uninit(&pM4a->ds); } ma_result m4a_decoder_read_pcm_frames(m4a_decoder *pM4a, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { if (pM4a == NULL || pM4a->onRead == NULL || pM4a->onSeek == NULL || pM4a->sampleSize == 0 || pFramesOut == NULL || frameCount == 0) { return MA_INVALID_ARGS; } ma_result result = MA_SUCCESS; ma_format format; ma_uint32 channels; ma_uint32 sampleRate; m4a_decoder_get_data_format(pM4a, &format, &channels, &sampleRate, NULL, 0); // only two channels supported for now if (channels > 2) { return MA_ERROR; } int stream_index = av_find_best_stream(pM4a->format_context, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); if (stream_index < 0) { return MA_ERROR; } AVFrame *frame = av_frame_alloc(); if (!frame) { return MA_ERROR; } AVPacket packet; ma_uint64 totalFramesProcessed = 0; if (leftoverSampleCount > 0) { int leftoverToProcess = (leftoverSampleCount < frameCount) ? leftoverSampleCount : frameCount; int leftoverBytes = leftoverToProcess * channels * pM4a->sampleSize; memcpy(pFramesOut, leftoverBuffer, leftoverBytes); totalFramesProcessed += leftoverToProcess; int bytesToShift = (leftoverSampleCount - leftoverToProcess) * channels * pM4a->sampleSize; uint8_t *shiftSource = leftoverBuffer + leftoverToProcess * channels * pM4a->sampleSize; memmove(leftoverBuffer, shiftSource, bytesToShift); leftoverSampleCount -= leftoverToProcess; } while (totalFramesProcessed < frameCount) { if (av_read_frame(pM4a->format_context, &packet) < 0) { // Error in reading frame or EOF result = MA_AT_END; break; } if (packet.stream_index == stream_index) { if (avcodec_send_packet(pM4a->codec_context, &packet) == 0) { while (totalFramesProcessed < frameCount && avcodec_receive_frame(pM4a->codec_context, frame) == 0) { int samplesToProcess = (frame->nb_samples < (int)(frameCount - totalFramesProcessed)) ? frame->nb_samples : (int)(frameCount - totalFramesProcessed); int outputBufferLen = samplesToProcess * channels * pM4a->sampleSize; uint8_t *output_buffer = (uint8_t *)malloc(outputBufferLen); if (!output_buffer) { av_frame_free(&frame); return MA_ERROR; } for (int i = 0; i < samplesToProcess; i++) { for (ma_uint32 c = 0; c < channels; c++) { int byteOffset = (i * channels + c) * pM4a->sampleSize; if (frame->extended_data == NULL || frame->extended_data[c] == NULL) { continue; } if (pM4a->format == ma_format_s16) { if (pM4a->codec_context->sample_fmt != AV_SAMPLE_FMT_S16) { continue; } int16_t sample = ((int16_t *)frame->extended_data[c])[i]; memcpy(output_buffer + byteOffset, &sample, sizeof(int16_t)); } else { float sample = ((float *)frame->extended_data[c])[i]; memcpy(output_buffer + byteOffset, &sample, sizeof(float)); } } } int current_frame_buffer_size = samplesToProcess * channels * pM4a->sampleSize; memcpy((uint8_t *)pFramesOut + totalFramesProcessed * channels * pM4a->sampleSize, output_buffer, current_frame_buffer_size); totalFramesProcessed += samplesToProcess; // Check if there are leftovers if (samplesToProcess < frame->nb_samples) { int remainingSamples = frame->nb_samples - samplesToProcess; for (int i = samplesToProcess; i < frame->nb_samples; i++) { for (ma_uint32 c = 0; c < channels; c++) { int byteOffset = ((i - samplesToProcess) * channels + c) * pM4a->sampleSize; memcpy(leftoverBuffer + byteOffset, (uint8_t *)frame->extended_data[c] + i * pM4a->sampleSize, pM4a->sampleSize); } } leftoverSampleCount = remainingSamples; } free(output_buffer); } } } av_packet_unref(&packet); } av_frame_free(&frame); pM4a->cursor += totalFramesProcessed; if (pFramesRead != NULL) { *pFramesRead = totalFramesProcessed; } return result; } MA_API ma_result m4a_decoder_seek_to_pcm_frame(m4a_decoder *pM4a, ma_uint64 frameIndex) { if (pM4a == NULL || pM4a->codec_context == NULL || pM4a->format_context == NULL) { return MA_INVALID_ARGS; } AVStream *stream = NULL; for (unsigned int i = 0; i < pM4a->format_context->nb_streams; i++) { if (pM4a->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { stream = pM4a->format_context->streams[i]; break; } } if (stream == NULL) { return MA_ERROR; } // Convert frame index to the stream's time base. int64_t timestamp = av_rescale_q(frameIndex, (AVRational){1, pM4a->codec_context->sample_rate}, stream->time_base); if (av_seek_frame(pM4a->format_context, stream->index, timestamp, AVSEEK_FLAG_BACKWARD) < 0) { return MA_ERROR; } // After seeking, we must clear the codec's internal buffer. avcodec_flush_buffers(pM4a->codec_context); return MA_SUCCESS; } MA_API ma_result m4a_decoder_get_data_format( m4a_decoder *pM4a, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap) { if (pFormat != NULL) { *pFormat = ma_format_unknown; } if (pChannels != NULL) { *pChannels = 0; } if (pSampleRate != NULL) { *pSampleRate = 0; } if (pChannelMap != NULL) { MA_ZERO_MEMORY(pChannelMap, sizeof(*pChannelMap) * channelMapCap); } if (pM4a == NULL || pM4a->codec_context == NULL) { return MA_INVALID_OPERATION; } if (pFormat != NULL) { *pFormat = ffmpeg_to_mini_al_format(pM4a->codec_context->sample_fmt); } if (pChannels != NULL) { for (unsigned int i = 0; i < pM4a->format_context->nb_streams; i++) { if (pM4a->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { #if (LIBAVCODEC_VERSION_MAJOR > 59) || ((LIBAVCODEC_VERSION_MAJOR == 59) && (LIBAVCODEC_VERSION_MINOR > 24)) *pChannels = pM4a->format_context->streams[i]->codecpar->ch_layout.nb_channels; #else *pChannels = pM4a->format_context->streams[i]->codecpar->channels; #endif } } } if (pSampleRate != NULL) { *pSampleRate = pM4a->codec_context->sample_rate; } if (pChannelMap != NULL) { ma_channel_map_init_standard(ma_standard_channel_map_microsoft, pChannelMap, channelMapCap, *pChannels); } return MA_SUCCESS; } MA_API ma_result m4a_decoder_get_cursor_in_pcm_frames(m4a_decoder *pM4a, ma_uint64 *pCursor) { if (pCursor == NULL) { return MA_INVALID_ARGS; } *pCursor = 0; /* Safety. */ if (pM4a == NULL || pM4a->format_context == NULL || pM4a->codec_context == NULL) { return MA_INVALID_ARGS; } *pCursor = pM4a->cursor; return MA_SUCCESS; } // Note: This returns an approximation MA_API ma_result m4a_decoder_get_length_in_pcm_frames(m4a_decoder *pM4a, ma_uint64 *pLength) { if (pLength == NULL) { return MA_INVALID_ARGS; } *pLength = 0; // Safety. if (pM4a == NULL || pM4a->format_context == NULL || pM4a->codec_context == NULL) { return MA_INVALID_ARGS; } AVStream *audio_stream = NULL; for (unsigned int i = 0; i < pM4a->format_context->nb_streams; i++) { if (pM4a->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { audio_stream = pM4a->format_context->streams[i]; break; } } if (audio_stream == NULL) { return MA_ERROR; } // Use duration and time base to calculate total number of frames if (audio_stream->duration != AV_NOPTS_VALUE) { int64_t duration_ts = audio_stream->duration; AVRational time_base = audio_stream->time_base; AVRational target_time_base = {1, pM4a->codec_context->sample_rate}; *pLength = av_rescale_q(duration_ts, time_base, target_time_base); return MA_SUCCESS; } return MA_ERROR; } #endif #endif kew-2.4.2/src/mpris.c000066400000000000000000001207471457005474400144170ustar00rootroot00000000000000#include "mpris.h" /* mpris.c Functions related to mpris implementation. */ GMainContext *global_main_context = NULL; GMainLoop *main_loop; guint registration_id; guint player_registration_id; guint bus_name_id; static const gchar *LoopStatus = "None"; static gdouble Rate = 1.0; static gdouble Volume = 0.5; static gdouble MinimumRate = 1.0; static gdouble MaximumRate = 1.0; static gboolean CanGoNext = TRUE; static gboolean CanGoPrevious = TRUE; static gboolean CanPlay = TRUE; static gboolean CanPause = TRUE; static gboolean CanSeek = FALSE; static gboolean CanControl = TRUE; void updatePlaybackStatus(const gchar *status) { GVariant *status_variant = g_variant_new_string(status); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "PlaybackStatus", g_variant_new("(s)", status_variant), NULL); g_variant_unref(status_variant); } const gchar *introspection_xml = "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"; static const gchar *identity = "kew"; static const gchar *desktopIconName = ""; // Without file extension static const gchar *desktopEntry = ""; // The name of your .desktop file static void handle_raise(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)invocation; (void)user_data; } static void handle_quit(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)invocation; (void)user_data; quit(); } static gboolean get_identity(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_string(identity); return TRUE; } static gboolean get_desktop_entry(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_string(desktopEntry); return TRUE; } static gboolean get_desktop_icon_name(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_string(desktopIconName); return TRUE; } static void handle_next(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)user_data; skipToNextSong(); g_dbus_method_invocation_return_value(invocation, NULL); } static void handle_previous(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)user_data; skipToPrevSong(); g_dbus_method_invocation_return_value(invocation, NULL); } static void handle_pause(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)invocation; (void)user_data; playbackPause(&pause_time); } static void handle_play_pause(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)user_data; togglePause(&totalPauseSeconds, &pauseSeconds, &pause_time); g_dbus_method_invocation_return_value(invocation, NULL); } static void handle_stop(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)user_data; quit(); g_dbus_method_invocation_return_value(invocation, NULL); } static void handle_play(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)invocation; (void)user_data; playbackPlay(&totalPauseSeconds, &pauseSeconds); } static void handle_seek(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)invocation; (void)user_data; } static void handle_set_position(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)method_name; (void)parameters; (void)invocation; (void)user_data; } static void handle_method_call(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { if (g_strcmp0(method_name, "PlayPause") == 0) { handle_play_pause(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Next") == 0) { handle_next(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Previous") == 0) { handle_previous(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Pause") == 0) { handle_pause(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Stop") == 0) { handle_stop(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Play") == 0) { handle_play(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Seek") == 0) { handle_seek(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "SetPosition") == 0) { handle_set_position(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Raise") == 0) { handle_raise(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else if (g_strcmp0(method_name, "Quit") == 0) { handle_quit(connection, sender, object_path, interface_name, method_name, parameters, invocation, user_data); } else { g_dbus_method_invocation_return_dbus_error(invocation, "org.freedesktop.DBus.Error.UnknownMethod", "No such method"); } } static void on_bus_name_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) { (void)connection; (void)name; (void)user_data; } static void on_bus_name_lost(GDBusConnection *connection, const gchar *name, gpointer user_data) { (void)connection; (void)name; (void)user_data; } static gboolean get_playback_status(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; const gchar *status = "Stopped"; if (isPaused()) { status = "Paused"; } else if (currentSong == NULL) { status = "Stopped"; } else { status = "Playing"; } *value = g_variant_new_string(status); return TRUE; } static gboolean get_loop_status(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_string(LoopStatus); return TRUE; } static gboolean get_rate(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_double(Rate); return TRUE; } static gboolean get_shuffle(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_boolean(isShuffleEnabled() ? TRUE : FALSE); return TRUE; } static gboolean get_metadata(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; SongData *currentSongData = getCurrentSongData(); GVariantBuilder metadata_builder; g_variant_builder_init(&metadata_builder, G_VARIANT_TYPE_DICTIONARY); if (currentSong != NULL && currentSongData != NULL && currentSongData->metadata != NULL) { g_variant_builder_add(&metadata_builder, "{sv}", "xesam:title", g_variant_new_string(currentSongData->metadata->title)); // Build list of strings for artist const gchar *artistList[2]; if (currentSongData->metadata->artist[0] != '\0') { artistList[0] = currentSongData->metadata->artist; artistList[1] = NULL; } else { artistList[0] = ""; artistList[1] = NULL; } gchar *coverArtUrl = g_strdup_printf("file://%s", currentSongData->coverArtPath); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:artist", g_variant_new_strv(artistList, -1)); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:album", g_variant_new_string(currentSongData->metadata->album)); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:contentCreated", g_variant_new_string(currentSongData->metadata->date)); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:artUrl", g_variant_new_string(coverArtUrl)); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:trackid", g_variant_new_object_path(currentSongData->trackId)); gint64 length = llround(currentSongData->duration * G_USEC_PER_SEC); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:length", g_variant_new_int64(length)); g_free(coverArtUrl); } else { g_variant_builder_add(&metadata_builder, "{sv}", "xesam:title", g_variant_new_string("")); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:artist", g_variant_new_strv((const gchar *[]){"", NULL}, -1)); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:album", g_variant_new_string("")); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:contentCreated", g_variant_new_string("")); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:artUrl", g_variant_new_string("")); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:trackid", g_variant_new_object_path("/org/mpris/MediaPlayer2/TrackList/NoTrack")); gint64 placeholderLength = 0; g_variant_builder_add(&metadata_builder, "{sv}", "mpris:length", g_variant_new_int64(placeholderLength)); } GVariant *metadata_variant = g_variant_builder_end(&metadata_builder); *value = g_variant_ref_sink(metadata_variant); return TRUE; } static gboolean get_volume(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; Volume = (gdouble)getCurrentVolume(); *value = g_variant_new_double(Volume); return TRUE; } static gboolean get_position(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; // Convert elapsedSeconds from milliseconds to microseconds gint64 positionMicroseconds = llround(elapsedSeconds * G_USEC_PER_SEC); *value = g_variant_new_int64(positionMicroseconds); return TRUE; } static gboolean get_minimum_rate(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_double(MinimumRate); return TRUE; } static gboolean get_maximum_rate(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_double(MaximumRate); return TRUE; } static gboolean get_can_go_next(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; CanGoNext = (currentSong == NULL || currentSong->next != NULL) ? TRUE : FALSE; *value = g_variant_new_boolean(CanGoNext); return TRUE; } static gboolean get_can_go_previous(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; CanGoPrevious = (currentSong == NULL || currentSong->prev != NULL) ? TRUE : FALSE; *value = g_variant_new_boolean(CanGoPrevious); return TRUE; } static gboolean get_can_play(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_boolean(CanPlay); return TRUE; } static gboolean get_can_pause(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_boolean(CanPause); return TRUE; } static gboolean get_can_seek(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_boolean(CanSeek); return TRUE; } static gboolean get_can_control(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant **value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)error; (void)user_data; *value = g_variant_new_boolean(CanControl); return TRUE; } static GVariant *get_property_callback(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GError **error, gpointer user_data) { GVariant *value = NULL; if (g_strcmp0(property_name, "PlaybackStatus") == 0) { get_playback_status(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "LoopStatus") == 0) { get_loop_status(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Rate") == 0) { get_rate(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Shuffle") == 0) { get_shuffle(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Metadata") == 0) { get_metadata(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Volume") == 0) { get_volume(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Position") == 0) { get_position(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "MinimumRate") == 0) { get_minimum_rate(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "MaximumRate") == 0) { get_maximum_rate(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanGoNext") == 0) { get_can_go_next(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanGoPrevious") == 0) { get_can_go_previous(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanPlay") == 0) { get_can_play(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanPause") == 0) { get_can_pause(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanSeek") == 0) { get_can_seek(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "CanControl") == 0) { get_can_control(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "DesktopIconName") == 0) { get_desktop_icon_name(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "DesktopEntry") == 0) { get_desktop_entry(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else if (g_strcmp0(property_name, "Identity") == 0) { get_identity(connection, sender, object_path, interface_name, property_name, &value, error, user_data); } else { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Unknown property"); } // Check if value is NULL and set an error if needed if (value == NULL && error == NULL) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Property value is NULL"); } return value; } static gboolean set_property_callback(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GVariant *value, GError **error, gpointer user_data) { (void)connection; (void)sender; (void)object_path; (void)interface_name; (void)property_name; (void)user_data; if (g_strcmp0(interface_name, "org.mpris.MediaPlayer2.Player") == 0) { if (g_strcmp0(property_name, "PlaybackStatus") == 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Setting PlaybackStatus property not supported"); return FALSE; } else if (g_strcmp0(property_name, "Volume") == 0) { double new_volume; g_variant_get(value, "d", &new_volume); setVolume((int)new_volume); return TRUE; } else if (g_strcmp0(property_name, "LoopStatus") == 0) { toggleRepeat(); return TRUE; } else if (g_strcmp0(property_name, "Shuffle") == 0) { toggleShuffle(); return TRUE; } else if (g_strcmp0(property_name, "Position") == 0) { // gint64 new_position; // g_variant_get(value, "x", &new_position); // set_position(new_position); // return TRUE; g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Setting Position property not supported"); return FALSE; } else { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Setting property not supported"); return FALSE; } } else { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Unknown interface"); return FALSE; } } // MPRIS MediaPlayer2 interface vtable static const GDBusInterfaceVTable media_player_interface_vtable = { .method_call = handle_method_call, // We're using individual method handlers .get_property = get_property_callback, // Handle the property getters individually .set_property = set_property_callback, .padding = { handle_raise, handle_quit}}; // MPRIS Player interface vtable static const GDBusInterfaceVTable player_interface_vtable = { .method_call = handle_method_call, // We're using individual method handlers .get_property = get_property_callback, // Handle the property getters individually .set_property = set_property_callback, .padding = { handle_next, handle_previous, handle_pause, handle_play_pause, handle_stop, handle_play, handle_seek, handle_set_position}}; void emitPlaybackStoppedMpris() { if (connection) { g_dbus_connection_call(connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Set", g_variant_new("(ssv)", "org.mpris.MediaPlayer2.Player", "PlaybackStatus", g_variant_new_string("Stopped")), G_VARIANT_TYPE("(v)"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); } } void cleanupMpris() { if (registration_id > 0) { g_dbus_connection_unregister_object(connection, registration_id); registration_id = -1; } if (player_registration_id > 0) { g_dbus_connection_unregister_object(connection, player_registration_id); player_registration_id = -1; } if (bus_name_id > 0) { g_bus_unown_name(bus_name_id); bus_name_id = -1; } if (connection != NULL) { g_object_unref(connection); connection = NULL; } if (global_main_context != NULL) { g_main_context_unref(global_main_context); global_main_context = NULL; } } void initMpris() { if (global_main_context == NULL) { global_main_context = g_main_context_new(); } GDBusNodeInfo *introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL); connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL); if (!connection) { g_dbus_node_info_unref(introspection_data); g_printerr("Failed to connect to D-Bus\n"); exit(0); } const char *app_name = "org.mpris.MediaPlayer2.kew"; char unique_name[256]; snprintf(unique_name, sizeof(unique_name), "%s%d", app_name, getpid()); GError *error = NULL; bus_name_id = g_bus_own_name_on_connection(connection, unique_name, G_BUS_NAME_OWNER_FLAGS_NONE, on_bus_name_acquired, on_bus_name_lost, NULL, NULL); if (bus_name_id == 0) { printf("Failed to own D-Bus name: %s\n", unique_name); exit(0); } registration_id = g_dbus_connection_register_object( connection, "/org/mpris/MediaPlayer2", introspection_data->interfaces[0], &media_player_interface_vtable, NULL, NULL, &error); if (!registration_id) { g_dbus_node_info_unref(introspection_data); g_printerr("Failed to register media player object: %s\n", error->message); g_error_free(error); exit(0); } player_registration_id = g_dbus_connection_register_object( connection, "/org/mpris/MediaPlayer2", introspection_data->interfaces[1], &player_interface_vtable, NULL, NULL, &error); if (!player_registration_id) { g_dbus_node_info_unref(introspection_data); g_printerr("Failed to register media player object: %s\n", error->message); g_error_free(error); exit(0); } g_dbus_node_info_unref(introspection_data); } void emitStartPlayingMpris() { GVariant *parameters = g_variant_new("(s)", "Playing"); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "PlaybackStatusChanged", parameters, NULL); } void emitMetadataChanged(const gchar *title, const gchar *artist, const gchar *album, const gchar *coverArtPath, const gchar *trackId, Node *currentSong, gint64 length) { gchar *coverArtUrl = g_strdup_printf("file://%s", coverArtPath); GVariantBuilder metadata_builder; g_variant_builder_init(&metadata_builder, G_VARIANT_TYPE_DICTIONARY); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:title", g_variant_new_string(title)); const gchar *artistList[2]; if (artist) { artistList[0] = artist; artistList[1] = NULL; } else { artistList[0] = ""; artistList[1] = NULL; } g_variant_builder_add(&metadata_builder, "{sv}", "xesam:artist", g_variant_new_strv(artistList, -1)); g_variant_builder_add(&metadata_builder, "{sv}", "xesam:album", g_variant_new_string(album)); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:artUrl", g_variant_new_string(coverArtUrl)); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:trackid", g_variant_new_object_path(trackId)); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:length", g_variant_new_int64(length)); GVariantBuilder changed_properties_builder; g_variant_builder_init(&changed_properties_builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&changed_properties_builder, "{sv}", "Metadata", g_variant_builder_end(&metadata_builder)); g_variant_builder_add(&changed_properties_builder, "{sv}", "CanGoPrevious", g_variant_new_boolean((currentSong != NULL && currentSong->prev != NULL))); g_variant_builder_add(&changed_properties_builder, "{sv}", "CanGoNext", g_variant_new_boolean((currentSong != NULL && currentSong->next != NULL))); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changed_properties_builder, NULL), NULL); g_variant_builder_clear(&metadata_builder); g_variant_builder_clear(&changed_properties_builder); g_free(coverArtUrl); } kew-2.4.2/src/mpris.h000066400000000000000000000013351457005474400144130ustar00rootroot00000000000000#ifndef MPRIS_H #define MPRIS_H #include #include #include "playerops.h" #include "playlist.h" #include "sound.h" #include "soundcommon.h" extern GDBusConnection *connection; extern GMainContext *global_main_context; extern GMainLoop *main_loop; void initMpris(void); void emitStringPropertyChanged(const gchar *propertyName, const gchar *newValue); void emitBooleanPropertyChanged(const gchar *propertyName, gboolean newValue); void emitMetadataChanged(const gchar *title, const gchar *artist, const gchar *album, const gchar *coverArtPath, const gchar *trackId, Node *currentSong, gint64 length); void emitStartPlayingMpris(void); void emitPlaybackStoppedMpris(void); void cleanupMpris(void); #endif kew-2.4.2/src/player.c000066400000000000000000001272001457005474400145500ustar00rootroot00000000000000#include "player.h" /* player.c Functions related to printing the player to the screen. */ #ifndef PIXELDATA_STRUCT #define PIXELDATA_STRUCT #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif typedef struct { unsigned char r; unsigned char g; unsigned char b; } PixelData; #endif const char VERSION[] = "2.4.2"; int mainColor = 6; int titleColor = 6; int artistColor = 6; int enqueuedColor = 6; const int ABSOLUTE_MIN_WIDTH = 64; bool visualizerEnabled = true; bool coverEnabled = true; bool hideLogo = false; bool hideHelp = false; bool quitAfterStopping = false; bool coverAnsi = false; bool metaDataEnabled = true; bool timeEnabled = true; bool drewCover = true; bool uiEnabled = true; bool showList = true; bool resetPlaylistDisplay = true; bool useProfileColors = true; bool fastForwarding = false; bool rewinding = false; bool nerdFontsEnabled = true; int numProgressBars = 15; int elapsedBars = 0; int chosenRow = 0; int chosenSong = 0; int startIter = 0; int aboutHeight = 8; int visualizerHeight = 5; int minWidth = ABSOLUTE_MIN_WIDTH; int minHeight = 2; int maxWidth = 0; int coverRow = 0; int preferredWidth = 0; int preferredHeight = 0; int textWidth = 0; int indent = 0; char *tagsPath; double totalDurationSeconds = 0.0; PixelData color = {125, 125, 125}; PixelData lastRowColor = {90, 90, 90}; TagSettings metadata = {}; double pauseSeconds = 0.0; double totalPauseSeconds = 0.0; double seekAccumulatedSeconds = 0.0; int maxListSize = 0; unsigned char defaultColor = 150; int numDirectoryTreeEntries = 0; int numTopLevelSongs = 0; int startLibIter = 0; int maxLibListSize = 0; int chosenLibRow = 0; bool allowChooseSongs = false; FileSystemEntry *currentEntry = NULL; FileSystemEntry *chosenDir = NULL; int libIter = 0; int libSongIter = 0; int libTopLevelSongIter = 0; int chosenNodeId = 0; int cacheLibrary = -1; const char LIBRARY_FILE[] = "kewlibrary"; FileSystemEntry *library = NULL; void setTextColorRGB2(int r, int g, int b) { if (!useProfileColors) setTextColorRGB(r, g, b); } void setColor() { if (useProfileColors) { setDefaultTextColor(); return; } if (color.r == 150 && color.g == 150 && color.b == 150) setDefaultTextColor(); else if (color.r >= 210 && color.g >= 210 && color.b >= 210) { color.r = defaultColor; color.g = defaultColor; color.b = defaultColor; } else { setTextColorRGB2(color.r, color.g, color.b); } } bool hasNerdFonts() { bool nerdFonts = true; if (printf("\uf28b") < 0) { nerdFonts = false; } clearScreen(); fflush(stdout); return nerdFonts; } int calcMetadataHeight() { int term_w, term_h; getTermSize(&term_w, &term_h); if (metadata.title[0] != '\0') { size_t titleLength = strlen(metadata.title); int titleHeight = (int)ceil((float)titleLength / term_w); size_t artistLength = strlen(metadata.artist); int artistHeight = (int)ceil((float)artistLength / term_w); size_t albumLength = strlen(metadata.album); int albumHeight = (int)ceil((float)albumLength / term_w); int yearHeight = 1; return titleHeight + artistHeight + albumHeight + yearHeight; } else { return 4; } } int calcIdealImgSize(int *width, int *height, const int visualizerHeight, const int metatagHeight) { int term_w, term_h; getTermSize(&term_w, &term_h); int timeDisplayHeight = 1; int heightMargin = 2; int minHeight = visualizerHeight + metatagHeight + timeDisplayHeight + heightMargin; *height = term_h - minHeight; *width = ceil(*height * 2); if (*width > term_w) { *width = term_w; *height = floor(*width / 2); } int remainder = *width % 2; if (remainder == 1) { *width -= 1; } *width -= 3; *height -= 2; return 0; } void calcPreferredSize() { minHeight = 2 + (visualizerEnabled ? visualizerHeight : 0); calcIdealImgSize(&preferredWidth, &preferredHeight, (visualizerEnabled ? visualizerHeight : 0), calcMetadataHeight()); } void shortenString(char *str, size_t maxLength) { size_t length = strlen(str); if (length > maxLength) { str[maxLength] = '\0'; } } void printHelp() { printf(" kew - a command-line music player.\n"); printf("\n"); printf(" \033[1;4mUsage:\033[0m kew path \"path to music library\"\n"); printf(" (Saves the music library path. Use this the first time. Ie: kew path \"/home/joe/Music/\")\n"); printf(" kew (no argument, opens library)\n"); printf(" kew all (loads all your songs up to 10 000)\n"); printf(" kew \n"); printf(" kew --help, -? or -h\n"); printf(" kew --version or -v\n"); printf(" kew dir (Sometimes it's necessary to specify it's a directory you want)\n"); printf(" kew song \n"); printf(" kew list \n"); printf(" kew shuffle

(random and rand works too)\n"); printf(" kew artistA:artistB (plays artistA and artistB shuffled)\n"); printf(" kew . (plays kew.m3u file)\n"); printf("\n"); printf(" \033[1;4mExample:\033[0m kew moon\n"); printf(" (Plays the first song or directory it finds that has the word moon, ie moonlight sonata)\n"); printf("\n"); printf(" kew returns the first directory or file whose name partially matches the string you provide.\n\n"); printf(" Use quotes when providing strings with single quotes in them (') or vice versa.\n"); printf(" Use ←, → or h, l to play the next or previous track in the playlist.\n"); printf(" Use + (or =), - to adjust volume.\n"); printf(" Use a, d to seek in a song.\n"); printf(" Press space or p to pause.\n"); printf(" Press u to update the library.\n"); printf(" Press F2 to display playlist.\n"); printf(" Press F3 to display music library.\n"); printf(" Press F4 to display song info.\n"); printf(" Press F5 to display key bindings.\n"); printf(" Press F5 to display key bindings.\n"); printf(" Press . to add the currently playing song to kew.m3u.\n"); printf(" Press q to quit.\n"); printf("\n"); } int printLogo(SongData *songData) { if (useProfileColors) setTextColor(mainColor); else setColor(); int height = 0; int logoWidth = 0; if (!hideLogo) { printBlankSpaces(indent); printf(" __\n"); printBlankSpaces(indent); printf("| |--.-----.--.--.--.\n"); printBlankSpaces(indent); printf("| <| -__| | | |\n"); printBlankSpaces(indent); printf("|__|__|_____|________|"); logoWidth = 22; height += 3; } else { printf("\n"); height += 1; } if (songData != NULL && songData->metadata != NULL) { int term_w, term_h; getTermSize(&term_w, &term_h); char title[MAXPATHLEN] = {0}; if (hideLogo && songData->metadata->artist[0] != '\0') { printBlankSpaces(indent); snprintf(title, MAXPATHLEN, "%s - %s", songData->metadata->artist, songData->metadata->title); } else { strncpy(title, songData->metadata->title, MAXPATHLEN - 1); title[MAXPATHLEN - 1] = '\0'; } shortenString(title, term_w - indent - indent - logoWidth - 4); if (useProfileColors) setTextColor(titleColor); printf(" %s\n\n", title); height += 2; } else { printf("\n\n"); height += 2; } return height; } int getYear(const char *dateString) { int year; if (sscanf(dateString, "%d", &year) != 1) { return -1; } return year; } int displayCover(FIBITMAP *cover, const char *coverArtPath, int height, bool ansii) { int width = height * 2; if (!ansii) { printBitmapCentered(cover, width, height); } else { output_ascii(coverArtPath, height, width); } printf("\n"); return 0; } void printCover(SongData *songdata) { clearRestOfScreen(); minWidth = ABSOLUTE_MIN_WIDTH + indent; if (songdata->cover != NULL && coverEnabled) { clearScreen(); displayCover(songdata->cover, songdata->coverArtPath, preferredHeight, coverAnsi); drewCover = true; } else { clearRestOfScreen(); for (int i = 0; i < preferredHeight - 1; i++) { printf("\n"); } drewCover = false; } } void printWithDelay(const char *text, int delay, int maxWidth) { int length = strlen(text); int max = (maxWidth > length) ? length : maxWidth; for (int i = 0; i <= max; i++) { printf("\r "); printBlankSpaces(indent); for (int j = 0; j < i; j++) { printf("%c", text[j]); } printf("█"); fflush(stdout); c_sleep(delay); } c_sleep(delay * 20); printf("\r"); printf("\033[K"); printBlankSpaces(indent); printf("\033[1K %.*s", maxWidth, text); printf("\n"); fflush(stdout); } void printBasicMetadata(TagSettings const *metadata) { int term_w, term_h; getTermSize(&term_w, &term_h); maxWidth = textWidth; // term_w - 3 - (indent * 2); printf("\n"); setColor(); int rows = 1; if (strlen(metadata->artist) > 0) { printBlankSpaces(indent); printf(" %.*s\n", maxWidth, metadata->artist); rows++; } if (strlen(metadata->album) > 0) { printBlankSpaces(indent); printf(" %.*s\n", maxWidth, metadata->album); rows++; } if (strlen(metadata->date) > 0) { printBlankSpaces(indent); int year = getYear(metadata->date); if (year == -1) printf(" %s\n", metadata->date); else printf(" %d\n", year); rows++; } cursorJump(rows); if (strlen(metadata->title) > 0) { PixelData pixel = increaseLuminosity(color, 20); if (pixel.r == 255 && pixel.g == 255 && pixel.b == 255) { PixelData gray; gray.r = defaultColor; gray.g = defaultColor; gray.b = defaultColor; printf("\033[1;38;2;%03u;%03u;%03um", gray.r, gray.g, gray.b); } else { printf("\033[1;38;2;%03u;%03u;%03um", pixel.r, pixel.g, pixel.b); } if (useProfileColors) printf("\e[1m\e[39m"); printWithDelay(metadata->title, 9, maxWidth - 2); } cursorJumpDown(rows - 1); } int calcElapsedBars(double elapsedSeconds, double duration, int numProgressBars) { if (elapsedSeconds == 0) return 0; return (int)((elapsedSeconds / duration) * numProgressBars); } void printProgress(double elapsed_seconds, double total_seconds) { int progressWidth = 39; int term_w, term_h; getTermSize(&term_w, &term_h); if (term_w < progressWidth) return; // Save the current cursor position printf("\033[s"); int elapsed_hours = (int)(elapsed_seconds / 3600); int elapsed_minutes = (int)(((int)elapsed_seconds / 60) % 60); int elapsed_seconds_remainder = (int)elapsed_seconds % 60; int total_hours = (int)(total_seconds / 3600); int total_minutes = (int)(((int)total_seconds / 60) % 60); int total_seconds_remainder = (int)total_seconds % 60; int progress_percentage = (int)((elapsed_seconds / total_seconds) * 100); int vol = getCurrentVolume(); // Clear the current line printf("\r\033[K"); printBlankSpaces(indent); printf(" %02d:%02d:%02d / %02d:%02d:%02d (%d%%) Vol:%d%%", elapsed_hours, elapsed_minutes, elapsed_seconds_remainder, total_hours, total_minutes, total_seconds_remainder, progress_percentage, vol); // Restore the cursor position printf("\033[u"); } void printMetadata(TagSettings const *metadata) { if (!metaDataEnabled || appState.currentView == LIBRARY_VIEW || appState.currentView == PLAYLIST_VIEW) return; c_sleep(100); setColor(); printBasicMetadata(metadata); } void printTime(double elapsedSeconds) { if (!timeEnabled || appState.currentView == LIBRARY_VIEW || appState.currentView == PLAYLIST_VIEW) return; setColor(); int term_w, term_h; getTermSize(&term_w, &term_h); printBlankSpaces(indent); if (term_h > minHeight) printProgress(elapsedSeconds, duration); } int getRandomNumber(int min, int max) { return min + rand() % (max - min + 1); } void printGlimmeringText(char *text, char *nerdFontText, PixelData color) { int textLength = strlen(text); int brightIndex = 0; PixelData vbright = increaseLuminosity(color, 120); PixelData bright = increaseLuminosity(color, 60); printBlankSpaces(indent); while (brightIndex < textLength) { for (int i = 0; i < textLength; i++) { if (i == brightIndex) { setTextColorRGB(vbright.r, vbright.g, vbright.b); printf("%c", text[i]); } else if (i == brightIndex - 1 || i == brightIndex + 1) { setTextColorRGB(bright.r, bright.g, bright.b); printf("%c", text[i]); } else { setTextColorRGB(color.r, color.g, color.b); printf("%c", text[i]); } fflush(stdout); c_usleep(50); } printf("%s", nerdFontText); fflush(stdout); c_usleep(50); brightIndex++; printf("\r"); printBlankSpaces(indent); } } void printLastRow() { int term_w, term_h; getTermSize(&term_w, &term_h); if (term_w < minWidth) return; setTextColorRGB(lastRowColor.r, lastRowColor.g, lastRowColor.b); char text[100] = " [F2 Playlist] [F3 Library] [F4 Track] [F5 Keys] [Q Quit]"; char nerdFontText[100] = ""; printf("\r"); if (nerdFontsEnabled) { if (isPaused()) { char pauseText[] = " \uf04c"; strcat(nerdFontText, pauseText); } if (isRepeatEnabled()) { char repeatText[] = " \uf01e"; strcat(nerdFontText, repeatText); } if (isShuffleEnabled()) { char shuffleText[] = " \uf074"; strcat(nerdFontText, shuffleText); } if (fastForwarding) { char forwardText[] = " \uf04e"; strcat(nerdFontText, forwardText); } if (rewinding) { char rewindText[] = " \uf04a"; strcat(nerdFontText, rewindText); } } else { if (isRepeatEnabled()) { char repeatText[] = " R"; strcat(text, repeatText); } if (isShuffleEnabled()) { char shuffleText[] = " S"; strcat(text, shuffleText); } } printf("\033[K"); // clear the line int randomNumber = getRandomNumber(1, 808); if (randomNumber == 808) printGlimmeringText(text, nerdFontText, lastRowColor); else { printBlankSpaces(indent); printf("%s", text); printf("%s", nerdFontText); } } int printAbout(SongData *songdata) { clearScreen(); int numRows = printLogo(songdata); setDefaultTextColor(); printBlankSpaces(indent); printf(" kew version: %s\n\n", VERSION); numRows += 2; return numRows; } void removeUnneededChars(char *str) { if (strlen(str) < 6) { return; } int i; for (i = 0; i < 3 && str[i] != '\0' && str[i] != ' '; i++) { if (isdigit(str[i]) || str[i] == '-' || str[i] == ' ') { int j; for (j = i; str[j] != '\0'; j++) { str[j] = str[j + 1]; } str[j] = '\0'; i--; // Decrement i to re-check the current index } } i = 0; while (str[i] != '\0') { if (str[i] == '-' || str[i] == '_') { str[i] = ' '; } i++; } } int showKeyBindings(SongData *songdata, AppSettings *settings) { int numPrintedRows = 0; int term_w, term_h; getTermSize(&term_w, &term_h); numPrintedRows += printAbout(songdata); setDefaultTextColor(); printBlankSpaces(indent); printf(" - Use %s (or %s) and %s to adjust volume.\n", settings->volumeUp, settings->volumeUpAlt, settings->volumeDown); printBlankSpaces(indent); printf(" - Use ←, → or %s, %s keys to switch tracks.\n", settings->previousTrackAlt, settings->nextTrackAlt); printBlankSpaces(indent); printf(" - Use ↑, ↓ or %s, %s keys to scroll through playlist.\n", settings->scrollUpAlt, settings->scrollDownAlt); printBlankSpaces(indent); printf(" - Enter a number then Enter to switch song.\n"); printBlankSpaces(indent); printf(" - Space (or %s) to toggle pause.\n", settings->togglePause); printBlankSpaces(indent); printf(" - %s toggle color derived from album or from profile.\n", settings->toggleColorsDerivedFrom); printBlankSpaces(indent); printf(" - %s to update the library.\n", settings->updateLibrary); printBlankSpaces(indent); printf(" - %s to show/hide the spectrum visualizer.\n", settings->toggleVisualizer); printBlankSpaces(indent); printf(" - %s to show/hide album covers.\n", settings->toggleCovers); printBlankSpaces(indent); printf(" - %s to toggle album covers drawn in ascii.\n", settings->toggleAscii); printBlankSpaces(indent); printf(" - %s to repeat the current song.\n", settings->toggleRepeat); printBlankSpaces(indent); printf(" - %s to shuffle the playlist.\n", settings->toggleShuffle); printBlankSpaces(indent); printf(" - %s to seek backward.\n", settings->seekBackward); printBlankSpaces(indent); printf(" - %s to seek forward.\n", settings->seekForward); printBlankSpaces(indent); printf(" - %s to save the playlist to your music folder.\n", settings->savePlaylist); printBlankSpaces(indent); printf(" - %s to add current song to kew.m3u (run with \"kew .\").\n", settings->addToMainPlaylist); printBlankSpaces(indent); printf(" - %s to quit.\n", settings->quit); printf("\n"); printLastRow(); numPrintedRows += 17; return numPrintedRows; } void toggleShowPlaylist() { refresh = true; if (appState.currentView == PLAYLIST_VIEW) { appState.currentView = SONG_VIEW; } else { appState.currentView = PLAYLIST_VIEW; } } void toggleShowLibrary() { refresh = true; if (appState.currentView == LIBRARY_VIEW) { appState.currentView = SONG_VIEW; } else { appState.currentView = LIBRARY_VIEW; } } void showTrack() { refresh = true; appState.currentView = SONG_VIEW; } void toggleShowKeyBindings() { refresh = true; if (appState.currentView == KEYBINDINGS_VIEW) { appState.currentView = SONG_VIEW; } else { appState.currentView = KEYBINDINGS_VIEW; } } void flipNextPage() { if (appState.currentView == LIBRARY_VIEW) { chosenLibRow += maxLibListSize - 1; startLibIter += maxLibListSize - 1; refresh = true; } else if (appState.currentView == PLAYLIST_VIEW) { chosenRow += maxListSize - 1; refresh = true; } } void flipPrevPage() { if (appState.currentView == LIBRARY_VIEW) { chosenLibRow -= maxLibListSize; startLibIter -= maxLibListSize; refresh = true; } else if (appState.currentView == PLAYLIST_VIEW) { chosenRow -= maxListSize; refresh = true; } } void scrollNext() { if (appState.currentView == PLAYLIST_VIEW) { chosenRow++; refresh = true; } else if (appState.currentView == LIBRARY_VIEW) { chosenLibRow++; refresh = true; } } void scrollPrev() { if (appState.currentView == PLAYLIST_VIEW) { chosenRow--; refresh = true; } else if (appState.currentView == LIBRARY_VIEW) { chosenLibRow--; refresh = true; } } int getRowWithinBounds(int row) { if (row >= originalPlaylist->count) { row = originalPlaylist->count - 1; } if (row < 0) row = 0; return row; } int showPlaylist(SongData *songData) { Node *node = originalPlaylist->head; Node *foundNode = NULL; bool startFromCurrent = false; int term_w, term_h; getTermSize(&term_w, &term_h); int totalHeight = term_h; maxListSize = totalHeight - 3; int numRows = 0; int numPrintedRows = 0; int foundAt = -1; numRows++; int aboutRows = printLogo(songData); maxListSize -= aboutRows - 1; setDefaultTextColor(); printBlankSpaces(indent); if (term_w > 52 && !hideHelp) { maxListSize -= 3; printf(" Use ↑, ↓ or k, j to choose. Enter to accept.\n"); printBlankSpaces(indent); printf(" Pg Up and Pg Dn to scroll. Del to remove entry.\n\n"); } int numSongs = 0; for (int i = 0; i < originalPlaylist->count; i++) { if (node == NULL) break; if (currentSong != NULL && currentSong->id == node->id) { foundAt = numSongs; foundNode = node; } node = node->next; numSongs++; if (numSongs > maxListSize) { startFromCurrent = true; if (foundAt > -1) break; } } if (startFromCurrent) node = foundNode; else node = originalPlaylist->head; if (chosenRow >= originalPlaylist->count) { chosenRow = originalPlaylist->count - 1; } chosenSong = chosenRow; chosenSong = (chosenSong < 0) ? 0 : chosenSong; if (chosenSong < startIter) { startIter = chosenSong; } if (chosenRow > startIter + maxListSize - round(maxListSize / 2)) { startIter = chosenRow - maxListSize + round(maxListSize / 2); } if (startIter == 0 && chosenRow < 0) { chosenRow = 0; } if (resetPlaylistDisplay && !audioData.endOfListReached) { startIter = chosenRow = chosenSong = foundAt; } for (int i = foundAt; i > startIter; i--) { if (i > 0 && node->prev != NULL) node = node->prev; } if (foundAt > -1) { for (int i = foundAt; i < startIter; i++) { if (node->next != NULL) node = node->next; } } for (int i = (startFromCurrent ? startIter : 0); i < (startFromCurrent ? startIter : 0) + maxListSize; i++) { setDefaultTextColor(); if (node == NULL) break; char filePath[MAXPATHLEN]; c_strcpy(filePath, sizeof(filePath), node->song.filePath); char *lastSlash = strrchr(filePath, '/'); char *lastDot = strrchr(filePath, '.'); printf("\r"); printBlankSpaces(indent); if (lastSlash != NULL && lastDot != NULL && lastDot > lastSlash) { char copiedString[256]; strncpy(copiedString, lastSlash + 1, lastDot - lastSlash - 1); copiedString[lastDot - lastSlash - 1] = '\0'; removeUnneededChars(copiedString); if (i == chosenSong) { chosenNodeId = node->id; printf("\x1b[7m"); } if (foundNode != NULL && foundNode->id == node->id) { printf("\e[1m\e[39m"); } shortenString(copiedString, term_w - 10 - indent); trim(copiedString); if (i + 1 < 10) printf(" "); if (startFromCurrent) { printf(" %d. %s \n", i + 1, copiedString); } else { printf(" %d. %s \n", numRows, copiedString); } numPrintedRows++; numRows++; setTextColorRGB2(color.r, color.g, color.b); setDefaultTextColor(); } node = node->next; } printf("\n"); numPrintedRows++; printLastRow(); if (numRows > 1) { while (numPrintedRows < maxListSize) { printf("\n"); numPrintedRows++; } } numPrintedRows += 2; numPrintedRows += aboutRows; if (term_w > 46) { numPrintedRows += 2; } resetPlaylistDisplay = false; return numPrintedRows; } void printElapsedBars(int elapsedBars) { printBlankSpaces(indent); printf(" "); for (int i = 0; i < numProgressBars; i++) { if (i == 0) { printf("■ "); } else if (i < elapsedBars) printf("■ "); else { printf("= "); } } printf("\n"); } void printVisualizer(double elapsedSeconds) { if (visualizerEnabled && appState.currentView == SONG_VIEW) { printf("\n"); int term_w, term_h; getTermSize(&term_w, &term_h); int visualizerWidth = (ABSOLUTE_MIN_WIDTH > preferredWidth) ? ABSOLUTE_MIN_WIDTH : preferredWidth; visualizerWidth = (visualizerWidth < textWidth && textWidth < term_w - 2) ? textWidth : visualizerWidth; visualizerWidth = (visualizerWidth > term_w - 2) ? term_w - 2 : visualizerWidth; numProgressBars = (int)visualizerWidth / 2; drawSpectrumVisualizer(visualizerHeight, visualizerWidth, color, indent, useProfileColors); printElapsedBars(calcElapsedBars(elapsedSeconds, duration, numProgressBars)); printLastRow(); int jumpAmount = visualizerHeight + 2; cursorJump(jumpAmount); saveCursorPosition(); } else if (!visualizerEnabled) { int term_w, term_h; getTermSize(&term_w, &term_h); if (term_w >= minWidth) { printf("\n\n"); printLastRow(); cursorJump(2); } } } void calcIndent(SongData *songdata) { if (songdata == NULL || appState.currentView != SONG_VIEW) { int textWidth = (ABSOLUTE_MIN_WIDTH > preferredWidth) ? ABSOLUTE_MIN_WIDTH : preferredWidth; indent = getIndentation(textWidth - 1) - 1; return; } int titleLength = strlen(songdata->metadata->title); int albumLength = strlen(songdata->metadata->album); int maxTextLength = (albumLength > titleLength) ? albumLength : titleLength; textWidth = (ABSOLUTE_MIN_WIDTH > preferredWidth) ? ABSOLUTE_MIN_WIDTH : preferredWidth; int term_w, term_h; getTermSize(&term_w, &term_h); int maxSize = term_w - 2; if (titleLength > 0 && titleLength < maxSize && titleLength > textWidth) textWidth = titleLength; if (maxTextLength > 0 && maxTextLength < maxSize && maxTextLength > textWidth) textWidth = maxTextLength; if (textWidth > maxSize) textWidth = maxSize; indent = getIndentation(textWidth - 1) - 1; } FileSystemEntry *getCurrentLibEntry() { return currentEntry; } FileSystemEntry *getLibrary() { return library; } FileSystemEntry *getChosenDir() { return chosenDir; } void processName(const char *name, char *output, int maxWidth) { char *lastDot = strrchr(name, '.'); if (lastDot != NULL) { int copyLength = lastDot - name; if (copyLength > maxWidth) { copyLength = maxWidth; } strncpy(output, name, copyLength); output[copyLength] = '\0'; removeUnneededChars(output); } else { strncpy(output, name, maxWidth); output[maxWidth] = '\0'; removeUnneededChars(output); } } void setCurrentAsChosenDir() { if (currentEntry->isDirectory) chosenDir = currentEntry; } void resetChosenDir() { chosenDir = NULL; } int displayTree(FileSystemEntry *root, int depth, int maxListSize, int maxNameWidth) { char dirName[maxNameWidth + 1]; char filename[maxNameWidth + 1]; if (libIter >= startLibIter + maxListSize) { return 0; } FileSystemEntry *tmp = root->parent == NULL ? NULL : root->parent->children; int numAudioChildren = 0; while (tmp != NULL) { if (!tmp->isDirectory) numAudioChildren++; tmp = tmp->next; } if (chosenLibRow > startLibIter + maxListSize - round(maxListSize / 2)) { startLibIter = chosenLibRow - maxListSize + round(maxListSize / 2) + 1; } if (allowChooseSongs) { if (chosenLibRow >= libIter + libSongIter && libSongIter != 0) { if (chosenLibRow >= numDirectoryTreeEntries + numTopLevelSongs + numAudioChildren) { startLibIter = numDirectoryTreeEntries + numTopLevelSongs + numAudioChildren - maxListSize; chosenLibRow = numDirectoryTreeEntries + numTopLevelSongs + numAudioChildren - 1; } } } else { if (chosenLibRow >= numDirectoryTreeEntries + numTopLevelSongs) { startLibIter = numDirectoryTreeEntries + numTopLevelSongs - maxListSize; chosenLibRow = numDirectoryTreeEntries + numTopLevelSongs - 1; } } if (chosenLibRow < 0) startLibIter = chosenLibRow = libIter = 0; if (root == NULL) return 0; if (root->isDirectory || (!root->isDirectory && depth == 1) || (chosenDir != NULL && allowChooseSongs && root->parent != NULL && (strcmp(root->parent->fullPath, chosenDir->fullPath) == 0 || strcmp(root->fullPath, chosenDir->fullPath) == 0))) { if (depth > 0) { if (libIter >= startLibIter) { if (depth == 1) { if (useProfileColors) setTextColor(artistColor); else setColor(); } else { setDefaultTextColor(); } if (depth >= 2) printf(" "); printBlankSpaces(indent); if (chosenLibRow == libIter) { if (root->isEnqueued) { if (useProfileColors) setTextColor(enqueuedColor); else setColor(); printf("\x1b[7m * "); } else { printf(" \x1b[7m "); } currentEntry = root; if (allowChooseSongs == true && (chosenDir == NULL || (currentEntry != NULL && currentEntry->parent != NULL && chosenDir != NULL && (strcmp(currentEntry->parent->fullPath, chosenDir->fullPath) != 0) && strcmp(root->fullPath, chosenDir->fullPath) != 0))) { chosenLibRow -= libSongIter; allowChooseSongs = false; chosenDir = NULL; refresh = true; } } else { if (root->isEnqueued) { if (useProfileColors) setTextColor(enqueuedColor); else setColor(); printf(" * "); } else printf(" "); } if (root->isDirectory) { dirName[0] = '\0'; snprintf(dirName, maxNameWidth + 1, "%s", root->name); if (depth == 1) printf("%s \n", stringToUpper(dirName)); else printf("%s \n", dirName); } else { filename[0] = '\0'; processName(root->name, filename, maxNameWidth); printf(" └─%s \n", filename); libSongIter++; } setColor(); } libIter++; } FileSystemEntry *child = root->children; while (child != NULL) { if (displayTree(child, depth + 1, maxListSize, maxNameWidth) == -1) return -1; child = child->next; } } return 0; } char *getLibraryFilePath() { char *configdir = getConfigPath(); char *filepath = NULL; size_t filepath_length = strlen(configdir) + strlen("/") + strlen(LIBRARY_FILE) + 1; filepath = (char *)malloc(filepath_length); strcpy(filepath, configdir); strcat(filepath, "/"); strcat(filepath, LIBRARY_FILE); free(configdir); return filepath; } void showLibrary(SongData *songData) { libIter = 0; libSongIter = 0; startLibIter = 0; refresh = false; int term_w, term_h; getTermSize(&term_w, &term_h); int totalHeight = term_h; maxLibListSize = totalHeight; setColor(); int aboutSize = printLogo(songData); int maxNameWidth = term_w - 10 - indent; maxLibListSize -= aboutSize + 2; setDefaultTextColor(); if (term_w > 60 && !hideHelp) { maxLibListSize -= 3; printBlankSpaces(indent); printf(" Use ↑, ↓ or k, j to choose. Enter to enqueue/dequeue.\n"); printBlankSpaces(indent); printf(" Pg Up and Pg Dn to scroll. Press u to update the library.\n\n"); } numTopLevelSongs = 0; FileSystemEntry *tmp = library->children; while (tmp != NULL) { if (!tmp->isDirectory) numTopLevelSongs++; tmp = tmp->next; } displayTree(library, 0, maxLibListSize, maxNameWidth); printf("\n"); printLastRow(); if (refresh) { printf("\033[1;1H"); clearScreen(); showLibrary(songData); } } int printPlayer(SongData *songdata, double elapsedSeconds, AppSettings *settings) { if (!uiEnabled) { return 0; } hideCursor(); setColor(); if (songdata != NULL && songdata->metadata != NULL && !songdata->hasErrors && (songdata->hasErrors < 1)) { metadata = *songdata->metadata; duration = songdata->duration; if (songdata->cover != NULL && coverEnabled) { color.r = songdata->red; color.g = songdata->green; color.b = songdata->blue; } } else { if (appState.currentView != LIBRARY_VIEW && appState.currentView != PLAYLIST_VIEW && appState.currentView != KEYBINDINGS_VIEW) { appState.currentView = LIBRARY_VIEW; } color.r = defaultColor; color.g = defaultColor; color.b = defaultColor; } calcPreferredSize(); calcIndent(songdata); if (preferredWidth <= 0 || preferredHeight <= 0) return -1; if (appState.currentView != PLAYLIST_VIEW) resetPlaylistDisplay = true; if (appState.currentView == KEYBINDINGS_VIEW && refresh) { clearScreen(); showKeyBindings(songdata, settings); saveCursorPosition(); refresh = false; } else if (appState.currentView == PLAYLIST_VIEW && refresh) { clearScreen(); int height = showPlaylist(songdata); cursorJump(height); saveCursorPosition(); refresh = false; } else if (appState.currentView == LIBRARY_VIEW && refresh) { clearScreen(); showLibrary(songdata); refresh = false; } else if (appState.currentView == SONG_VIEW && songdata != NULL) { if (refresh) { clearScreen(); printf("\n"); printCover(songdata); printMetadata(songdata->metadata); refresh = false; } printTime(elapsedSeconds); printVisualizer(elapsedSeconds); } fflush(stdout); return 0; } void showHelp() { printHelp(); } void freeMainDirectoryTree() { if (library == NULL) return; char *filepath = getLibraryFilePath(); if (cacheLibrary) freeAndWriteTree(library, filepath); else freeTree(library); free(filepath); } kew-2.4.2/src/player.h000066400000000000000000000036501457005474400145570ustar00rootroot00000000000000#ifndef PLAYER_H #define PLAYER_H #include #include #include #include #include "../include/imgtotxt/write_ascii.h" #include "../include/imgtotxt/options.h" #include "chafafunc.h" #include "directorytree.h" #include "playlist.h" #include "songloader.h" #include "sound.h" #include "term.h" #include "utils.h" #include "visuals.h" extern int mainColor; extern int artistColor; extern int enqueuedColor; extern int titleColor; extern const char VERSION[]; extern bool coverEnabled; extern bool uiEnabled; extern bool coverAnsi; extern bool visualizerEnabled; extern bool useThemeColors; extern bool hasPrintedPaused; extern bool quitAfterStopping; extern bool nerdFontsEnabled; extern bool hideLogo; extern bool hideHelp; extern int numProgressBars; extern int chosenSong; extern bool resetPlaylistDisplay; extern int visualizerHeight; extern TagSettings metadata; extern bool fastForwarding; extern bool rewinding; extern double elapsedSeconds; extern double pauseSeconds; extern double totalPauseSeconds; extern double seekAccumulatedSeconds; extern bool allowChooseSongs; extern int chosenLibRow; extern int chosenRow; extern int chosenNodeId; extern bool useProfileColors; extern int cacheLibrary; extern int numDirectoryTreeEntries; extern FileSystemEntry *library; bool hasNerdFonts(); int printPlayer(SongData *songdata, double elapsedSeconds, AppSettings *settings); void flipNextPage(); void flipPrevPage(); void showHelp(void); int printAbout(SongData *songdata); FileSystemEntry *getCurrentLibEntry(); FileSystemEntry *getChosenDir(); FileSystemEntry *getLibrary(); void scrollNext(void); void scrollPrev(void); void setCurrentAsChosenDir(); void toggleShowKeyBindings(void); void toggleShowLibrary(); void toggleShowPlaylist(void); void showTrack(); void setTextColorRGB2(int r, int g, int b); void freeMainDirectoryTree(); char *getLibraryFilePath(); void resetChosenDir(); #endif kew-2.4.2/src/playerops.c000066400000000000000000001120371457005474400152740ustar00rootroot00000000000000#include "playerops.h" /* playerops.c Related to features/actions of the player. */ #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef ASK_IF_USE_CACHE_LIMIT_SECONDS #define ASK_IF_USE_CACHE_LIMIT_SECONDS 1 #endif struct timespec current_time; struct timespec start_time; struct timespec pause_time; struct timespec lastInputTime; struct timespec lastPlaylistChangeTime; struct timespec lastUpdateTime = {0, 0}; bool playlistNeedsUpdate = false; bool nextSongNeedsRebuilding = false; bool enqueuedNeedsUpdate = false; bool playingMainPlaylist = false; bool usingSongDataA = true; LoadingThreadData loadingdata; volatile bool loadedNextSong = false; bool waitingForPlaylist = false; bool waitingForNext = false; Node *nextSong = NULL; Node *tryNextSong = NULL; Node *prevSong = NULL; int lastPlayedId = -1; bool songHasErrors = false; bool skipOutOfOrder = false; bool skipping = false; bool loadingFailed = false; bool forceSkip = false; volatile bool clearingErrors = false; volatile bool songLoading = false; GDBusConnection *connection = NULL; void updatePlaylist() { playlistNeedsUpdate = false; if (isShuffleEnabled()) { if (currentSong != NULL) shufflePlaylistStartingFromSong(&playlist, currentSong); else shufflePlaylist(&playlist); nextSongNeedsRebuilding = true; } } void rebuildAndUpdatePlaylist() { if (!playlistNeedsUpdate && !nextSongNeedsRebuilding) return; pthread_mutex_lock(&(playlist.mutex)); if (playlistNeedsUpdate) { updatePlaylist(); } pthread_mutex_unlock(&(playlist.mutex)); } void skip() { setCurrentImplementationType(NONE); setRepeatEnabled(false); audioData.endOfListReached = false; rebuildAndUpdatePlaylist(); if (!isPlaying()) { switchAudioImplementation(); } else { setSkipToNext(true); } if (!skipOutOfOrder) refresh = true; } void updateLastSongSwitchTime() { clock_gettime(CLOCK_MONOTONIC, &start_time); } void updateLastPlaylistChangeTime() { clock_gettime(CLOCK_MONOTONIC, &lastPlaylistChangeTime); } void updateLastInputTime() { clock_gettime(CLOCK_MONOTONIC, &lastInputTime); } void updatePlaybackPosition(double elapsedSeconds) { GVariantBuilder changedPropertiesBuilder; g_variant_builder_init(&changedPropertiesBuilder, G_VARIANT_TYPE_DICTIONARY); g_variant_builder_add(&changedPropertiesBuilder, "{sv}", "Position", g_variant_new_int64(llround(elapsedSeconds * G_USEC_PER_SEC))); GVariant *parameters = g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changedPropertiesBuilder, NULL); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", parameters, NULL); } void emitSeekedSignal(double newPositionSeconds) { gint64 newPositionMicroseconds = llround(newPositionSeconds * G_USEC_PER_SEC); GVariant *parameters = g_variant_new("(x)", newPositionMicroseconds); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "Seeked", parameters, NULL); } void emitStringPropertyChanged(const gchar *propertyName, const gchar *newValue) { GVariantBuilder changed_properties_builder; g_variant_builder_init(&changed_properties_builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&changed_properties_builder, "{sv}", propertyName, g_variant_new_string(newValue)); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changed_properties_builder, NULL), NULL); g_variant_builder_clear(&changed_properties_builder); } void emitBooleanPropertyChanged(const gchar *propertyName, gboolean newValue) { GVariantBuilder changed_properties_builder; g_variant_builder_init(&changed_properties_builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&changed_properties_builder, "{sv}", propertyName, g_variant_new_boolean(newValue)); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changed_properties_builder, NULL), NULL); g_variant_builder_clear(&changed_properties_builder); } void playbackPause(struct timespec *pause_time) { if (!isPaused()) { emitStringPropertyChanged("PlaybackStatus", "Paused"); clock_gettime(CLOCK_MONOTONIC, pause_time); } pausePlayback(); } void playbackPlay(double *totalPauseSeconds, double *pauseSeconds) { if (isPaused()) { *totalPauseSeconds += *pauseSeconds; emitStringPropertyChanged("PlaybackStatus", "Playing"); } resumePlayback(); } void togglePause(double *totalPauseSeconds, double *pauseSeconds, struct timespec *pause_time) { togglePausePlayback(); if (isPaused()) { emitStringPropertyChanged("PlaybackStatus", "Paused"); clock_gettime(CLOCK_MONOTONIC, pause_time); } else { *totalPauseSeconds += *pauseSeconds; emitStringPropertyChanged("PlaybackStatus", "Playing"); } } void toggleRepeat() { bool repeatEnabled = !isRepeatEnabled(); setRepeatEnabled(repeatEnabled); if (repeatEnabled) { emitStringPropertyChanged("LoopStatus", "Track"); } else { emitStringPropertyChanged("LoopStatus", "None"); } if (appState.currentView != SONG_VIEW) refresh = true; } void addToSpecialPlaylist() { if (currentSong == NULL) return; int id = currentSong->id; Node *node = NULL; if (findSelectedEntryById(specialPlaylist, id) != NULL) // song is already in list return; createNode(&node, currentSong->song.filePath, id); addToList(specialPlaylist, node); } void toggleShuffle() { bool shuffleEnabled = !isShuffleEnabled(); setShuffleEnabled(shuffleEnabled); if (shuffleEnabled) { pthread_mutex_lock(&(playlist.mutex)); shufflePlaylistStartingFromSong(&playlist, currentSong); pthread_mutex_unlock(&(playlist.mutex)); emitBooleanPropertyChanged("Shuffle", TRUE); } else { char *path = NULL; if (currentSong != NULL) { path = strdup(currentSong->song.filePath); } pthread_mutex_lock(&(playlist.mutex)); deletePlaylist(&playlist); // Doesn't destroy the mutex deepCopyPlayListOntoList(originalPlaylist, &playlist); if (path != NULL) { currentSong = findPathInPlaylist(path, &playlist); free(path); } pthread_mutex_unlock(&(playlist.mutex)); emitBooleanPropertyChanged("Shuffle", FALSE); } loadedNextSong = false; nextSong = NULL; if (appState.currentView == PLAYLIST_VIEW || appState.currentView == LIBRARY_VIEW) refresh = true; } void toggleBlocks(AppSettings *settings) { coverAnsi = !coverAnsi; c_strcpy(settings->coverAnsi, sizeof(settings->coverAnsi), coverAnsi ? "1" : "0"); if (coverEnabled) { clearScreen(); refresh = true; } } void toggleColors(AppSettings *settings) { useProfileColors = !useProfileColors; c_strcpy(settings->useProfileColors, sizeof(settings->useProfileColors), useProfileColors ? "1" : "0"); clearScreen(); refresh = true; } void toggleCovers(AppSettings *settings) { coverEnabled = !coverEnabled; c_strcpy(settings->coverEnabled, sizeof(settings->coverEnabled), coverEnabled ? "1" : "0"); clearScreen(); refresh = true; } void toggleVisualizer(AppSettings *settings) { visualizerEnabled = !visualizerEnabled; c_strcpy(settings->visualizerEnabled, sizeof(settings->visualizerEnabled), visualizerEnabled ? "1" : "0"); restoreCursorPosition(); refresh = true; } void quit() { doQuit = true; } SongData *getCurrentSongData() { if (currentSong == NULL) return NULL; return (audioData.currentFileIndex == 0) ? userData.songdataA : userData.songdataB; } void calcElapsedTime() { clock_gettime(CLOCK_MONOTONIC, ¤t_time); double timeSinceLastUpdate = (double)(current_time.tv_sec - lastUpdateTime.tv_sec) + (double)(current_time.tv_nsec - lastUpdateTime.tv_nsec) / 1e9; if (!isPaused()) { elapsedSeconds = (double)(current_time.tv_sec - start_time.tv_sec) + (double)(current_time.tv_nsec - start_time.tv_nsec) / 1e9; double seekElapsed = getSeekElapsed(); double diff = elapsedSeconds + (seekElapsed + seekAccumulatedSeconds - totalPauseSeconds); if (diff < 0) seekElapsed -= diff; elapsedSeconds += seekElapsed + seekAccumulatedSeconds - totalPauseSeconds; if (elapsedSeconds > duration) elapsedSeconds = duration; setSeekElapsed(seekElapsed); if (elapsedSeconds < 0.0) { elapsedSeconds = 0.0; } if (currentSong != NULL && timeSinceLastUpdate >= 1.0) { updatePlaybackPosition(elapsedSeconds); // Update the last update time to the current time lastUpdateTime = current_time; } } else { pauseSeconds = (double)(current_time.tv_sec - pause_time.tv_sec) + (double)(current_time.tv_nsec - pause_time.tv_nsec) / 1e9; } } void flushSeek() { if (seekAccumulatedSeconds != 0.0) { if (currentSong != NULL) { if (endsWith(currentSong->song.filePath, "ogg")) { return; } } setSeekElapsed(getSeekElapsed() + seekAccumulatedSeconds); seekAccumulatedSeconds = 0.0; calcElapsedTime(); float percentage = elapsedSeconds / (float)duration * 100.0; if (percentage < 0.0) { setSeekElapsed(0.0); percentage = 0.0; } seekPercentage(percentage); emitSeekedSignal(elapsedSeconds); } } void seekForward() { if (currentSong != NULL) { if (endsWith(currentSong->song.filePath, "ogg")) { return; } } if (duration != 0.0) { float step = 100 / numProgressBars; seekAccumulatedSeconds += step * duration / 100.0; } fastForwarding = true; } void seekBack() { if (currentSong != NULL) { if (endsWith(currentSong->song.filePath, "ogg")) { return; } } if (duration != 0.0) { float step = 100 / numProgressBars; seekAccumulatedSeconds -= step * duration / 100.0; } rewinding = true; } Node *findSelectedEntryById(PlayList *playlist, int id) { Node *node = playlist->head; if (node == NULL || id < 0) return NULL; bool found = false; for (int i = 0; i < playlist->count; i++) { if (node != NULL && node->id == id) { found = true; break; } else if (node == NULL) { return NULL; } node = node->next; } if (found) { return node; } return NULL; } Node *findSelectedEntry(PlayList *playlist, int row) { Node *node = playlist->head; if (node == NULL) return NULL; bool found = false; for (int i = 0; i < playlist->count; i++) { if (i == row) { found = true; break; } node = node->next; } if (found) { return node; } return NULL; } bool markAsDequeued(FileSystemEntry *root, char *path) { int numChildrenEnqueued = 0; if (root == NULL) return false; if (!root->isDirectory) { if (strcmp(root->fullPath, path) == 0) { root->isEnqueued = false; return true; } } else { FileSystemEntry *child = root->children; bool found = false; while (child != NULL) { found = markAsDequeued(child, path); child = child->next; if (found) break; } if (found) { child = root->children; while (child != NULL) { if (child->isEnqueued) numChildrenEnqueued++; child = child->next; } if (numChildrenEnqueued == 0) root->isEnqueued = false; return true; } } return false; } Node *getNextSong() { if (nextSong != NULL) return nextSong; else if (currentSong != NULL && currentSong->next != NULL) { return currentSong->next; } else { return NULL; } } void enqueueSong(FileSystemEntry *child) { int id = nodeIdCounter++; Node *node = NULL; createNode(&node, child->fullPath, id); addToList(originalPlaylist, node); Node *node2 = NULL; createNode(&node2, child->fullPath, id); addToList(&playlist, node2); child->isEnqueued = 1; child->parent->isEnqueued = 1; } void dequeueSong(FileSystemEntry *child) { Node *node1 = findLastPathInPlaylist(child->fullPath, originalPlaylist); if (node1 == NULL) return; if (currentSong != NULL && currentSong->id == node1->id) return; int id = node1->id; Node *node2 = findSelectedEntryById(&playlist, id); if (node1 != NULL) deleteFromList(originalPlaylist, node1); if (node2 != NULL) deleteFromList(&playlist, node2); child->isEnqueued = 0; // check if parent needs to be dequeued as well bool isEnqueued = false; FileSystemEntry *ch = child->parent->children; while (ch != NULL) { if (ch->isEnqueued) { isEnqueued = true; break; } ch = ch->next; } if (!isEnqueued) { child->parent->isEnqueued = 0; if (child->parent->parent != NULL) child->parent->parent->isEnqueued = 0; } } void dequeueChildren(FileSystemEntry *parent) { FileSystemEntry *child = parent->children; while (child != NULL) { if (child->isDirectory && child->children != NULL) { dequeueChildren(child); } else { dequeueSong(child); } child = child->next; } } void enqueueChildren(FileSystemEntry *child) { while (child != NULL) { if (child->isDirectory && child->children != NULL) { child->isEnqueued = 1; enqueueChildren(child->children); } else if (!child->isEnqueued) { enqueueSong(child); } child = child->next; } } bool hasSongChildren(FileSystemEntry *entry) { FileSystemEntry *child = entry->children; int numSongs = 0; while (child != NULL) { if (!child->isDirectory) numSongs++; child = child->next; } if (numSongs == 0) { return false; } return true; } bool hasDequeuedChildren(FileSystemEntry *parent) { FileSystemEntry *child = parent->children; bool isDequeued = false; while (child != NULL) { if (!child->isEnqueued) { isDequeued = true; } child = child->next; } return isDequeued; } void enqueueSongs() { FileSystemEntry *tmp = getCurrentLibEntry(); FileSystemEntry *chosenDir = getChosenDir(); bool hasEnqueued = false; if (tmp != NULL) { if (tmp->isDirectory) { if (!hasSongChildren(tmp) || (chosenDir != NULL && strcmp(tmp->fullPath, chosenDir->fullPath) == 0)) { if (hasDequeuedChildren(tmp)) { tmp->isEnqueued = 1; tmp = tmp->children; enqueueChildren(tmp); nextSongNeedsRebuilding = true; hasEnqueued = true; } else { dequeueChildren(tmp); nextSongNeedsRebuilding = true; } } setCurrentAsChosenDir(); allowChooseSongs = true; } else { if (!tmp->isEnqueued) { nextSong = NULL; nextSongNeedsRebuilding = true; enqueueSong(tmp); hasEnqueued = true; } else { nextSong = NULL; nextSongNeedsRebuilding = true; dequeueSong(tmp); } } refresh = true; } if (hasEnqueued) { waitingForNext = true; audioData.endOfListReached = false; } if (nextSongNeedsRebuilding) { updatePlaylist(); } updateLastPlaylistChangeTime(); } void handleRemove() { if (refresh) return; if (appState.currentView != PLAYLIST_VIEW) { return; } bool rebuild = false; Node *node = findSelectedEntry(originalPlaylist, chosenRow); Node *song = getNextSong(); int id = node->id; int currentId = (currentSong != NULL) ? currentSong->id : -1; if (node != NULL && playlist.head != NULL && playlist.head->id == node->id && playlist.count == 1) { return; } if (currentId == node->id) { return; } pthread_mutex_lock(&(playlist.mutex)); if (node != NULL && song != NULL) { if (strcmp(song->song.filePath, node->song.filePath) == 0 || (currentSong != NULL && currentSong->next != NULL && id == currentSong->next->id)) rebuild = true; } if (node != NULL) markAsDequeued(getLibrary(), node->song.filePath); Node *node2 = findSelectedEntryById(&playlist, id); if (node != NULL) deleteFromList(originalPlaylist, node); if (node2 != NULL) deleteFromList(&playlist, node2); updateLastPlaylistChangeTime(); if (isShuffleEnabled()) rebuild = true; currentSong = findSelectedEntryById(&playlist, currentId); if (rebuild) { node = NULL; nextSong = NULL; updatePlaylist(); tryNextSong = currentSong->next; nextSongNeedsRebuilding = false; nextSong = NULL; nextSong = getListNext(currentSong); rebuildNextSong(nextSong); loadedNextSong = true; } pthread_mutex_unlock(&(playlist.mutex)); refresh = true; } Node *getSongByNumber(PlayList *playlist, int songNumber) { Node *song = playlist->head; if (!song) return currentSong; if (songNumber <= 0) { return song; } int count = 1; while (song->next != NULL && count != songNumber) { song = getListNext(song); count++; } return song; } int loadDecoder(SongData *songData, bool *songDataDeleted) { int result = 0; if (songData != NULL) { *songDataDeleted = false; // this should only be done for the second song, as switchAudioImplementation() handles the first one if (!loadingdata.loadingFirstDecoder) { if (hasBuiltinDecoder(songData->filePath)) result = prepareNextDecoder(songData->filePath); else if (endsWith(songData->filePath, "opus")) result = prepareNextOpusDecoder(songData->filePath); else if (endsWith(songData->filePath, "ogg")) result = prepareNextVorbisDecoder(songData->filePath); else if (endsWith(songData->filePath, "m4a") || endsWith(songData->filePath, "aac") || endsWith(songData->filePath, "mp4")) result = prepareNextM4aDecoder(songData->filePath); } } return result; } int assignLoadedData() { int result = 0; if (loadingdata.loadA) { userData.songdataA = loadingdata.songdataA; result = loadDecoder(loadingdata.songdataA, &userData.songdataADeleted); } else { userData.songdataB = loadingdata.songdataB; result = loadDecoder(loadingdata.songdataB, &userData.songdataBDeleted); } return result; } void *songDataReaderThread(void *arg) { LoadingThreadData *loadingdata = (LoadingThreadData *)arg; // Acquire the mutex lock pthread_mutex_lock(&(loadingdata->mutex)); char filepath[MAXPATHLEN]; c_strcpy(filepath, sizeof(filepath), loadingdata->filePath); SongData *songdata = NULL; if (loadingdata->loadA) { if (!userData.songdataADeleted) { userData.songdataADeleted = true; unloadSongData(&loadingdata->songdataA); } } else { if (!userData.songdataBDeleted) { userData.songdataBDeleted = true; unloadSongData(&loadingdata->songdataB); } } if (filepath[0] != '\0') { songdata = loadSongData(filepath); } else songdata = NULL; if (loadingdata->loadA) { loadingdata->songdataA = songdata; } else { loadingdata->songdataB = songdata; } int result = assignLoadedData(); if (result < 0) songdata->hasErrors = true; // Release the mutex lock pthread_mutex_unlock(&(loadingdata->mutex)); if (songdata != NULL && songdata->hasErrors) { songHasErrors = true; clearingErrors = true; nextSong = NULL; } else { songHasErrors = false; clearingErrors = false; nextSong = tryNextSong; tryNextSong = NULL; } loadedNextSong = true; skipping = false; songLoading = false; return NULL; } void loadSong(Node *song, LoadingThreadData *loadingdata) { if (song == NULL) { loadedNextSong = true; skipping = false; songLoading = false; return; } c_strcpy(loadingdata->filePath, sizeof(loadingdata->filePath), song->song.filePath); pthread_t loadingThread; pthread_create(&loadingThread, NULL, songDataReaderThread, (void *)loadingdata); } void loadNext(LoadingThreadData *loadingdata) { nextSong = getListNext(currentSong); if (nextSong == NULL) { c_strcpy(loadingdata->filePath, sizeof(loadingdata->filePath), ""); } else { c_strcpy(loadingdata->filePath, sizeof(loadingdata->filePath), nextSong->song.filePath); } pthread_t loadingThread; pthread_create(&loadingThread, NULL, songDataReaderThread, (void *)loadingdata); } void rebuildNextSong(Node *song) { if (song == NULL) return; loadingdata.loadA = !usingSongDataA; loadingdata.loadingFirstDecoder = false; songLoading = true; loadSong(song, &loadingdata); int maxNumTries = 50; int numtries = 0; while (songLoading && !loadedNextSong && !loadingFailed && numtries < maxNumTries) { c_sleep(100); numtries++; } songLoading = false; } void skipToNextSong() { if (currentSong == NULL || currentSong->next == NULL) { return; } if (songLoading || nextSongNeedsRebuilding || skipping || clearingErrors) return; playbackPlay(&totalPauseSeconds, &pauseSeconds); skipping = true; updateLastSongSwitchTime(); skip(); } void skipToPrevSong() { if ((currentSong == NULL || currentSong->prev == NULL) && !isShuffleEnabled()) { return; } if (songLoading || skipping || clearingErrors) if (!forceSkip) return; if (isShuffleEnabled() && currentSong != NULL && currentSong->prev == NULL) { if (currentSong->prev == NULL && currentSong->next != NULL) currentSong = currentSong->next; else return; } else currentSong = currentSong->prev; playbackPlay(&totalPauseSeconds, &pauseSeconds); skipping = true; skipOutOfOrder = true; loadedNextSong = false; songLoading = true; forceSkip = false; loadingdata.loadA = !usingSongDataA; loadingdata.loadingFirstDecoder = true; loadSong(currentSong, &loadingdata); int maxNumTries = 50; int numtries = 0; while (!loadedNextSong && !loadingFailed && numtries < maxNumTries) { c_sleep(100); numtries++; } if (songHasErrors) { songHasErrors = false; forceSkip = true; skipToPrevSong(); } updateLastSongSwitchTime(); skip(); } void skipToSong(int id) { if (songLoading || !loadedNextSong || skipping || skipOutOfOrder || clearingErrors) if (!forceSkip) return; Node *found = NULL; findNodeInList(&playlist, id, &found); if (found != NULL) currentSong = found; else { return; } skipping = true; skipOutOfOrder = true; loadedNextSong = false; songLoading = true; forceSkip = false; playbackPlay(&totalPauseSeconds, &pauseSeconds); loadingdata.loadA = !usingSongDataA; loadingdata.loadingFirstDecoder = true; loadSong(currentSong, &loadingdata); int maxNumTries = 50; int numtries = 0; while (!loadedNextSong && !loadingFailed && numtries < maxNumTries) { c_sleep(100); numtries++; } if (songHasErrors) { songHasErrors = false; forceSkip = true; if (currentSong->next != NULL) skipToSong(currentSong->next->id); } updateLastSongSwitchTime(); skip(); } void skipToNumberedSong(int songNumber) { if (songLoading || !loadedNextSong || skipping || clearingErrors) if (!forceSkip) return; playbackPlay(&totalPauseSeconds, &pauseSeconds); skipping = true; skipOutOfOrder = true; loadedNextSong = false; songLoading = true; forceSkip = false; currentSong = getSongByNumber(originalPlaylist, songNumber); loadingdata.loadA = !usingSongDataA; loadingdata.loadingFirstDecoder = true; loadSong(currentSong, &loadingdata); int maxNumTries = 50; int numtries = 0; while (!loadedNextSong && !loadingFailed && numtries < maxNumTries) { c_sleep(100); numtries++; } if (songHasErrors) { songHasErrors = false; forceSkip = true; if (songNumber < playlist.count) skipToNumberedSong(songNumber + 1); } updateLastSongSwitchTime(); skip(); } void skipToLastSong() { Node *song = playlist.head; if (!song) return; int count = 1; while (song->next != NULL) { song = getListNext(song); count++; } skipToNumberedSong(count); } void loadFirstSong(Node *song) { if (song == NULL) return; loadingdata.loadingFirstDecoder = true; loadSong(song, &loadingdata); int i = 0; while (!loadedNextSong) { if (i != 0 && i % 1000 == 0 && uiEnabled) printf("."); i++; c_sleep(10); fflush(stdout); } } void unloadPreviousSong() { pthread_mutex_lock(&(loadingdata.mutex)); if (usingSongDataA && (skipping || (userData.currentSongData == NULL || userData.songdataADeleted == false || (loadingdata.songdataA != NULL && userData.songdataADeleted == false && userData.currentSongData->hasErrors == 0 && userData.currentSongData->trackId != NULL && strcmp(loadingdata.songdataA->trackId, userData.currentSongData->trackId) != 0)))) { userData.songdataADeleted = true; unloadSongData(&loadingdata.songdataA); usingSongDataA = false; if (!audioData.endOfListReached) loadedNextSong = false; } else if (!usingSongDataA && (skipping || (userData.currentSongData == NULL || userData.songdataBDeleted == false || (loadingdata.songdataB != NULL && userData.songdataBDeleted == false && userData.currentSongData->hasErrors == 0 && userData.currentSongData->trackId != NULL && strcmp(loadingdata.songdataB->trackId, userData.currentSongData->trackId) != 0)))) { userData.songdataBDeleted = true; unloadSongData(&loadingdata.songdataB); usingSongDataA = true; if (!audioData.endOfListReached) loadedNextSong = false; } pthread_mutex_unlock(&(loadingdata.mutex)); } int loadFirst(Node *song) { loadFirstSong(song); usingSongDataA = true; while (songHasErrors && currentSong->next != NULL) { songHasErrors = false; loadedNextSong = false; currentSong = currentSong->next; loadFirstSong(currentSong); } if (songHasErrors) { // couldn't play any of the songs unloadPreviousSong(); currentSong = NULL; songHasErrors = false; return -1; } userData.currentPCMFrame = 0; userData.currentSongData = userData.songdataA; return 0; } void *updateLibraryThread(void *arg) { char *path = (char *)arg; int tmpDirectoryTreeEntries = 0; FileSystemEntry *temp = createDirectoryTree(path, &tmpDirectoryTreeEntries); pthread_mutex_lock(&switchMutex); freeTree(library); library = temp; numDirectoryTreeEntries = tmpDirectoryTreeEntries; resetChosenDir(); pthread_mutex_unlock(&switchMutex); refresh = true; return NULL; } void updateLibrary(char *path) { pthread_t threadId; if (pthread_create(&threadId, NULL, updateLibraryThread, path) != 0) { perror("Failed to create thread"); return; } } void askIfCacheLibrary() { if (cacheLibrary > -1) // Only use this function if cacheLibrary isn't set return; char input = '\0'; restoreTerminalMode(); enableInputBuffering(); showCursor(); printf("Would you like to enable the library cache for quicker startup times?\nYou can update the cache at any time by pressing 'u'. (y/n): "); fflush(stdout); do { int res = scanf(" %c", &input); if (res < 0) break; } while (input != 'Y' && input != 'y' && input != 'N' && input != 'n'); if (input == 'Y' || input == 'y') { printf("Y\n"); cacheLibrary = 1; } else { printf("N\n"); cacheLibrary = 0; } setNonblockingMode(); disableInputBuffering(); hideCursor(); } void createLibrary(AppSettings *settings) { if (cacheLibrary > 0) { char *libFilepath = getLibraryFilePath(); library = reconstructTreeFromFile(libFilepath, settings->path, &numDirectoryTreeEntries); free(libFilepath); } if (library == NULL || library->children == NULL) { struct timeval start, end; gettimeofday(&start, NULL); library = createDirectoryTree(settings->path, &numDirectoryTreeEntries); gettimeofday(&end, NULL); long seconds = end.tv_sec - start.tv_sec; long microseconds = end.tv_usec - start.tv_usec; double elapsed = seconds + microseconds * 1e-6; // If time to load the library was significant, ask to use cache instead if (elapsed > ASK_IF_USE_CACHE_LIMIT_SECONDS) { askIfCacheLibrary(); } } if (library == NULL || library->children == NULL) { exit(0); } } kew-2.4.2/src/playerops.h000066400000000000000000000054001457005474400152740ustar00rootroot00000000000000 #ifndef PLAYEROPS_H #define PLAYEROPS_H #include #include #include "player.h" #include "songloader.h" #include "settings.h" #include "soundcommon.h" #ifndef CLOCK_MONOTONIC #define CLOCK_MONOTONIC 1 #endif #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif typedef struct { char filePath[MAXPATHLEN]; SongData *songdataA; SongData *songdataB; bool loadA; bool loadingFirstDecoder; pthread_mutex_t mutex; } LoadingThreadData; extern GDBusConnection *connection; extern LoadingThreadData loadingdata; extern double elapsedSeconds; extern double pauseSeconds; extern double totalPauseSeconds; extern struct timespec pause_time; extern volatile bool loadedNextSong; extern bool playlistNeedsUpdate; extern bool nextSongNeedsRebuilding; extern bool enqueuedNeedsUpdate; extern bool waitingForPlaylist; extern bool waitingForNext; extern bool usingSongDataA; extern Node *nextSong; extern int lastPlayedId; extern bool playingMainPlaylist; extern bool songHasErrors; extern bool doQuit; extern bool loadingFailed; extern volatile bool clearingErrors; extern volatile bool songLoading; extern struct timespec start_time; extern bool skipping; extern bool skipOutOfOrder; extern Node *tryNextSong; extern struct timespec lastInputTime; extern struct timespec lastPlaylistChangeTime; extern UserData userData; SongData *getCurrentSongData(void); void rebuildAndUpdatePlaylist(); Node *getNextSong(); void handleRemove(); void enqueueSongs(); void updateLastSongSwitchTime(void); void updateLastInputTime(void); void playbackPause(struct timespec *pause_time); void playbackPlay(double *totalPauseSeconds, double *pauseSeconds); void togglePause(double *totalPauseSeconds, double *pauseSeconds, struct timespec *pause_time); void toggleRepeat(void); void toggleShuffle(void); void addToSpecialPlaylist(void); void toggleBlocks(AppSettings *settings); void toggleColors(AppSettings *settings); void toggleCovers(AppSettings *settings); void toggleVisualizer(AppSettings *settings); void quit(void); void calcElapsedTime(void); Node *getSongByNumber(PlayList *playlist, int songNumber); void skipToNextSong(void); void skipToPrevSong(void); void skipToSong(int id); void seekForward(void); void seekBack(void); void skipToNumberedSong(int songNumber); void skipToLastSong(void); void loadSong(Node *song, LoadingThreadData *loadingdata); void loadNext(LoadingThreadData *loadingdata); int loadFirst(Node *song); void flushSeek(void); Node *findSelectedEntryById(PlayList *playlist, int id); void emitSeekedSignal(double newPositionSeconds); void rebuildNextSong(Node *song); void updateLibrary(char *path); void askIfCacheLibrary(); void unloadPreviousSong(); void createLibrary(AppSettings *settings); #endif kew-2.4.2/src/playlist.c000066400000000000000000000532461457005474400151250ustar00rootroot00000000000000#define _XOPEN_SOURCE 700 #define __USE_XOPEN_EXTENDED 1 #include "playlist.h" /* playlist.c Playlist related functions. */ #define MAX_SEARCH_SIZE 256 #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif const char PLAYLIST_EXTENSIONS[] = "\\.(m3u)$"; const char mainPlaylistName[] = "kew.m3u"; // The playlist unshuffled as it appears in playlist view PlayList *originalPlaylist = NULL; // The (sometimes shuffled) sequence of songs that will be played PlayList playlist = {NULL, NULL, 0, PTHREAD_MUTEX_INITIALIZER}; // The playlist from kew.m3u PlayList *specialPlaylist = NULL; char search[MAX_SEARCH_SIZE]; char playlistName[MAX_SEARCH_SIZE]; bool shuffle = false; int numDirs = 0; volatile int stopPlaylistDurationThread = 0; Node *currentSong = NULL; int nodeIdCounter = 0; Node *getListNext(Node *node) { return (node == NULL) ? NULL : node->next; } Node *getListPrev(Node *node) { return (node == NULL) ? NULL : node->prev; } void addToList(PlayList *list, Node *newNode) { if (list->count >= MAX_FILES) return; list->count++; if (list->head == NULL) { newNode->prev = NULL; list->head = newNode; list->tail = newNode; } else { newNode->prev = list->tail; list->tail->next = newNode; list->tail = newNode; } } Node *deleteFromList(PlayList *list, Node *node) { if (list->head == NULL || node == NULL) return NULL; if (list->head == node) { list->head = node->next; if (list->head == NULL) { list->tail = NULL; } } if (node == list->tail) list->tail = node->prev; if (node->prev != NULL) node->prev->next = node->next; if (node->next != NULL) node->next->prev = node->prev; if (node->song.filePath != NULL) free(node->song.filePath); Node *nextNode = node->next; free(node); list->count--; return nextNode; } void deletePlaylist(PlayList *list) { if (list == NULL) return; Node *current = list->head; while (current != NULL) { Node *next = current->next; free(current->song.filePath); free(current); current = next; } // Reset the playlist list->head = NULL; list->tail = NULL; list->count = 0; } void shufflePlaylist(PlayList *playlist) { if (playlist == NULL || playlist->count <= 1) { return; // No need to shuffle } // Convert the linked list to an array Node **nodes = (Node **)malloc(playlist->count * sizeof(Node *)); if (nodes == NULL) { printf("Memory allocation error.\n"); exit(0); } Node *current = playlist->head; int i = 0; while (current != NULL) { nodes[i++] = current; current = current->next; } // Shuffle the array using Fisher-Yates algorithm for (int j = playlist->count - 1; j >= 1; --j) { int k = rand() % (j + 1); Node *temp = nodes[j]; nodes[j] = nodes[k]; nodes[k] = temp; } playlist->head = nodes[0]; playlist->tail = nodes[playlist->count - 1]; for (int j = 0; j < playlist->count; ++j) { nodes[j]->next = (j < playlist->count - 1) ? nodes[j + 1] : NULL; nodes[j]->prev = (j > 0) ? nodes[j - 1] : NULL; } free(nodes); } void insertAsFirst(Node *currentSong, PlayList *playlist) { if (currentSong == NULL || playlist == NULL) { return; } if (playlist->head == NULL) { currentSong->next = NULL; currentSong->prev = NULL; playlist->head = currentSong; playlist->tail = currentSong; } else { if (currentSong != playlist->head) { if (currentSong->next != NULL) { currentSong->next->prev = currentSong->prev; } else { playlist->tail = currentSong->prev; } if (currentSong->prev != NULL) { currentSong->prev->next = currentSong->next; } // Add the currentSong as the new head currentSong->next = playlist->head; currentSong->prev = NULL; playlist->head->prev = currentSong; playlist->head = currentSong; } } } void shufflePlaylistStartingFromSong(PlayList *playlist, Node *song) { shufflePlaylist(playlist); if (song != NULL && playlist->count > 1) { insertAsFirst(song, playlist); } } int compare(const struct dirent **a, const struct dirent **b) { const char *nameA = (*a)->d_name; const char *nameB = (*b)->d_name; if (nameA[0] == '_' && nameB[0] != '_') { return -1; } else if (nameA[0] != '_' && nameB[0] == '_') { return 1; } return strcmp(nameA, nameB); } void createNode(Node **node, const char *directoryPath, int id) { SongInfo song; song.filePath = strdup(directoryPath); song.duration = 0.0; *node = (Node *)malloc(sizeof(Node)); if (*node == NULL) { printf("Failed to allocate memory."); exit(0); return; } (*node)->song = song; (*node)->next = NULL; (*node)->prev = NULL; (*node)->id = id; } void buildPlaylistRecursive(const char *directoryPath, const char *allowedExtensions, PlayList *playlist) { int res = isDirectory(directoryPath); if (res != 1 && res != -1 && directoryPath != NULL) { Node *node = NULL; createNode(&node, directoryPath, nodeIdCounter++); addToList(playlist, node); return; } DIR *dir = opendir(directoryPath); if (dir == NULL) { printf("Failed to open directory: %s\n", directoryPath); return; } regex_t regex; int ret = regcomp(®ex, allowedExtensions, REG_EXTENDED); if (ret != 0) { printf("Failed to compile regular expression\n"); closedir(dir); return; } char exto[6]; struct dirent **entries; int numEntries = scandir(directoryPath, &entries, NULL, compare); if (numEntries < 0) { printf("Failed to scan directory: %s\n", directoryPath); return; } for (int i = 0; i < numEntries && playlist->count < MAX_FILES; i++) { struct dirent *entry = entries[i]; if (entry->d_name[0] == '.' || strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } char filePath[FILENAME_MAX]; snprintf(filePath, sizeof(filePath), "%s/%s", directoryPath, entry->d_name); if (isDirectory(filePath)) { int songCount = playlist->count; buildPlaylistRecursive(filePath, allowedExtensions, playlist); if (playlist->count > songCount) numDirs++; } else { extractExtension(entry->d_name, sizeof(exto) - 1, exto); if (match_regex(®ex, exto) == 0) { snprintf(filePath, sizeof(filePath), "%s/%s", directoryPath, entry->d_name); Node *node = NULL; createNode(&node, filePath, nodeIdCounter++); addToList(playlist, node); } } } for (int i = 0; i < numEntries; i++) { free(entries[i]); } free(entries); closedir(dir); regfree(®ex); } int playDirectory(const char *directoryPath, const char *allowedExtensions, PlayList *playlist) { DIR *dir = opendir(directoryPath); if (dir == NULL) { printf("Failed to open directory: %s\n", directoryPath); return -1; } regex_t regex; int ret = regcomp(®ex, allowedExtensions, REG_EXTENDED); if (ret != 0) { return -1; } char ext[6]; struct dirent *entry; while ((entry = readdir(dir)) != NULL) { extractExtension(entry->d_name, sizeof(ext) - 1, ext); if (match_regex(®ex, ext) == 0) { char filePath[FILENAME_MAX]; snprintf(filePath, sizeof(filePath), "%s/%s", directoryPath, entry->d_name); Node *node = NULL; createNode(&node, filePath, nodeIdCounter++); addToList(playlist, node); } } closedir(dir); return 0; } int joinPlaylist(PlayList *dest, PlayList *src) { if (src->count == 0) { return 0; } if (dest->count == 0) { dest->head = src->head; dest->tail = src->tail; } else { dest->tail->next = src->head; src->head->prev = dest->tail; dest->tail = src->tail; } dest->count += src->count; src->head = NULL; src->tail = NULL; src->count = 0; return 1; } void makePlaylistName(const char *search) { char *duplicateSearch = strdup(search); strcat(playlistName, duplicateSearch); free(duplicateSearch); strcat(playlistName, ".m3u"); int i = 0; while (playlistName[i] != '\0') { if (playlistName[i] == ':') { playlistName[i] = '-'; } i++; } } void readM3UFile(const char *filename, PlayList *playlist) { FILE *file = fopen(filename, "r"); char directory[MAXPATHLEN]; if (file == NULL) { return; } getDirectoryFromPath(filename, directory); char line[MAXPATHLEN]; while (fgets(line, sizeof(line), file)) { size_t len = strcspn(line, "\r\n"); line[len] = '\0'; size_t start = 0; while (isspace(line[start])) { start++; } size_t end = strlen(line); while (end > start && isspace(line[end - 1])) { end--; } line[end] = '\0'; if (line[0] != '#' && line[0] != '\0') { char songPath[MAXPATHLEN]; memset(songPath, '\0', sizeof(songPath)); if (strchr(line, '/') == NULL && strchr(line, '\\') == NULL) strcat(songPath, directory); strcat(songPath, line); Node *newNode = NULL; createNode(&newNode, songPath, nodeIdCounter++); if (playlist->head == NULL) { playlist->head = newNode; playlist->tail = newNode; } else { playlist->tail->next = newNode; newNode->prev = playlist->tail; playlist->tail = newNode; } playlist->count++; } } fclose(file); } int makePlaylist(int argc, char *argv[], bool exactSearch, const char *path) { enum SearchType searchType = SearchAny; int searchTypeIndex = 1; const char *delimiter = ":"; PlayList partialPlaylist = {NULL, NULL, 0, PTHREAD_MUTEX_INITIALIZER}; const char *allowedExtensions = AUDIO_EXTENSIONS; if (strcmp(argv[1], "all") == 0) { searchType = ReturnAllSongs; shuffle = true; } if (argc > 1) { if (strcmp(argv[1], "list") == 0 && argc > 2) { allowedExtensions = PLAYLIST_EXTENSIONS; searchType = SearchPlayList; } if (strcmp(argv[1], "random") == 0 || strcmp(argv[1], "rand") == 0 || strcmp(argv[1], "shuffle") == 0) { int count = 0; while (argv[count] != NULL) { count++; } if (count > 2) { searchTypeIndex = 2; shuffle = true; } } if (strcmp(argv[searchTypeIndex], "dir") == 0) searchType = DirOnly; else if (strcmp(argv[searchTypeIndex], "song") == 0) searchType = FileOnly; } int start = searchTypeIndex + 1; if (searchType == FileOnly || searchType == DirOnly || searchType == SearchPlayList) start = searchTypeIndex + 2; search[0] = '\0'; for (int i = start - 1; i < argc; i++) { strcat(search, " "); strcat(search, argv[i]); } makePlaylistName(search); if (strstr(search, delimiter)) { shuffle = true; } if (searchType == ReturnAllSongs) { pthread_mutex_lock(&(playlist.mutex)); buildPlaylistRecursive(path, allowedExtensions, &playlist); pthread_mutex_unlock(&(playlist.mutex)); } else { char *token = strtok(search, delimiter); while (token != NULL) { char buf[MAXPATHLEN] = {0}; if (strncmp(token, "song", 4) == 0) { memmove(token, token + 4, strlen(token + 4) + 1); searchType = FileOnly; } trim(token); if (walker(path, token, buf, allowedExtensions, searchType, exactSearch) == 0) { if (strcmp(argv[1], "list") == 0) { readM3UFile(buf, &playlist); } else { pthread_mutex_lock(&(playlist.mutex)); buildPlaylistRecursive(buf, allowedExtensions, &partialPlaylist); joinPlaylist(&playlist, &partialPlaylist); pthread_mutex_unlock(&(playlist.mutex)); } } token = strtok(NULL, delimiter); } } if (numDirs > 1) shuffle = true; if (shuffle) shufflePlaylist(&playlist); if (playlist.head == NULL) printf("Music not found\n"); return 0; } void writeM3UFile(const char *filename, PlayList *playlist) { FILE *file = fopen(filename, "w"); if (file == NULL) { return; } Node *currentNode = playlist->head; while (currentNode != NULL) { fprintf(file, "%s\n", currentNode->song.filePath); currentNode = currentNode->next; } fclose(file); } void loadSpecialPlaylist(const char *directory) { char playlistPath[MAXPATHLEN]; c_strcpy(playlistPath, sizeof(playlistPath), directory); if (playlistPath[strlen(playlistPath) - 1] != '/') strcat(playlistPath, "/"); strcat(playlistPath, mainPlaylistName); specialPlaylist = malloc(sizeof(PlayList)); if (specialPlaylist == NULL) { printf("Failed to allocate memory for special playlist.\n"); exit(0); } specialPlaylist->count = 0; specialPlaylist->head = NULL; specialPlaylist->tail = NULL; readM3UFile(playlistPath, specialPlaylist); } void saveSpecialPlaylist(const char *directory, bool isPlayingMain) { char playlistPath[MAXPATHLEN]; c_strcpy(playlistPath, sizeof(playlistPath), directory); if (playlistPath[strlen(playlistPath) - 1] != '/') strcat(playlistPath, "/"); strcat(playlistPath, mainPlaylistName); if (isPlayingMain && playlist.count > 0) writeM3UFile(playlistPath, &playlist); else if (specialPlaylist != NULL && specialPlaylist->count > 0) writeM3UFile(playlistPath, specialPlaylist); } void savePlaylist(const char *path) { char playlistPath[MAXPATHLEN]; c_strcpy(playlistPath, sizeof(playlistPath), path); if (playlistPath[strlen(playlistPath) - 1] != '/') strcat(playlistPath, "/"); strcat(playlistPath, playlistName); writeM3UFile(playlistPath, &playlist); } Node *deepCopyNode(Node *originalNode) { if (originalNode == NULL) { return NULL; } Node *newNode = malloc(sizeof(Node)); if (newNode == NULL) { return NULL; } newNode->song.filePath = strdup(originalNode->song.filePath); newNode->song.duration = originalNode->song.duration; newNode->prev = NULL; newNode->id = originalNode->id; newNode->next = deepCopyNode(originalNode->next); if (newNode->next != NULL) { newNode->next->prev = newNode; } return newNode; } Node *findTail(Node *head) { if (head == NULL) return NULL; Node *current = head; while (current->next != NULL) { current = current->next; } return current; } PlayList deepCopyPlayList(PlayList *originalList) { PlayList newList = {NULL, NULL, 0, PTHREAD_MUTEX_INITIALIZER}; deepCopyPlayListOntoList(originalList, &newList); return newList; } void deepCopyPlayListOntoList(PlayList *originalList, PlayList *newList) { if (originalList == NULL) { return; } newList->head = deepCopyNode(originalList->head); newList->tail = findTail(newList->head); newList->count = originalList->count; } Node *findPathInPlaylist(char *path, PlayList *playlist) { Node *currentNode = playlist->head; while (currentNode != NULL) { if (strcmp(currentNode->song.filePath, path) == 0) { return currentNode; } currentNode = currentNode->next; } return NULL; } Node *findLastPathInPlaylist(char *path, PlayList *playlist) { Node *currentNode = playlist->tail; while (currentNode != NULL) { if (strcmp(currentNode->song.filePath, path) == 0) { return currentNode; } currentNode = currentNode->prev; } return NULL; } int findNodeInList(PlayList *list, int id, Node **foundNode) { Node *node = list->head; int row = 0; while (node != NULL) { if (id == node->id) { *foundNode = node; return row; } node = node->next; row++; } *foundNode = NULL; return -1; } void addSongToPlayList(PlayList *list, const char *filePath, int playlistMax) { if (list->count >= playlistMax) return; Node *newNode = NULL; createNode(&newNode, filePath, list->count); addToList(list, newNode); } void traverseFileSystemEntry(FileSystemEntry *entry, PlayList *list, int playlistMax) { if (entry == NULL || list->count >= playlistMax) return; if (entry->isDirectory == 0) { addSongToPlayList(list, entry->fullPath, playlistMax); } if (entry->isDirectory == 1 && entry->children != NULL) { traverseFileSystemEntry(entry->children, list, playlistMax); } if (entry->next != NULL) { traverseFileSystemEntry(entry->next, list, playlistMax); } } void createPlayListFromFileSystemEntry(FileSystemEntry *root, PlayList *list, int playlistMax) { traverseFileSystemEntry(root, list, playlistMax); } kew-2.4.2/src/playlist.h000066400000000000000000000035741457005474400151310ustar00rootroot00000000000000#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef __USE_XOPEN_EXTENDED #define __USE_XOPEN_EXTENDED #endif #include #include #include #include #include #include "directorytree.h" #include "file.h" #define MAX_FILES 10000 #ifndef PLAYLIST_STRUCT #define PLAYLIST_STRUCT typedef struct { char *filePath; double duration; } SongInfo; typedef struct Node { int id; SongInfo song; struct Node *next; struct Node *prev; } Node; typedef struct { Node *head; Node *tail; int count; pthread_mutex_t mutex; } PlayList; extern Node *currentSong; #endif extern PlayList playlist; extern PlayList *specialPlaylist; extern PlayList *originalPlaylist; extern int nodeIdCounter; Node *getListNext(Node *node); Node *getListPrev(Node *node); void createNode(Node **node, const char *directoryPath, int id); void addToList(PlayList *list, Node *newNode); Node *deleteFromList(PlayList *list, Node *node); void deletePlaylist(PlayList *playlist); void shufflePlaylist(PlayList *playlist); void shufflePlaylistStartingFromSong(PlayList *playlist, Node *song); int makePlaylist(int argc, char *argv[], bool exactSearch, const char *path); void writeM3UFile(const char *filename, PlayList *playlist); void loadSpecialPlaylist(const char *directory); void saveSpecialPlaylist(const char *directory, bool isPlayingMain); void savePlaylist(const char *path); PlayList deepCopyPlayList(PlayList *originalList); void deepCopyPlayListOntoList(PlayList *originalList, PlayList *newList); Node *findPathInPlaylist(char *path, PlayList *playlist); Node *findLastPathInPlaylist(char *path, PlayList *playlist); int findNodeInList(PlayList *list, int id, Node **foundNode); void createPlayListFromFileSystemEntry(FileSystemEntry *root, PlayList *list, int playlistMax); kew-2.4.2/src/settings.c000066400000000000000000000551431457005474400151220ustar00rootroot00000000000000#include "settings.h" /* settings.c Functions related to the config file. */ #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif const char SETTINGS_FILE[] = "kewrc"; void freeKeyValuePairs(KeyValuePair *pairs, int count) { for (int i = 0; i < count; i++) { free(pairs[i].key); free(pairs[i].value); } free(pairs); } AppSettings constructAppSettings(KeyValuePair *pairs, int count) { AppSettings settings; memset(&settings, 0, sizeof(settings)); strncpy(settings.coverEnabled, "1", sizeof(settings.coverEnabled)); strncpy(settings.allowNotifications, "1", sizeof(settings.allowNotifications)); strncpy(settings.coverAnsi, "0", sizeof(settings.coverAnsi)); strncpy(settings.visualizerEnabled, "1", sizeof(settings.visualizerEnabled)); strncpy(settings.useProfileColors, "1", sizeof(settings.useProfileColors)); strncpy(settings.hideLogo, "0", sizeof(settings.hideLogo)); strncpy(settings.hideHelp, "0", sizeof(settings.hideHelp)); strncpy(settings.cacheLibrary, "-1", sizeof(settings.cacheLibrary)); strncpy(settings.volumeUp, "+", sizeof(settings.volumeUp)); strncpy(settings.volumeUpAlt, "=", sizeof(settings.volumeUpAlt)); strncpy(settings.volumeDown, "-", sizeof(settings.volumeDown)); strncpy(settings.previousTrackAlt, "h", sizeof(settings.previousTrackAlt)); strncpy(settings.nextTrackAlt, "l", sizeof(settings.nextTrackAlt)); strncpy(settings.scrollUpAlt, "k", sizeof(settings.scrollUpAlt)); strncpy(settings.scrollDownAlt, "j", sizeof(settings.scrollDownAlt)); strncpy(settings.toggleColorsDerivedFrom, "i", sizeof(settings.toggleColorsDerivedFrom)); strncpy(settings.toggleVisualizer, "v", sizeof(settings.toggleVisualizer)); strncpy(settings.toggleCovers, "c", sizeof(settings.toggleCovers)); strncpy(settings.toggleAscii, "b", sizeof(settings.toggleAscii)); strncpy(settings.toggleRepeat, "r", sizeof(settings.toggleRepeat)); strncpy(settings.toggleShuffle, "s", sizeof(settings.toggleShuffle)); strncpy(settings.togglePause, "p", sizeof(settings.togglePause)); strncpy(settings.seekBackward, "a", sizeof(settings.seekBackward)); strncpy(settings.seekForward, "d", sizeof(settings.seekForward)); strncpy(settings.savePlaylist, "x", sizeof(settings.savePlaylist)); strncpy(settings.updateLibrary, "u", sizeof(settings.updateLibrary)); strncpy(settings.addToMainPlaylist, ".", sizeof(settings.addToMainPlaylist)); strncpy(settings.hardPlayPause, " ", sizeof(settings.hardPlayPause)); strncpy(settings.hardSwitchNumberedSong, "\n", sizeof(settings.hardSwitchNumberedSong)); strncpy(settings.hardPrev, "[D", sizeof(settings.hardPrev)); strncpy(settings.hardNext, "[C", sizeof(settings.hardNext)); strncpy(settings.hardScrollUp, "[A", sizeof(settings.hardScrollUp)); strncpy(settings.hardScrollDown, "[B", sizeof(settings.hardScrollDown)); strncpy(settings.hardShowInfo, "OQ", sizeof(settings.hardShowInfo)); strncpy(settings.hardShowInfoAlt, "[[B", sizeof(settings.hardShowInfoAlt)); strncpy(settings.hardShowKeys, "[15~", sizeof(settings.hardShowKeys)); strncpy(settings.hardShowKeysAlt, "[[E", sizeof(settings.hardShowKeysAlt)); strncpy(settings.hardShowTrack, "OS", sizeof(settings.hardShowTrack)); strncpy(settings.hardShowTrackAlt, "[[D", sizeof(settings.hardShowTrackAlt)); strncpy(settings.hardEndOfPlaylist, "G", sizeof(settings.hardEndOfPlaylist)); strncpy(settings.hardShowLibrary, "OR", sizeof(settings.hardShowLibrary)); strncpy(settings.hardShowLibraryAlt, "[[C", sizeof(settings.hardShowLibraryAlt)); strncpy(settings.hardNextPage, "[6~", sizeof(settings.hardNextPage)); strncpy(settings.hardPrevPage, "[5~", sizeof(settings.hardPrevPage)); strncpy(settings.hardRemove, "[3~", sizeof(settings.hardRemove)); strncpy(settings.lastVolume, "100", sizeof(settings.lastVolume)); strncpy(settings.color, "6", sizeof(settings.color)); strncpy(settings.artistColor, "6", sizeof(settings.artistColor)); strncpy(settings.enqueuedColor, "6", sizeof(settings.enqueuedColor)); strncpy(settings.titleColor, "6", sizeof(settings.titleColor)); strncpy(settings.quit, "q", sizeof(settings.quit)); if (pairs == NULL) { return settings; } for (int i = 0; i < count; i++) { KeyValuePair *pair = &pairs[i]; if (strcmp(stringToLower(pair->key), "path") == 0) { snprintf(settings.path, sizeof(settings.path), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "coverenabled") == 0) { snprintf(settings.coverEnabled, sizeof(settings.coverEnabled), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "coveransi") == 0) { snprintf(settings.coverAnsi, sizeof(settings.coverAnsi), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "visualizerenabled") == 0) { snprintf(settings.visualizerEnabled, sizeof(settings.visualizerEnabled), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "useprofilecolors") == 0) { snprintf(settings.useProfileColors, sizeof(settings.useProfileColors), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "visualizerheight") == 0) { snprintf(settings.visualizerHeight, sizeof(settings.visualizerHeight), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "volumeup") == 0) { snprintf(settings.volumeUp, sizeof(settings.volumeUp), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "volumeupalt") == 0) { snprintf(settings.volumeUpAlt, sizeof(settings.volumeUpAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "volumedown") == 0) { snprintf(settings.volumeDown, sizeof(settings.volumeDown), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "previoustrackalt") == 0) { snprintf(settings.previousTrackAlt, sizeof(settings.previousTrackAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "nexttrackalt") == 0) { snprintf(settings.nextTrackAlt, sizeof(settings.nextTrackAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "scrollupalt") == 0) { snprintf(settings.scrollUpAlt, sizeof(settings.scrollUpAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "scrolldownalt") == 0) { snprintf(settings.scrollDownAlt, sizeof(settings.scrollDownAlt), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "switchnumberedsong") == 0) { snprintf(settings.switchNumberedSong, sizeof(settings.switchNumberedSong), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "togglepause") == 0) { snprintf(settings.togglePause, sizeof(settings.togglePause), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "togglecolorsderivedfrom") == 0) { snprintf(settings.toggleColorsDerivedFrom, sizeof(settings.toggleColorsDerivedFrom), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "togglevisualizer") == 0) { snprintf(settings.toggleVisualizer, sizeof(settings.toggleVisualizer), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "togglecovers") == 0) { snprintf(settings.toggleCovers, sizeof(settings.toggleCovers), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "toggleascii") == 0) { snprintf(settings.toggleAscii, sizeof(settings.toggleAscii), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "togglerepeat") == 0) { snprintf(settings.toggleRepeat, sizeof(settings.toggleRepeat), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "toggleshuffle") == 0) { snprintf(settings.toggleShuffle, sizeof(settings.toggleShuffle), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "seekbackward") == 0) { snprintf(settings.seekBackward, sizeof(settings.seekBackward), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "seekforward") == 0) { snprintf(settings.seekForward, sizeof(settings.seekForward), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "saveplaylist") == 0) { snprintf(settings.savePlaylist, sizeof(settings.savePlaylist), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "addtomainplaylist") == 0) { snprintf(settings.quit, sizeof(settings.quit), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "lastvolume") == 0) { snprintf(settings.lastVolume, sizeof(settings.lastVolume), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "allownotifications") == 0) { snprintf(settings.allowNotifications, sizeof(settings.allowNotifications), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "color") == 0) { snprintf(settings.color, sizeof(settings.color), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "artistcolor") == 0) { snprintf(settings.artistColor, sizeof(settings.artistColor), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "enqueuedcolor") == 0) { snprintf(settings.enqueuedColor, sizeof(settings.enqueuedColor), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "titlecolor") == 0) { snprintf(settings.titleColor, sizeof(settings.titleColor), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "hidelogo") == 0) { snprintf(settings.hideLogo, sizeof(settings.hideLogo), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "hidehelp") == 0) { snprintf(settings.hideHelp, sizeof(settings.hideHelp), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "cachelibrary") == 0) { snprintf(settings.cacheLibrary, sizeof(settings.cacheLibrary), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "quit") == 0) { snprintf(settings.quit, sizeof(settings.quit), "%s", pair->value); } else if (strcmp(stringToLower(pair->key), "updatelibrary") == 0) { snprintf(settings.updateLibrary, sizeof(settings.updateLibrary), "%s", pair->value); } } freeKeyValuePairs(pairs, count); return settings; } KeyValuePair *readKeyValuePairs(const char *file_path, int *count) { FILE *file = fopen(file_path, "r"); if (file == NULL) { // fprintf(stderr, "Error opening the settings file.\n"); return NULL; } KeyValuePair *pairs = NULL; int pair_count = 0; char line[256]; while (fgets(line, sizeof(line), file)) { // Remove trailing newline character if present line[strcspn(line, "\n")] = '\0'; char *delimiter = strchr(line, '='); if (delimiter != NULL) { *delimiter = '\0'; char *value = delimiter + 1; pair_count++; pairs = realloc(pairs, pair_count * sizeof(KeyValuePair)); KeyValuePair *current_pair = &pairs[pair_count - 1]; current_pair->key = strdup(line); current_pair->value = strdup(value); } } fclose(file); *count = pair_count; return pairs; } const char *getDefaultMusicFolder() { const char *home = getHomePath(); if (home != NULL) { static char musicPath[MAXPATHLEN]; snprintf(musicPath, sizeof(musicPath), "%s/Music", home); return musicPath; } else { return NULL; // Return NULL if XDG home is not found. } } int getMusicLibraryPath(char *path) { char expandedPath[MAXPATHLEN]; if (path[0] != '\0' && path[0] != '\r') { if (expandPath(path, expandedPath) >= 0) strcpy(path, expandedPath); } return 0; } void getConfig(AppSettings *settings) { int pair_count; char *configdir = getConfigPath(); char *filepath = NULL; size_t filepath_length = strlen(configdir) + strlen("/") + strlen(SETTINGS_FILE) + 1; filepath = (char *)malloc(filepath_length); strcpy(filepath, configdir); strcat(filepath, "/"); strcat(filepath, SETTINGS_FILE); KeyValuePair *pairs = readKeyValuePairs(filepath, &pair_count); free(filepath); *settings = constructAppSettings(pairs, pair_count); allowNotifications = (settings->allowNotifications[0] == '1'); coverEnabled = (settings->coverEnabled[0] == '1'); coverAnsi = (settings->coverAnsi[0] == '1'); visualizerEnabled = (settings->visualizerEnabled[0] == '1'); useProfileColors = (settings->useProfileColors[0] == '1'); hideLogo = (settings->hideLogo[0] == '1'); hideHelp = (settings->hideHelp[0] == '1'); int temp = atoi(settings->color); if (temp >= 0) mainColor = temp; temp = atoi(settings->artistColor); if (temp >= 0) artistColor = temp; temp = atoi(settings->enqueuedColor); if (temp >= 0) enqueuedColor = temp; temp = atoi(settings->titleColor); if (temp >= 0) titleColor = temp; int temp2 = atoi(settings->visualizerHeight); if (temp2 > 0) visualizerHeight = temp2; int temp3 = atoi(settings->lastVolume); if (temp3 >= 0) setVolume(temp3); int temp4 = atoi(settings->cacheLibrary); if (temp4 >= 0) cacheLibrary = temp4; getMusicLibraryPath(settings->path); free(configdir); } void setConfig(AppSettings *settings) { // Create the file path char *configdir = getConfigPath(); char *filepath = (char *)malloc(strlen(configdir) + strlen("/") + strlen(SETTINGS_FILE) + 1); strcpy(filepath, configdir); strcat(filepath, "/"); strcat(filepath, SETTINGS_FILE); FILE *file = fopen(filepath, "w"); if (file == NULL) { fprintf(stderr, "Error opening file: %s\n", filepath); free(filepath); free(configdir); return; } if (settings->allowNotifications[0] == '\0') allowNotifications ? c_strcpy(settings->allowNotifications, sizeof(settings->allowNotifications), "1") : c_strcpy(settings->allowNotifications, sizeof(settings->allowNotifications), "0"); if (settings->coverEnabled[0] == '\0') coverEnabled ? c_strcpy(settings->coverEnabled, sizeof(settings->coverEnabled), "1") : c_strcpy(settings->coverEnabled, sizeof(settings->coverEnabled), "0"); if (settings->coverAnsi[0] == '\0') coverAnsi ? c_strcpy(settings->coverAnsi, sizeof(settings->coverAnsi), "1") : c_strcpy(settings->coverAnsi, sizeof(settings->coverAnsi), "0"); if (settings->visualizerEnabled[0] == '\0') visualizerEnabled ? c_strcpy(settings->visualizerEnabled, sizeof(settings->visualizerEnabled), "1") : c_strcpy(settings->visualizerEnabled, sizeof(settings->visualizerEnabled), "0"); if (settings->useProfileColors[0] == '\0') useProfileColors ? c_strcpy(settings->useProfileColors, sizeof(settings->useProfileColors), "1") : c_strcpy(settings->useProfileColors, sizeof(settings->useProfileColors), "0"); if (settings->visualizerHeight[0] == '\0') { sprintf(settings->visualizerHeight, "%d", visualizerHeight); } if (settings->hideLogo[0] == '\0') hideLogo ? c_strcpy(settings->hideLogo, sizeof(settings->hideLogo), "1") : c_strcpy(settings->hideLogo, sizeof(settings->hideLogo), "0"); if (settings->hideHelp[0] == '\0') hideHelp ? c_strcpy(settings->hideHelp, sizeof(settings->hideHelp), "1") : c_strcpy(settings->hideHelp, sizeof(settings->hideHelp), "0"); sprintf(settings->cacheLibrary, "%d", cacheLibrary); sprintf(settings->lastVolume, "%d", getCurrentVolume()); // Null-terminate the character arrays settings->path[MAXPATHLEN - 1] = '\0'; settings->coverEnabled[1] = '\0'; settings->coverAnsi[1] = '\0'; settings->visualizerEnabled[1] = '\0'; settings->visualizerHeight[5] = '\0'; settings->lastVolume[5] = '\0'; settings->useProfileColors[1] = '\0'; settings->allowNotifications[1] = '\0'; settings->hideLogo[1] = '\0'; settings->hideHelp[1] = '\0'; settings->cacheLibrary[5] = '\0'; // Write the settings to the file fprintf(file, "# Make sure that kew is closed before editing this file in order for changes to take effect.\n\n"); fprintf(file, "path=%s\n", settings->path); // fprintf(file, "useThemeColors=%s\n", settings->useThemeColors); fprintf(file, "coverEnabled=%s\n", settings->coverEnabled); fprintf(file, "coverAnsi=%s\n", settings->coverAnsi); fprintf(file, "visualizerEnabled=%s\n", settings->visualizerEnabled); fprintf(file, "visualizerHeight=%s\n", settings->visualizerHeight); fprintf(file, "useProfileColors=%s\n", settings->useProfileColors); fprintf(file, "allowNotifications=%s\n", settings->allowNotifications); fprintf(file, "hideLogo=%s\n", settings->hideLogo); fprintf(file, "hideHelp=%s\n", settings->hideHelp); fprintf(file, "lastVolume=%s\n", settings->lastVolume); fprintf(file, "\n# Cache: Set to 1 to use cache of the library for faster startup times.\n"); fprintf(file, "cacheLibrary=%s\n", settings->cacheLibrary); fprintf(file, "\n# Color values are 0=Black, 1=Red, 2=Green, 3=Yellow, 4=Blue, 5=Magenta, 6=Cyan, 7=White\n"); fprintf(file, "# These mostly affect the library view.\n\n"); fprintf(file, "# Logo color: \n"); fprintf(file, "color=%s\n", settings->color); fprintf(file, "# Header color in library view: \n"); fprintf(file, "artistColor=%s\n", settings->artistColor); fprintf(file, "# Now playing song text in library view: \n"); fprintf(file, "titleColor=%s\n", settings->titleColor); fprintf(file, "# Color of enqueued songs in library view: \n"); fprintf(file, "enqueuedColor=%s\n", settings->enqueuedColor); fprintf(file, "\n# Key Bindings:\n\n"); fprintf(file, "volumeUp=%s\n", settings->volumeUp); fprintf(file, "volumeUpAlt=%s\n", settings->volumeUpAlt); fprintf(file, "volumeDown=%s\n", settings->volumeDown); fprintf(file, "previousTrackAlt=%s\n", settings->previousTrackAlt); fprintf(file, "nextTrackAlt=%s\n", settings->nextTrackAlt); fprintf(file, "scrollUpAlt=%s\n", settings->scrollUpAlt); fprintf(file, "scrollDownAlt=%s\n", settings->scrollDownAlt); fprintf(file, "switchNumberedSong=%s\n", settings->switchNumberedSong); fprintf(file, "togglePause=%s\n", settings->togglePause); fprintf(file, "toggleColorsDerivedFrom=%s\n", settings->toggleColorsDerivedFrom); fprintf(file, "toggleVisualizer=%s\n", settings->toggleVisualizer); fprintf(file, "toggleCovers=%s\n", settings->toggleCovers); fprintf(file, "toggleAscii=%s\n", settings->toggleAscii); fprintf(file, "toggleRepeat=%s\n", settings->toggleRepeat); fprintf(file, "toggleShuffle=%s\n", settings->toggleShuffle); fprintf(file, "seekBackward=%s\n", settings->seekBackward); fprintf(file, "seekForward=%s\n", settings->seekForward); fprintf(file, "savePlaylist=%s\n", settings->savePlaylist); fprintf(file, "addToMainPlaylist=%s\n", settings->addToMainPlaylist); fprintf(file, "updateLibrary=%s\n", settings->updateLibrary); fprintf(file, "quit=%s\n\n", settings->quit); fprintf(file, "# For special keys use terminal codes: OS, for F4 for instance. This can depend on the terminal.\n"); fprintf(file, "# You can find out the codes for the keys by using tools like showkey.\n"); fprintf(file, "# For special keys, see the key value after the bracket \"[\" after typing \"showkey -a\" in the terminal and then pressing a key you want info about.\n"); fprintf(file, "\n\n"); fclose(file); free(filepath); free(configdir); } kew-2.4.2/src/settings.h000066400000000000000000000006561457005474400151260ustar00rootroot00000000000000#ifndef SETTINGS_H #define SETTINGS_H #include #include #include #include #include #include #include #include "file.h" #include "soundcommon.h" #include "player.h" #include "utils.h" #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif extern AppSettings settings; void getConfig(AppSettings *settings); void setConfig(AppSettings *settings); #endif kew-2.4.2/src/songloader.c000066400000000000000000000227071457005474400154170ustar00rootroot00000000000000#include "songloader.h" /* songloader.c This file should contain only functions related to loading song data. */ #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif Cache *tempCache = NULL; void removeTagPrefix(char *value) { char *colon_pos = strchr(value, ':'); if (colon_pos) { // Remove the tag prefix by shifting the characters memmove(value, colon_pos + 1, strlen(colon_pos)); } } char *findLargestImageFile(const char *directoryPath, char *largestImageFile, off_t *largestFileSize) { DIR *directory = opendir(directoryPath); struct dirent *entry; struct stat fileStats; if (directory == NULL) { fprintf(stderr, "Failed to open directory: %s\n", directoryPath); return largestImageFile; } while ((entry = readdir(directory)) != NULL) { char filePath[MAXPATHLEN]; snprintf(filePath, sizeof(filePath), "%s%s", directoryPath, entry->d_name); if (stat(filePath, &fileStats) == -1) { continue; } if (S_ISREG(fileStats.st_mode)) { // Check if the entry is an image file and has a larger size than the current largest image file char *extension = strrchr(entry->d_name, '.'); if (extension != NULL && (strcasecmp(extension, ".jpg") == 0 || strcasecmp(extension, ".jpeg") == 0 || strcasecmp(extension, ".png") == 0 || strcasecmp(extension, ".gif") == 0)) { if (fileStats.st_size > *largestFileSize) { *largestFileSize = fileStats.st_size; if (largestImageFile != NULL) { free(largestImageFile); } largestImageFile = strdup(filePath); } } } } closedir(directory); return largestImageFile; } void turnFilePathIntoTitle(const char *filePath, char *title) { char *lastSlash = strrchr(filePath, '/'); char *lastDot = strrchr(filePath, '.'); if (lastSlash != NULL && lastDot != NULL && lastDot > lastSlash) { size_t maxSize = sizeof(title) - 1; // Reserve space for null terminator snprintf(title, maxSize, "%s", lastSlash + 1); memcpy(title, lastSlash + 1, lastDot - lastSlash - 1); // Copy up to dst_size - 1 bytes title[lastDot - lastSlash - 1] = '\0'; trim(title); } } // Extracts metadata, returns -1 if no album cover found and -2 if no file found int extractTags(const char *input_file, TagSettings *tag_settings, double *duration, const char *coverFilePath) { AVFormatContext *fmt_ctx = NULL; AVDictionaryEntry *tag = NULL; int ret; if ((ret = avformat_open_input(&fmt_ctx, input_file, NULL, NULL)) < 0) { fprintf(stderr, "Could not open input file '%s'\n", input_file); return -2; } if ((ret = avformat_find_stream_info(fmt_ctx, NULL)) < 0) { fprintf(stderr, "Could not find stream information\n"); avformat_close_input(&fmt_ctx); return -2; } memset(tag_settings->title, 0, sizeof(tag_settings->title)); memset(tag_settings->artist, 0, sizeof(tag_settings->artist)); memset(tag_settings->album_artist, 0, sizeof(tag_settings->album_artist)); memset(tag_settings->album, 0, sizeof(tag_settings->album)); memset(tag_settings->date, 0, sizeof(tag_settings->date)); while ((tag = av_dict_get(fmt_ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) { if (strcasecmp(tag->key, "title") == 0) { snprintf(tag_settings->title, sizeof(tag_settings->title), "%s", tag->value); } else if (strcasecmp(tag->key, "artist") == 0) { snprintf(tag_settings->artist, sizeof(tag_settings->artist), "%s", tag->value); } else if (strcasecmp(tag->key, "album_artist") == 0) { snprintf(tag_settings->album_artist, sizeof(tag_settings->album_artist), "%s", tag->value); } else if (strcasecmp(tag->key, "album") == 0) { snprintf(tag_settings->album, sizeof(tag_settings->album), "%s", tag->value); } else if (strcasecmp(tag->key, "date") == 0) { snprintf(tag_settings->date, sizeof(tag_settings->date), "%s", tag->value); } } if (strlen(tag_settings->title) <= 0) { char title[MAXPATHLEN]; turnFilePathIntoTitle(input_file, title); strncpy(tag_settings->title, title, sizeof(tag_settings->title) - 1); tag_settings->title[sizeof(tag_settings->title) - 1] = '\0'; } if (fmt_ctx->duration != AV_NOPTS_VALUE) { *duration = (double)(fmt_ctx->duration / AV_TIME_BASE); } else { *duration = 0.0; } int stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); if (stream_index < 0) { fprintf(stderr, "Could not find a video stream in the input file\n"); avformat_close_input(&fmt_ctx); return -1; } AVStream *video_stream = fmt_ctx->streams[stream_index]; if (video_stream->disposition & AV_DISPOSITION_ATTACHED_PIC) { AVPacket *pkt = &video_stream->attached_pic; FILE *file = fopen(coverFilePath, "wb"); if (!file) { fprintf(stderr, "Could not open output file '%s'\n", coverFilePath); avformat_close_input(&fmt_ctx); return -1; } fwrite(pkt->data, 1, pkt->size, file); fclose(file); avformat_close_input(&fmt_ctx); return 0; } else { avformat_close_input(&fmt_ctx); return -1; } avformat_close_input(&fmt_ctx); return 0; } static guint track_counter = 0; // Generate a new track ID gchar *generateTrackId() { gchar *trackId = g_strdup_printf("/org/kew/tracklist/track%d", track_counter); track_counter++; return trackId; } void loadColor(SongData *songdata) { getCoverColor(songdata->cover, &(songdata->red), &(songdata->green), &(songdata->blue)); } void loadMetaData(SongData *songdata) { char path[MAXPATHLEN]; songdata->metadata = malloc(sizeof(TagSettings)); generateTempFilePath(songdata->coverArtPath, "cover", ".jpg"); int res = extractTags(songdata->filePath, songdata->metadata, &songdata->duration, songdata->coverArtPath); if (res == -2) { songdata->hasErrors = true; return; } else if (res == -1) { getDirectoryFromPath(songdata->filePath, path); char *tmp = NULL; off_t size = 0; tmp = findLargestImageFile(path, tmp, &size); if (tmp != NULL) c_strcpy(songdata->coverArtPath, sizeof(songdata->coverArtPath), tmp); else c_strcpy(songdata->coverArtPath, sizeof(songdata->coverArtPath), ""); } else { addToCache(tempCache, songdata->coverArtPath); } songdata->cover = getBitmap(songdata->coverArtPath); } SongData *loadSongData(char *filePath) { SongData *songdata = NULL; songdata = malloc(sizeof(SongData)); songdata->trackId = generateTrackId(); songdata->hasErrors = false; c_strcpy(songdata->filePath, sizeof(songdata->filePath), ""); c_strcpy(songdata->coverArtPath, sizeof(songdata->coverArtPath), ""); songdata->red = 150; songdata->green = 150; songdata->blue = 150; songdata->metadata = NULL; songdata->cover = NULL; songdata->duration = 0.0; c_strcpy(songdata->filePath, sizeof(songdata->filePath), filePath); loadMetaData(songdata); loadColor(songdata); return songdata; } void unloadSongData(SongData **songdata) { if (*songdata == NULL) return; SongData *data = *songdata; if (data->cover != NULL) { FreeImage_Unload(data->cover); data->cover = NULL; } if (existsInCache(tempCache, data->coverArtPath) && isInTempDir(data->coverArtPath)) { deleteFile(data->coverArtPath); } free(data->metadata); free(data->trackId); data->cover = NULL; data->metadata = NULL; data->trackId = NULL; free(*songdata); *songdata = NULL; } kew-2.4.2/src/songloader.h000066400000000000000000000021501457005474400154120ustar00rootroot00000000000000#include #include #include #include #include #include "cache.h" #include "chafafunc.h" #include "file.h" #include "sound.h" #include "soundcommon.h" #include "utils.h" #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef KEYVALUEPAIR_STRUCT #define KEYVALUEPAIR_STRUCT typedef struct { char *key; char *value; } KeyValuePair; #endif #ifndef TAGSETTINGS_STRUCT #define TAGSETTINGS_STRUCT typedef struct { char title[256]; char artist[256]; char album_artist[256]; char album[256]; char date[256]; } TagSettings; #endif #ifndef SONGDATA_STRUCT #define SONGDATA_STRUCT typedef struct { gchar *trackId; char filePath[MAXPATHLEN]; char coverArtPath[MAXPATHLEN]; unsigned char red; unsigned char green; unsigned char blue; TagSettings *metadata; FIBITMAP *cover; double duration; bool hasErrors; } SongData; #endif extern Cache *tempCache; SongData *loadSongData(char *filePath); void unloadSongData(SongData **songdata); kew-2.4.2/src/sound.c000066400000000000000000000411721457005474400144070ustar00rootroot00000000000000#define MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING #define MA_NO_ENGINE #define MINIAUDIO_IMPLEMENTATION #include #include "mpris.h" #include "sound.h" /* sound.c Functions related to miniaudio implementation */ ma_context context; UserData userData; AudioData audioData; int check_aac_codec_support() { const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_AAC); if (codec == NULL) { fprintf(stderr, "AAC codec not supported in this build of FFmpeg.\n"); return -1; } return 0; } ma_result initFirstDatasource(AudioData *pAudioData, UserData *pUserData) { char *filePath = NULL; filePath = (pAudioData->currentFileIndex == 0) ? pUserData->songdataA->filePath : pUserData->songdataB->filePath; pAudioData->pUserData = pUserData; pAudioData->currentPCMFrame = 0; pAudioData->restart = false; if (hasBuiltinDecoder(filePath)) { int result = prepareNextDecoder(filePath); if (result < 0) return -1; ma_decoder *first = getFirstDecoder(); pAudioData->format = first->outputFormat; pAudioData->channels = first->outputChannels; pAudioData->sampleRate = first->outputSampleRate; ma_data_source_get_length_in_pcm_frames(first, &pAudioData->totalFrames); } else if (endsWith(filePath, "opus")) { int result = prepareNextOpusDecoder(filePath); if (result < 0) return -1; ma_libopus *first = getFirstOpusDecoder(); ma_channel channelMap[MA_MAX_CHANNELS]; ma_libopus_ds_get_data_format(first, &pAudioData->format, &pAudioData->channels, &pAudioData->sampleRate, channelMap, MA_MAX_CHANNELS); ma_data_source_get_length_in_pcm_frames(first, &pAudioData->totalFrames); ma_data_source_base *base = (ma_data_source_base *)first; base->pCurrent = first; first->pReadSeekTellUserData = pAudioData; } else if (endsWith(filePath, "ogg")) { int result = prepareNextVorbisDecoder(filePath); if (result < 0) return -1; ma_libvorbis *first = getFirstVorbisDecoder(); ma_channel channelMap[MA_MAX_CHANNELS]; ma_libvorbis_ds_get_data_format(first, &pAudioData->format, &pAudioData->channels, &pAudioData->sampleRate, channelMap, MA_MAX_CHANNELS); ma_data_source_get_length_in_pcm_frames(first, &pAudioData->totalFrames); ma_data_source_base *base = (ma_data_source_base *)first; base->pCurrent = first; first->pReadSeekTellUserData = pAudioData; } else if (endsWith(filePath, "m4a") || endsWith(filePath, "aac") || endsWith(filePath, "mp4")) { int result = prepareNextM4aDecoder(filePath); if (result < 0) return -1; m4a_decoder *first = getFirstM4aDecoder(); ma_channel channelMap[MA_MAX_CHANNELS]; m4a_decoder_ds_get_data_format(first, &pAudioData->format, &pAudioData->channels, &pAudioData->sampleRate, channelMap, MA_MAX_CHANNELS); ma_data_source_get_length_in_pcm_frames(first, &pAudioData->totalFrames); ma_data_source_base *base = (ma_data_source_base *)first; base->pCurrent = first; first->pReadSeekTellUserData = pAudioData; } else { return MA_ERROR; } return MA_SUCCESS; } void createDevice(UserData *userData, ma_device *device, ma_context *context, ma_data_source_vtable *vtable, ma_device_data_proc callback) { ma_result result; ma_data_source_uninit(&audioData); result = initFirstDatasource(&audioData, userData); if (result != MA_SUCCESS) return; audioData.base.vtable = vtable; ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.format = audioData.format; deviceConfig.playback.channels = audioData.channels; deviceConfig.sampleRate = audioData.sampleRate; deviceConfig.dataCallback = callback; deviceConfig.pUserData = &audioData; result = ma_device_init(context, &deviceConfig, device); if (result != MA_SUCCESS) return; setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) return; emitStringPropertyChanged("PlaybackStatus", "Playing"); } void builtin_createAudioDevice(UserData *userData, ma_device *device, ma_context *context, ma_data_source_vtable *vtable) { createDevice(userData, device, context, vtable, builtin_on_audio_frames); } void vorbis_createAudioDevice(UserData *userData, ma_device *device, ma_context *context) { ma_result result; initFirstDatasource(&audioData, userData); ma_libvorbis *vorbis = getFirstVorbisDecoder(); ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.format = vorbis->format; deviceConfig.playback.channels = audioData.channels; deviceConfig.sampleRate = audioData.sampleRate; deviceConfig.dataCallback = vorbis_on_audio_frames; deviceConfig.pUserData = vorbis; result = ma_device_init(context, &deviceConfig, device); if (result != MA_SUCCESS) { printf("Failed to initialize miniaudio device.\n"); return; } setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) { printf("Failed to start miniaudio device.\n"); return; } emitStringPropertyChanged("PlaybackStatus", "Playing"); } void m4a_createAudioDevice(UserData *userData, ma_device *device, ma_context *context) { ma_result result; initFirstDatasource(&audioData, userData); m4a_decoder *decoder = getFirstM4aDecoder(); ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.format = decoder->format; deviceConfig.playback.channels = audioData.channels; deviceConfig.sampleRate = audioData.sampleRate; deviceConfig.dataCallback = m4a_on_audio_frames; deviceConfig.pUserData = decoder; result = ma_device_init(context, &deviceConfig, device); if (result != MA_SUCCESS) { printf("Failed to initialize miniaudio device.\n"); return; } setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) { printf("Failed to start miniaudio device.\n"); return; } emitStringPropertyChanged("PlaybackStatus", "Playing"); } void opus_createAudioDevice(UserData *userData, ma_device *device, ma_context *context) { ma_result result; initFirstDatasource(&audioData, userData); ma_libopus *opus = getFirstOpusDecoder(); ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.format = opus->format; deviceConfig.playback.channels = audioData.channels; deviceConfig.sampleRate = audioData.sampleRate; deviceConfig.dataCallback = opus_on_audio_frames; deviceConfig.pUserData = opus; result = ma_device_init(context, &deviceConfig, device); if (result != MA_SUCCESS) { printf("Failed to initialize miniaudio device.\n"); return; } setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) { printf("Failed to start miniaudio device.\n"); return; } emitStringPropertyChanged("PlaybackStatus", "Playing"); } int switchAudioImplementation() { if (audioData.endOfListReached) { setEOFNotReached(); setCurrentImplementationType(NONE); return 0; } enum AudioImplementation currentImplementation = getCurrentImplementationType(); userData.currentSongData = (audioData.currentFileIndex == 0) ? userData.songdataA : userData.songdataB; if (userData.currentSongData == NULL) { setEOFNotReached(); return 0; } char *filePath = strdup(userData.currentSongData->filePath); if (filePath == NULL || filePath[0] == '\0' || filePath[0] == '\r' || existsFile(filePath) < 0) { free(filePath); setEOFReached(); return -1; } if (hasBuiltinDecoder(filePath)) { ma_uint32 sampleRate = 0; ma_uint32 channels = 0; ma_format format = ma_format_unknown; ma_decoder *decoder = getCurrentBuiltinDecoder(); getFileInfo(filePath, &sampleRate, &channels, &format); bool sameFormat = (decoder != NULL && (sampleRate == decoder->outputSampleRate && channels == decoder->outputChannels && format == decoder->outputFormat)); if (isRepeatEnabled() || !(sameFormat && currentImplementation == BUILTIN)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(BUILTIN); cleanupPlaybackDevice(); resetDecoders(); resetVorbisDecoders(); resetM4aDecoders(); resetOpusDecoders(); resetAudioBuffer(); builtin_createAudioDevice(&userData, getDevice(), &context, &builtin_file_data_source_vtable); pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } } else if (endsWith(filePath, "opus")) { ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_uint32 nSampleRate; ma_uint32 nChannels; ma_format nFormat; ma_channel nChannelMap[MA_MAX_CHANNELS]; ma_libopus *decoder = getCurrentOpusDecoder(); getOpusFileInfo(filePath, &format, &channels, &sampleRate, channelMap); if (decoder != NULL) ma_libopus_ds_get_data_format(decoder, &nFormat, &nChannels, &nSampleRate, nChannelMap, MA_MAX_CHANNELS); bool sameFormat = (decoder != NULL && (format == decoder->format && channels == nChannels && sampleRate == nSampleRate)); if (isRepeatEnabled() || !(sameFormat && currentImplementation == OPUS)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(OPUS); cleanupPlaybackDevice(); resetDecoders(); resetVorbisDecoders(); resetM4aDecoders(); resetOpusDecoders(); resetAudioBuffer(); opus_createAudioDevice(&userData, getDevice(), &context); pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } } else if (endsWith(filePath, "ogg")) { ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_uint32 nSampleRate; ma_uint32 nChannels; ma_format nFormat; ma_channel nChannelMap[MA_MAX_CHANNELS]; ma_libvorbis *decoder = getCurrentVorbisDecoder(); getVorbisFileInfo(filePath, &format, &channels, &sampleRate, channelMap); if (decoder != NULL) ma_libvorbis_ds_get_data_format(decoder, &nFormat, &nChannels, &nSampleRate, nChannelMap, MA_MAX_CHANNELS); bool sameFormat = (decoder != NULL && (format == decoder->format && channels == nChannels && sampleRate == nSampleRate)); if (isRepeatEnabled() || !(sameFormat && currentImplementation == VORBIS)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(VORBIS); cleanupPlaybackDevice(); resetDecoders(); resetVorbisDecoders(); resetM4aDecoders(); resetOpusDecoders(); resetAudioBuffer(); vorbis_createAudioDevice(&userData, getDevice(), &context); pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } } else if (endsWith(filePath, "m4a") || endsWith(filePath, "aac") || endsWith(filePath, "mp4")) { if (check_aac_codec_support() < 0) { free(filePath); printf("\n\nUnable to find AAC codec. If you have the free version of FFmpeg, there might be no AAC/M4A file support.\n"); exit(0); } ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_uint32 nSampleRate; ma_uint32 nChannels; ma_format nFormat; ma_channel nChannelMap[MA_MAX_CHANNELS]; m4a_decoder *decoder = getCurrentM4aDecoder(); getM4aFileInfo(filePath, &format, &channels, &sampleRate, channelMap); if (decoder != NULL) m4a_decoder_ds_get_data_format(decoder, &nFormat, &nChannels, &nSampleRate, nChannelMap, MA_MAX_CHANNELS); bool sameFormat = (decoder != NULL && (format == decoder->format && channels == nChannels && sampleRate == nSampleRate)); if (isRepeatEnabled() || !(sameFormat && currentImplementation == M4A)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(M4A); cleanupPlaybackDevice(); resetDecoders(); resetVorbisDecoders(); resetM4aDecoders(); resetOpusDecoders(); resetAudioBuffer(); m4a_createAudioDevice(&userData, getDevice(), &context); pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } } else { free(filePath); return -1; } free(filePath); setEOFNotReached(); return 0; } void cleanupAudioContext() { ma_context_uninit(&context); } int createAudioDevice(UserData *userData) { ma_context_init(NULL, 0, NULL, &context); if (switchAudioImplementation() >= 0) { SongData *currentSongData = userData->currentSongData; if (currentSongData != NULL && currentSongData->hasErrors == 0 && currentSongData->metadata && strlen(currentSongData->metadata->title) > 0) displaySongNotification(currentSongData->metadata->artist, currentSongData->metadata->title, currentSongData->coverArtPath); } else { return -1; } return 0; } kew-2.4.2/src/sound.h000066400000000000000000000026451457005474400144160ustar00rootroot00000000000000#ifndef SOUND_H #define SOUND_H #include #include #include #include #include #include #include #include #include #include #include #include "file.h" #include "songloader.h" #include "soundbuiltin.h" #include "soundcommon.h" #ifndef USERDATA_STRUCT #define USERDATA_STRUCT typedef struct { SongData *songdataA; SongData *songdataB; bool songdataADeleted; bool songdataBDeleted; SongData *currentSongData; ma_uint32 currentPCMFrame; } UserData; #endif #ifndef AUDIODATA_STRUCT #define AUDIODATA_STRUCT typedef struct { ma_data_source_base base; UserData *pUserData; ma_format format; ma_uint32 channels; ma_uint32 sampleRate; ma_uint32 currentPCMFrame; bool switchFiles; int currentFileIndex; ma_uint64 totalFrames; bool endOfListReached; bool restart; } AudioData; #endif extern AudioData audioData; extern UserData userData; int prepareNextDecoder(char *filepath); int prepareNextOpusDecoder(char *filepath); int prepareNextVorbisDecoder(char *filepath); int prepareNextM4aDecoder(char *filepath); void setDecoders(bool usingA, char *filePath); int createAudioDevice(UserData *userData); int switchAudioImplementation(); void cleanupAudioContext(); #endif kew-2.4.2/src/soundbuiltin.c000066400000000000000000000154751457005474400160050ustar00rootroot00000000000000#include "soundbuiltin.h" /* soundbuiltin.c Functions related to miniaudio implementation for miniaudio built-in decoders (flac, wav and mp3) */ static ma_result builtin_file_data_source_read(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { // Dummy implementation (void)pDataSource; (void)pFramesOut; (void)frameCount; (void)pFramesRead; return MA_SUCCESS; } static ma_result builtin_file_data_source_seek(ma_data_source *pDataSource, ma_uint64 frameIndex) { AudioData *audioData = (AudioData *)pDataSource; if (getCurrentBuiltinDecoder() == NULL) { return MA_INVALID_ARGS; } ma_result result = ma_decoder_seek_to_pcm_frame(getCurrentBuiltinDecoder(), frameIndex); if (result == MA_SUCCESS) { audioData->currentPCMFrame = (ma_uint32)frameIndex; return MA_SUCCESS; } else { return result; } } static ma_result builtin_file_data_source_get_data_format(ma_data_source *pDataSource, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap) { (void)pChannelMap; (void)channelMapCap; AudioData *audioData = (AudioData *)pDataSource; *pFormat = audioData->format; *pChannels = audioData->channels; *pSampleRate = audioData->sampleRate; return MA_SUCCESS; } static ma_result builtin_file_data_source_get_cursor(ma_data_source *pDataSource, ma_uint64 *pCursor) { AudioData *audioData = (AudioData *)pDataSource; *pCursor = audioData->currentPCMFrame; return MA_SUCCESS; } static ma_result builtin_file_data_source_get_length(ma_data_source *pDataSource, ma_uint64 *pLength) { (void)pDataSource; ma_uint64 totalFrames = 0; if (getCurrentBuiltinDecoder() == NULL) { return MA_INVALID_ARGS; } ma_result result = ma_decoder_get_length_in_pcm_frames(getCurrentBuiltinDecoder(), &totalFrames); if (result != MA_SUCCESS) { return result; } *pLength = totalFrames; return MA_SUCCESS; } static ma_result builtin_file_data_source_set_looping(ma_data_source *pDataSource, ma_bool32 isLooping) { // Dummy implementation (void)pDataSource; (void)isLooping; return MA_SUCCESS; } ma_data_source_vtable builtin_file_data_source_vtable = { builtin_file_data_source_read, builtin_file_data_source_seek, builtin_file_data_source_get_data_format, builtin_file_data_source_get_cursor, builtin_file_data_source_get_length, builtin_file_data_source_set_looping, 0 // flags }; void builtin_read_pcm_frames(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { AudioData *audioData = (AudioData *)pDataSource; ma_uint64 framesRead = 0; while (framesRead < frameCount) { ma_uint64 remainingFrames = frameCount - framesRead; if (pthread_mutex_trylock(&dataSourceMutex) != 0) { return; } if (isImplSwitchReached() || audioData == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } if (audioData->switchFiles) { executeSwitch(audioData); pthread_mutex_unlock(&dataSourceMutex); break; } ma_decoder *decoder = getCurrentBuiltinDecoder(); if ((getCurrentImplementationType() != BUILTIN && !isSkipToNext()) || decoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } if (audioData->totalFrames == 0) ma_data_source_get_length_in_pcm_frames(decoder, &audioData->totalFrames); if (isSeekRequested()) { ma_uint64 totalFrames = audioData->totalFrames; ma_uint64 seekPercent = getSeekPercentage(); if (seekPercent >= 100.0) seekPercent = 100.0; ma_uint64 targetFrame = (totalFrames * seekPercent) / 100; ma_result seekResult = ma_decoder_seek_to_pcm_frame(decoder, targetFrame); if (seekResult != MA_SUCCESS) { setSeekRequested(false); pthread_mutex_unlock(&dataSourceMutex); return; } setSeekRequested(false); } ma_uint64 framesToRead = 0; ma_decoder *firstDecoder = getFirstDecoder(); ma_uint64 cursor = 0; ma_result result; if (decoder == NULL || firstDecoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } result = ma_data_source_read_pcm_frames(firstDecoder, (ma_int32 *)pFramesOut + framesRead * decoder->outputChannels, remainingFrames, &framesToRead); ma_data_source_get_cursor_in_pcm_frames(decoder, &cursor); if (((audioData->totalFrames != 0 && cursor != 0 && cursor >= audioData->totalFrames) || framesToRead == 0 || isSkipToNext() || result != MA_SUCCESS) && !isEOFReached()) { activateSwitch(audioData); pthread_mutex_unlock(&dataSourceMutex); continue; } framesRead += framesToRead; setBufferSize(framesToRead); pthread_mutex_unlock(&dataSourceMutex); } ma_int32 *audioBuffer = getAudioBuffer(); if (audioBuffer == NULL) { audioBuffer = malloc(sizeof(ma_int32) * MAX_BUFFER_SIZE); if (audioBuffer == NULL) { return; } } memcpy(audioBuffer, pFramesOut, sizeof(ma_int32) * framesRead); setAudioBuffer(audioBuffer); if (pFramesRead != NULL) { *pFramesRead = framesRead; } } void builtin_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount) { AudioData *pDataSource = (AudioData *)pDevice->pUserData; ma_uint64 framesRead = 0; builtin_read_pcm_frames(&pDataSource->base, pFramesOut, frameCount, &framesRead); (void)pFramesIn; } kew-2.4.2/src/soundbuiltin.h000066400000000000000000000006711457005474400160020ustar00rootroot00000000000000#ifndef SOUNDBUILTIN_H #define SOUNDBUILTIN_H #include #include #include #include "soundcommon.h" extern ma_data_source_vtable builtin_file_data_source_vtable; void builtin_read_pcm_frames(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead); void builtin_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount); #endif kew-2.4.2/src/soundcommon.c000066400000000000000000001321011457005474400156110ustar00rootroot00000000000000#include "soundcommon.h" /* soundcommon.c Related to common functions for decoders / miniaudio implementations. */ #define MAX_DECODERS 2 bool allowNotifications = true; bool repeatEnabled = false; bool shuffleEnabled = false; bool skipToNext = false; bool seekRequested = false; bool paused = false; float seekPercent = 0.0; double seekElapsed; _Atomic bool EOFReached = false; _Atomic bool switchReached = false; _Atomic bool readingFrames = false; pthread_mutex_t dataSourceMutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t switchMutex = PTHREAD_MUTEX_INITIALIZER; ma_device device = {0}; ma_int32 *audioBuffer = NULL; int bufSize; ma_event switchAudioImpl; enum AudioImplementation currentImplementation = NONE; bool doQuit = false; AppState appState; volatile bool refresh = true; double duration; double elapsedSeconds = 0.0; int soundVolume = 100; ma_decoder *firstDecoder; ma_decoder *currentDecoder; ma_decoder *decoders[MAX_DECODERS]; ma_libopus *opusDecoders[MAX_DECODERS]; ma_libopus *firstOpusDecoder; ma_libvorbis *vorbisDecoders[MAX_DECODERS]; ma_libvorbis *firstVorbisDecoder; m4a_decoder *m4aDecoders[MAX_DECODERS]; m4a_decoder *firstM4aDecoder; int decoderIndex = -1; int m4aDecoderIndex = -1; int opusDecoderIndex = -1; int vorbisDecoderIndex = -1; enum AudioImplementation getCurrentImplementationType() { return currentImplementation; } void setCurrentImplementationType(enum AudioImplementation value) { currentImplementation = value; } ma_decoder *getFirstDecoder() { return firstDecoder; } ma_decoder *getCurrentBuiltinDecoder() { if (decoderIndex == -1) return getFirstDecoder(); else return decoders[decoderIndex]; } void switchDecoder() { if (decoderIndex == -1) decoderIndex = 0; else decoderIndex = 1 - decoderIndex; } void resetDecoders() { decoderIndex = -1; if (firstDecoder != NULL && firstDecoder->outputFormat != ma_format_unknown) { ma_decoder_uninit(firstDecoder); free(firstDecoder); firstDecoder = NULL; } if (decoders[0] != NULL && decoders[0]->outputFormat != ma_format_unknown) { ma_decoder_uninit(decoders[0]); free(decoders[0]); decoders[0] = NULL; } if (decoders[1] != NULL && decoders[1]->outputFormat != ma_format_unknown) { ma_decoder_uninit(decoders[1]); free(decoders[1]); decoders[1] = NULL; } } void uninitPreviousDecoder() { if (decoderIndex == -1) { return; } ma_decoder *toUninit = decoders[1 - decoderIndex]; if (toUninit != NULL) { ma_decoder_uninit(toUninit); free(toUninit); decoders[1 - decoderIndex] = NULL; } } void uninitPreviousVorbisDecoder() { if (vorbisDecoderIndex == -1) { return; } ma_libvorbis *toUninit = vorbisDecoders[1 - vorbisDecoderIndex]; if (toUninit != NULL) { ma_libvorbis_uninit(toUninit, NULL); free(toUninit); vorbisDecoders[1 - vorbisDecoderIndex] = NULL; } } void uninitPreviousM4aDecoder() { if (m4aDecoderIndex == -1) // either start of the program or resetM4aDecoders has been called { return; } m4a_decoder *toUninit = m4aDecoders[1 - m4aDecoderIndex]; if (toUninit != NULL) { m4a_decoder_uninit(toUninit, NULL); free(toUninit); m4aDecoders[1 - m4aDecoderIndex] = NULL; } } void uninitPreviousOpusDecoder() { if (opusDecoderIndex == -1) { return; } ma_libopus *toUninit = opusDecoders[1 - opusDecoderIndex]; if (toUninit != NULL) { ma_libopus_uninit(toUninit, NULL); free(toUninit); opusDecoders[1 - opusDecoderIndex] = NULL; } } ma_libvorbis *getFirstVorbisDecoder() { return firstVorbisDecoder; } m4a_decoder *getFirstM4aDecoder() { return firstM4aDecoder; } ma_libopus *getFirstOpusDecoder() { return firstOpusDecoder; } ma_libvorbis *getCurrentVorbisDecoder() { if (vorbisDecoderIndex == -1) return getFirstVorbisDecoder(); else return vorbisDecoders[vorbisDecoderIndex]; } m4a_decoder *getCurrentM4aDecoder() { if (m4aDecoderIndex == -1) return getFirstM4aDecoder(); else return m4aDecoders[m4aDecoderIndex]; } ma_libopus *getCurrentOpusDecoder() { if (opusDecoderIndex == -1) return getFirstOpusDecoder(); else return opusDecoders[opusDecoderIndex]; } ma_format getCurrentFormat() { ma_format format = ma_format_unknown; if (getCurrentImplementationType() == BUILTIN) { ma_decoder *decoder = getCurrentBuiltinDecoder(); if (decoder != NULL) format = decoder->outputFormat; } else if (getCurrentImplementationType() == OPUS) { ma_libopus *decoder = getCurrentOpusDecoder(); if (decoder != NULL) format = decoder->format; } else if (getCurrentImplementationType() == VORBIS) { ma_libvorbis *decoder = getCurrentVorbisDecoder(); if (decoder != NULL) format = decoder->format; } else if (getCurrentImplementationType() == M4A) { m4a_decoder *decoder = getCurrentM4aDecoder(); if (decoder != NULL) format = decoder->format; } return format; } void switchVorbisDecoder() { if (vorbisDecoderIndex == -1) vorbisDecoderIndex = 0; else vorbisDecoderIndex = 1 - vorbisDecoderIndex; } void switchM4aDecoder() { if (m4aDecoderIndex == -1) m4aDecoderIndex = 0; else m4aDecoderIndex = 1 - m4aDecoderIndex; } void switchOpusDecoder() { if (opusDecoderIndex == -1) opusDecoderIndex = 0; else opusDecoderIndex = 1 - opusDecoderIndex; } void setNextVorbisDecoder(ma_libvorbis *decoder) { if (vorbisDecoderIndex == -1 && firstVorbisDecoder == NULL) { firstVorbisDecoder = decoder; } else if (vorbisDecoderIndex == -1) { if (vorbisDecoders[0] != NULL) { ma_libvorbis_uninit(vorbisDecoders[0], NULL); free(vorbisDecoders[0]); vorbisDecoders[0] = NULL; } vorbisDecoders[0] = decoder; } else { int nextIndex = 1 - vorbisDecoderIndex; if (vorbisDecoders[nextIndex] != NULL) { ma_libvorbis_uninit(vorbisDecoders[nextIndex], NULL); free(vorbisDecoders[nextIndex]); vorbisDecoders[nextIndex] = NULL; } vorbisDecoders[nextIndex] = decoder; } } void setNextM4aDecoder(m4a_decoder *decoder) { if (m4aDecoderIndex == -1 && firstM4aDecoder == NULL) { firstM4aDecoder = decoder; } else if (m4aDecoderIndex == -1) // array hasn't been used yet { if (m4aDecoders[0] != NULL) { m4a_decoder_uninit(m4aDecoders[0], NULL); free(m4aDecoders[0]); m4aDecoders[0] = NULL; } m4aDecoders[0] = decoder; } else { int nextIndex = 1 - m4aDecoderIndex; if (m4aDecoders[nextIndex] != NULL) { m4a_decoder_uninit(m4aDecoders[nextIndex], NULL); free(m4aDecoders[nextIndex]); m4aDecoders[nextIndex] = NULL; } m4aDecoders[nextIndex] = decoder; } } void setNextOpusDecoder(ma_libopus *decoder) { if (opusDecoderIndex == -1 && firstOpusDecoder == NULL) { firstOpusDecoder = decoder; } else if (opusDecoderIndex == -1) { if (opusDecoders[0] != NULL) { ma_libopus_uninit(opusDecoders[0], NULL); free(opusDecoders[0]); opusDecoders[0] = NULL; } opusDecoders[0] = decoder; } else { int nextIndex = 1 - opusDecoderIndex; if (opusDecoders[nextIndex] != NULL) { ma_libopus_uninit(opusDecoders[nextIndex], NULL); free(opusDecoders[nextIndex]); opusDecoders[nextIndex] = NULL; } opusDecoders[nextIndex] = decoder; } } void resetVorbisDecoders() { vorbisDecoderIndex = -1; if (firstVorbisDecoder != NULL && firstVorbisDecoder->format != ma_format_unknown) { ma_libvorbis_uninit(firstVorbisDecoder, NULL); free(firstVorbisDecoder); firstVorbisDecoder = NULL; } if (vorbisDecoders[0] != NULL && vorbisDecoders[0]->format != ma_format_unknown) { ma_libvorbis_uninit(vorbisDecoders[0], NULL); free(vorbisDecoders[0]); vorbisDecoders[0] = NULL; } if (vorbisDecoders[1] != NULL && vorbisDecoders[1]->format != ma_format_unknown) { ma_libvorbis_uninit(vorbisDecoders[1], NULL); free(vorbisDecoders[1]); vorbisDecoders[1] = NULL; } } void resetM4aDecoders() { m4aDecoderIndex = -1; if (firstM4aDecoder != NULL && firstM4aDecoder->format != ma_format_unknown) { m4a_decoder_uninit(firstM4aDecoder, NULL); free(firstM4aDecoder); firstM4aDecoder = NULL; } if (m4aDecoders[0] != NULL && m4aDecoders[0]->format != ma_format_unknown) { m4a_decoder_uninit(m4aDecoders[0], NULL); free(m4aDecoders[0]); m4aDecoders[0] = NULL; } if (m4aDecoders[1] != NULL && m4aDecoders[1]->format != ma_format_unknown) { m4a_decoder_uninit(m4aDecoders[1], NULL); free(m4aDecoders[1]); m4aDecoders[1] = NULL; } } void resetOpusDecoders() { opusDecoderIndex = -1; if (firstOpusDecoder != NULL && firstOpusDecoder->format != ma_format_unknown) { ma_libopus_uninit(firstOpusDecoder, NULL); free(firstOpusDecoder); firstOpusDecoder = NULL; } if (opusDecoders[0] != NULL && opusDecoders[0]->format != ma_format_unknown) { ma_libopus_uninit(opusDecoders[0], NULL); ma_free(opusDecoders[0], NULL); opusDecoders[0] = NULL; } if (opusDecoders[1] != NULL && opusDecoders[1]->format != ma_format_unknown) { ma_libopus_uninit(opusDecoders[1], NULL); ma_free(opusDecoders[1], NULL); opusDecoders[1] = NULL; } } void getFileInfo(const char *filename, ma_uint32 *sampleRate, ma_uint32 *channels, ma_format *format) { ma_decoder tmp; if (ma_decoder_init_file(filename, NULL, &tmp) == MA_SUCCESS) { *sampleRate = tmp.outputSampleRate; *channels = tmp.outputChannels; *format = tmp.outputFormat; ma_decoder_uninit(&tmp); } else { // Handle file open error. } } void getVorbisFileInfo(const char *filename, ma_format *format, ma_uint32 *channels, ma_uint32 *sampleRate, ma_channel *channelMap) { ma_libvorbis decoder; if (ma_libvorbis_init_file(filename, NULL, NULL, &decoder) == MA_SUCCESS) { *format = decoder.format; ma_libvorbis_get_data_format(&decoder, format, channels, sampleRate, channelMap, MA_MAX_CHANNELS); ma_libvorbis_uninit(&decoder, NULL); } } void getM4aFileInfo(const char *filename, ma_format *format, ma_uint32 *channels, ma_uint32 *sampleRate, ma_channel *channelMap) { m4a_decoder decoder; if (m4a_decoder_init_file(filename, NULL, NULL, &decoder) == MA_SUCCESS) { *format = decoder.format; m4a_decoder_get_data_format(&decoder, format, channels, sampleRate, channelMap, MA_MAX_CHANNELS); m4a_decoder_uninit(&decoder, NULL); } } void getOpusFileInfo(const char *filename, ma_format *format, ma_uint32 *channels, ma_uint32 *sampleRate, ma_channel *channelMap) { ma_libopus decoder; if (ma_libopus_init_file(filename, NULL, NULL, &decoder) == MA_SUCCESS) { *format = decoder.format; ma_libopus_get_data_format(&decoder, format, channels, sampleRate, channelMap, MA_MAX_CHANNELS); ma_libopus_uninit(&decoder, NULL); } } void setNextDecoder(ma_decoder *decoder) { if (decoderIndex == -1 && firstDecoder == NULL) { firstDecoder = decoder; } else if (decoderIndex == -1) { if (decoders[0] != NULL) { ma_decoder_uninit(decoders[0]); free(decoders[0]); decoders[0] = NULL; } decoders[0] = decoder; } else { int nextIndex = 1 - decoderIndex; if (decoders[nextIndex] != NULL) { ma_decoder_uninit(decoders[nextIndex]); free(decoders[nextIndex]); decoders[nextIndex] = NULL; } decoders[nextIndex] = decoder; } } MA_API ma_result m4a_read_pcm_frames_wrapper(void *pDecoder, void *pFramesOut, size_t frameCount, size_t *pFramesRead) { ma_decoder *dec = (ma_decoder *)pDecoder; return m4a_decoder_read_pcm_frames((m4a_decoder *)dec->pUserData, pFramesOut, frameCount, (ma_uint64 *)pFramesRead); } MA_API ma_result m4a_seek_to_pcm_frame_wrapper(void *pDecoder, long long int frameIndex, ma_seek_origin origin) { (void)origin; ma_decoder *dec = (ma_decoder *)pDecoder; return m4a_decoder_seek_to_pcm_frame((m4a_decoder *)dec->pUserData, frameIndex); } MA_API ma_result m4a_get_cursor_in_pcm_frames_wrapper(void *pDecoder, long long int *pCursor) { ma_decoder *dec = (ma_decoder *)pDecoder; return m4a_decoder_get_cursor_in_pcm_frames((m4a_decoder *)dec->pUserData, (ma_uint64 *)pCursor); } MA_API ma_result ma_libopus_read_pcm_frames_wrapper(void *pDecoder, void *pFramesOut, size_t frameCount, size_t *pFramesRead) { ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libopus_read_pcm_frames((ma_libopus *)dec->pUserData, pFramesOut, frameCount, (ma_uint64 *)pFramesRead); } MA_API ma_result ma_libopus_seek_to_pcm_frame_wrapper(void *pDecoder, long long int frameIndex, ma_seek_origin origin) { (void)origin; ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libopus_seek_to_pcm_frame((ma_libopus *)dec->pUserData, frameIndex); } MA_API ma_result ma_libopus_get_cursor_in_pcm_frames_wrapper(void *pDecoder, long long int *pCursor) { ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libopus_get_cursor_in_pcm_frames((ma_libopus *)dec->pUserData, (ma_uint64 *)pCursor); } MA_API ma_result ma_libvorbis_read_pcm_frames_wrapper(void *pDecoder, void *pFramesOut, size_t frameCount, size_t *pFramesRead) { ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libvorbis_read_pcm_frames((ma_libvorbis *)dec->pUserData, pFramesOut, frameCount, (ma_uint64 *)pFramesRead); } MA_API ma_result ma_libvorbis_seek_to_pcm_frame_wrapper(void *pDecoder, long long int frameIndex, ma_seek_origin origin) { (void)origin; ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libvorbis_seek_to_pcm_frame((ma_libvorbis *)dec->pUserData, frameIndex); } MA_API ma_result ma_libvorbis_get_cursor_in_pcm_frames_wrapper(void *pDecoder, long long int *pCursor) { ma_decoder *dec = (ma_decoder *)pDecoder; return ma_libvorbis_get_cursor_in_pcm_frames((ma_libvorbis *)dec->pUserData, (ma_uint64 *)pCursor); } int prepareNextVorbisDecoder(char *filepath) { ma_libvorbis *currentDecoder; if (vorbisDecoderIndex == -1) { currentDecoder = getFirstVorbisDecoder(); } else { currentDecoder = vorbisDecoders[vorbisDecoderIndex]; } ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_libvorbis_get_data_format(currentDecoder, &format, &channels, &sampleRate, channelMap, MA_MAX_CHANNELS); uninitPreviousVorbisDecoder(); ma_libvorbis *decoder = (ma_libvorbis *)malloc(sizeof(ma_libvorbis)); ma_result result = ma_libvorbis_init_file(filepath, NULL, NULL, decoder); if (result != MA_SUCCESS) return -1; ma_format nformat; ma_uint32 nchannels; ma_uint32 nsampleRate; ma_channel nchannelMap[MA_MAX_CHANNELS]; ma_libvorbis_get_data_format(decoder, &nformat, &nchannels, &nsampleRate, nchannelMap, MA_MAX_CHANNELS); bool sameFormat = (currentDecoder == NULL || (format == nformat && channels == nchannels && sampleRate == nsampleRate)); if (!sameFormat) { ma_libvorbis_uninit(decoder, NULL); free(decoder); return -1; } ma_libvorbis *first = getFirstVorbisDecoder(); if (first != NULL) { decoder->pReadSeekTellUserData = (AudioData *)first->pReadSeekTellUserData; } decoder->format = nformat; decoder->onRead = ma_libvorbis_read_pcm_frames_wrapper; decoder->onSeek = ma_libvorbis_seek_to_pcm_frame_wrapper; decoder->onTell = ma_libvorbis_get_cursor_in_pcm_frames_wrapper; setNextVorbisDecoder(decoder); if (currentDecoder != NULL) ma_data_source_set_next(currentDecoder, decoder); return 0; } int prepareNextDecoder(char *filepath) { ma_decoder *currentDecoder; if (decoderIndex == -1) { currentDecoder = getFirstDecoder(); } else { currentDecoder = decoders[decoderIndex]; } ma_uint32 sampleRate; ma_uint32 channels; ma_format format; getFileInfo(filepath, &sampleRate, &channels, &format); bool sameFormat = (currentDecoder == NULL || (format == currentDecoder->outputFormat && channels == currentDecoder->outputChannels && sampleRate == currentDecoder->outputSampleRate)); if (!sameFormat) { return 0; } uninitPreviousDecoder(); ma_decoder *decoder = (ma_decoder *)malloc(sizeof(ma_decoder)); ma_result result = ma_decoder_init_file(filepath, NULL, decoder); if (result != MA_SUCCESS) { free(decoder); return -1; } setNextDecoder(decoder); if (currentDecoder != NULL) ma_data_source_set_next(currentDecoder, decoder); return 0; } int prepareNextM4aDecoder(char *filepath) { m4a_decoder *currentDecoder; if (m4aDecoderIndex == -1) { currentDecoder = getFirstM4aDecoder(); } else { currentDecoder = m4aDecoders[m4aDecoderIndex]; } ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; m4a_decoder_get_data_format(currentDecoder, &format, &channels, &sampleRate, channelMap, MA_MAX_CHANNELS); uninitPreviousM4aDecoder(); m4a_decoder *decoder = (m4a_decoder *)malloc(sizeof(m4a_decoder)); ma_result result = m4a_decoder_init_file(filepath, NULL, NULL, decoder); if (result != MA_SUCCESS) return -1; ma_format nformat; ma_uint32 nchannels; ma_uint32 nsampleRate; ma_channel nchannelMap[MA_MAX_CHANNELS]; m4a_decoder_get_data_format(decoder, &nformat, &nchannels, &nsampleRate, nchannelMap, MA_MAX_CHANNELS); bool sameFormat = (currentDecoder == NULL || (format == nformat && channels == nchannels && sampleRate == nsampleRate)); if (!sameFormat) { m4a_decoder_uninit(decoder, NULL); free(decoder); return -1; } m4a_decoder *first = getFirstM4aDecoder(); if (first != NULL) { decoder->pReadSeekTellUserData = (AudioData *)first->pReadSeekTellUserData; } decoder->format = nformat; decoder->onRead = m4a_read_pcm_frames_wrapper; decoder->onSeek = m4a_seek_to_pcm_frame_wrapper; decoder->onTell = m4a_get_cursor_in_pcm_frames_wrapper; decoder->cursor = 0; setNextM4aDecoder(decoder); if (currentDecoder != NULL) ma_data_source_set_next(currentDecoder, decoder); return 0; } int prepareNextOpusDecoder(char *filepath) { ma_libopus *currentDecoder; if (opusDecoderIndex == -1) { currentDecoder = getFirstOpusDecoder(); } else { currentDecoder = opusDecoders[decoderIndex]; } ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_libopus_get_data_format(currentDecoder, &format, &channels, &sampleRate, channelMap, MA_MAX_CHANNELS); uninitPreviousOpusDecoder(); ma_libopus *decoder = (ma_libopus *)malloc(sizeof(ma_libopus)); ma_result result = ma_libopus_init_file(filepath, NULL, NULL, decoder); if (result != MA_SUCCESS) return -1; ma_format nformat; ma_uint32 nchannels; ma_uint32 nsampleRate; ma_channel nchannelMap[MA_MAX_CHANNELS]; ma_libopus_get_data_format(decoder, &nformat, &nchannels, &nsampleRate, nchannelMap, MA_MAX_CHANNELS); bool sameFormat = (currentDecoder == NULL || (format == nformat && channels == nchannels && sampleRate == nsampleRate)); if (!sameFormat) { ma_libopus_uninit(decoder, NULL); free(decoder); return -1; } ma_libopus *first = getFirstOpusDecoder(); if (first != NULL) { decoder->pReadSeekTellUserData = (AudioData *)first->pReadSeekTellUserData; } decoder->format = nformat; decoder->onRead = ma_libopus_read_pcm_frames_wrapper; decoder->onSeek = ma_libopus_seek_to_pcm_frame_wrapper; decoder->onTell = ma_libopus_get_cursor_in_pcm_frames_wrapper; setNextOpusDecoder(decoder); if (currentDecoder != NULL) ma_data_source_set_next(currentDecoder, decoder); return 0; } int getBufferSize() { return bufSize; } void setBufferSize(int value) { bufSize = value; } void initAudioBuffer() { if (audioBuffer == NULL) { audioBuffer = malloc(sizeof(ma_int32) * MAX_BUFFER_SIZE); if (audioBuffer == NULL) { // Memory allocation failed return; } } } ma_int32 *getAudioBuffer() { return audioBuffer; } void setAudioBuffer(ma_int32 *buf) { audioBuffer = buf; } void resetAudioBuffer() { memset(audioBuffer, 0, sizeof(float) * MAX_BUFFER_SIZE); } void freeAudioBuffer() { if (audioBuffer != NULL) { free(audioBuffer); audioBuffer = NULL; } } bool isRepeatEnabled() { return repeatEnabled; } void setRepeatEnabled(bool value) { repeatEnabled = value; } bool isShuffleEnabled() { return shuffleEnabled; } void setShuffleEnabled(bool value) { shuffleEnabled = value; } bool isSkipToNext() { return skipToNext; } void setSkipToNext(bool value) { skipToNext = value; } double getSeekElapsed() { return seekElapsed; } double getPercentageElapsed() { return elapsedSeconds / duration; } void setSeekElapsed(double value) { seekElapsed = value; } bool isEOFReached() { return atomic_load(&EOFReached); } void setEOFReached() { atomic_store(&EOFReached, true); } void setEOFNotReached() { atomic_store(&EOFReached, false); } bool isImplSwitchReached() { return atomic_load(&switchReached) ? true : false; } void setImplSwitchReached() { atomic_store(&switchReached, true); } void setImplSwitchNotReached() { atomic_store(&switchReached, false); } bool isPlaying() { return ma_device_is_started(&device); } bool isPlaybackDone() { if (isEOFReached()) { return true; } else { return false; } } float getSeekPercentage() { return seekPercent; } bool isSeekRequested() { return seekRequested; } void setSeekRequested(bool value) { seekRequested = value; } void seekPercentage(float percent) { seekPercent = percent; seekRequested = true; } void resumePlayback() { if (!ma_device_is_started(&device)) { ma_device_start(&device); } paused = false; if (appState.currentView != SONG_VIEW) { refresh = true; } } void pausePlayback() { if (ma_device_is_started(&device)) { ma_device_stop(&device); } paused = true; if (appState.currentView != SONG_VIEW) { refresh = true; } } void cleanupPlaybackDevice() { ma_device_stop(&device); while (ma_device_get_state(&device) != ma_device_state_stopped && ma_device_get_state(&device) != ma_device_state_uninitialized) { c_sleep(100); } ma_device_uninit(&device); } void togglePausePlayback() { if (ma_device_is_started(&device)) { pausePlayback(); } else if (paused) { resumePlayback(); } } bool isPaused() { return paused; } pthread_mutex_t deviceMutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t deviceStopped = PTHREAD_COND_INITIALIZER; void resetDevice() { pthread_mutex_lock(&deviceMutex); if (ma_device_get_state(&device) == ma_device_state_started) ma_device_stop(&device); while (ma_device_get_state(&device) == ma_device_state_started) { pthread_cond_wait(&deviceStopped, &deviceMutex); } pthread_mutex_unlock(&deviceMutex); ma_device_uninit(&device); } ma_device *getDevice() { return &device; } bool hasBuiltinDecoder(char *filePath) { char *extension = strrchr(filePath, '.'); return (extension != NULL && (strcasecmp(extension, ".wav") == 0 || strcasecmp(extension, ".flac") == 0 || strcasecmp(extension, ".mp3") == 0)); } void activateSwitch(AudioData *pAudioData) { setSkipToNext(false); if (!isRepeatEnabled()) { pthread_mutex_lock(&switchMutex); pAudioData->currentFileIndex = 1 - pAudioData->currentFileIndex; // Toggle between 0 and 1 pthread_mutex_unlock(&switchMutex); } pAudioData->switchFiles = true; } void sanitize_filepath(const char *input, char *sanitized, size_t size) { size_t i, j = 0; if (input != NULL) { for (i = 0; i < strlen(input) && j < size - 1; ++i) { if (isalnum((unsigned char)input[i]) || strchr(" :[]()/.,?!-", input[i])) { sanitized[j++] = input[i]; } } } sanitized[j] = '\0'; } char *remove_blacklisted_chars(const char *input, const char *blacklist) { if (!input || !blacklist) return NULL; char *output = malloc(strlen(input) + 1); if (!output) { perror("Failed to allocate memory"); exit(EXIT_FAILURE); } const char *in_ptr = input; char *out_ptr = output; while (*in_ptr) { // If the current character is not in the blacklist, copy it to the output if (!strchr(blacklist, *in_ptr)) { *out_ptr++ = *in_ptr; } in_ptr++; } *out_ptr = '\0'; return output; } int displaySongNotification(const char *artist, const char *title, const char *cover) { if (!allowNotifications) return 0; char command[MAXPATHLEN + 1024]; char sanitized_cover[MAXPATHLEN]; const char *blacklist = "&;`|*~<>^()[]{}$\\\""; char *sanitizedArtist = remove_blacklisted_chars(artist, blacklist); char *sanitizedTitle = remove_blacklisted_chars(title, blacklist); sanitize_filepath(cover, sanitized_cover, sizeof(sanitized_cover)); if (strlen(artist) <= 0) { snprintf(command, sizeof(command), "notify-send -a \"kew\" \"%s\" --icon \"%s\"", sanitizedTitle, sanitized_cover); } else { snprintf(command, sizeof(command), "notify-send -a \"kew\" \"%s - %s\" --icon \"%s\"", sanitizedArtist, sanitizedTitle, sanitized_cover); } free(sanitizedArtist); free(sanitizedTitle); return system(command); } void executeSwitch(AudioData *pAudioData) { pAudioData->switchFiles = false; switchDecoder(); switchOpusDecoder(); switchM4aDecoder(); switchVorbisDecoder(); pAudioData->pUserData->currentSongData = (pAudioData->currentFileIndex == 0) ? pAudioData->pUserData->songdataA : pAudioData->pUserData->songdataB; pAudioData->totalFrames = 0; pAudioData->currentPCMFrame = 0; setSeekElapsed(0.0); setEOFReached(); } int getCurrentVolume() { return soundVolume; } int getSystemVolume() { FILE *fp; char command_str[1000]; char buf[100]; int currentVolume = -1; // Build the command string snprintf(command_str, sizeof(command_str), "pactl get-sink-volume @DEFAULT_SINK@ | awk 'NR==1{print $5}'"); // Execute the command and read the output fp = popen(command_str, "r"); if (fp != NULL) { if (fgets(buf, sizeof(buf), fp) != NULL) { sscanf(buf, "%d", ¤tVolume); } pclose(fp); } return currentVolume; } void setVolume(int volume) { if (volume > 100) { volume = 100; } else if (volume < 0) { volume = 0; } soundVolume = volume; ma_device_set_master_volume(getDevice(), (float)volume / 100); } int adjustVolumePercent(int volumeChange) { int sysVol = getSystemVolume(); if (sysVol == 0) return 0; int step = 100 / sysVol * 5; int relativeVolChange = volumeChange / 5 * step; soundVolume += relativeVolChange; setVolume(soundVolume); return 0; } ma_uint64 lastCursor = 0; void m4a_read_pcm_frames(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { m4a_decoder *m4a = (m4a_decoder *)pDataSource; AudioData *pAudioData = (AudioData *)m4a->pReadSeekTellUserData; ma_uint64 framesRead = 0; while (framesRead < frameCount) { if (doQuit) return; if (isImplSwitchReached()) return; if (pthread_mutex_trylock(&dataSourceMutex) != 0) { return; } // Check if a file switch is required if (pAudioData->switchFiles) { executeSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); break; // Exit the loop after the file switch } if (getCurrentImplementationType() != M4A && !isSkipToNext()) { pthread_mutex_unlock(&dataSourceMutex); return; } m4a_decoder *decoder = getCurrentM4aDecoder(); if (pAudioData->totalFrames == 0) ma_data_source_get_length_in_pcm_frames(decoder, &pAudioData->totalFrames); // Check if seeking is requested if (isSeekRequested()) { ma_uint64 totalFrames = 0; m4a_decoder_get_length_in_pcm_frames(decoder, &totalFrames); ma_uint64 seekPercent = getSeekPercentage(); if (seekPercent >= 100.0) seekPercent = 100.0; ma_uint64 targetFrame = (totalFrames * seekPercent) / 100 - 1; // Remove one frame or we get invalid args if we send in totalframes // Set the read pointer for the decoder ma_result seekResult = m4a_decoder_seek_to_pcm_frame(decoder, targetFrame); if (seekResult != MA_SUCCESS) { // Handle seek error setSeekRequested(false); pthread_mutex_unlock(&dataSourceMutex); return; } setSeekRequested(false); // Reset seek flag } // Read from the current decoder ma_uint64 framesToRead = 0; ma_result result; ma_uint64 remainingFrames = frameCount - framesRead; m4a_decoder *firstDecoder = getFirstM4aDecoder(); ma_uint64 cursor = 0; if (firstDecoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } result = ma_data_source_read_pcm_frames(firstDecoder, (ma_int32 *)pFramesOut + framesRead * pAudioData->channels, remainingFrames, &framesToRead); ma_data_source_get_cursor_in_pcm_frames(decoder, &cursor); if (((cursor != 0 && cursor == lastCursor) || framesToRead == 0 || isSkipToNext() || result != MA_SUCCESS) && !isEOFReached()) { activateSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); continue; } lastCursor = cursor; framesRead += framesToRead; setBufferSize(framesToRead); pthread_mutex_unlock(&dataSourceMutex); } ma_int32 *audioBuffer = getAudioBuffer(); if (audioBuffer == NULL) { audioBuffer = malloc(sizeof(ma_int32) * MAX_BUFFER_SIZE); if (audioBuffer == NULL) { // Memory allocation failed return; } } // No format conversion needed, just copy the audio samples memcpy(audioBuffer, pFramesOut, sizeof(ma_int32) * framesRead); setAudioBuffer(audioBuffer); if (pFramesRead != NULL) { *pFramesRead = framesRead; } } void m4a_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount) { AudioData *pDataSource = (AudioData *)pDevice->pUserData; ma_uint64 framesRead = 0; m4a_read_pcm_frames(&pDataSource->base, pFramesOut, frameCount, &framesRead); (void)pFramesIn; } void opus_read_pcm_frames(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { ma_libopus *opus = (ma_libopus *)pDataSource; AudioData *pAudioData = (AudioData *)opus->pReadSeekTellUserData; ma_uint64 framesRead = 0; while (framesRead < frameCount) { if (doQuit) return; if (isImplSwitchReached()) return; if (pthread_mutex_trylock(&dataSourceMutex) != 0) { return; } // Check if a file switch is required if (pAudioData->switchFiles) { executeSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); break; // Exit the loop after the file switch } if (getCurrentImplementationType() != OPUS && !isSkipToNext()) { pthread_mutex_unlock(&dataSourceMutex); return; } ma_libopus *decoder = getCurrentOpusDecoder(); if (pAudioData->totalFrames == 0) ma_data_source_get_length_in_pcm_frames(decoder, &pAudioData->totalFrames); // Check if seeking is requested if (isSeekRequested()) { ma_uint64 totalFrames = 0; ma_libopus_get_length_in_pcm_frames(decoder, &totalFrames); ma_uint64 seekPercent = getSeekPercentage(); if (seekPercent >= 100.0) seekPercent = 100.0; ma_uint64 targetFrame = (totalFrames * seekPercent) / 100 - 1; // Remove one frame or we get invalid args if we send in totalframes // Set the read pointer for the decoder ma_result seekResult = ma_libopus_seek_to_pcm_frame(decoder, targetFrame); if (seekResult != MA_SUCCESS) { // Handle seek error setSeekRequested(false); pthread_mutex_unlock(&dataSourceMutex); return; } setSeekRequested(false); // Reset seek flag } // Read from the current decoder ma_uint64 framesToRead = 0; ma_result result; ma_uint64 remainingFrames = frameCount - framesRead; ma_libopus *firstDecoder = getFirstOpusDecoder(); ma_uint64 cursor = 0; if (firstDecoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } result = ma_data_source_read_pcm_frames(firstDecoder, (ma_int32 *)pFramesOut + framesRead * pAudioData->channels, remainingFrames, &framesToRead); ma_data_source_get_cursor_in_pcm_frames(decoder, &cursor); if (((cursor != 0 && cursor >= pAudioData->totalFrames) || framesToRead == 0 || isSkipToNext() || result != MA_SUCCESS) && !isEOFReached()) { activateSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); continue; } framesRead += framesToRead; setBufferSize(framesToRead); pthread_mutex_unlock(&dataSourceMutex); } ma_int32 *audioBuffer = getAudioBuffer(); if (audioBuffer == NULL) { audioBuffer = malloc(sizeof(ma_int32) * MAX_BUFFER_SIZE); if (audioBuffer == NULL) { // Memory allocation failed return; } } // No format conversion needed, just copy the audio samples memcpy(audioBuffer, pFramesOut, sizeof(ma_int32) * framesRead); setAudioBuffer(audioBuffer); if (pFramesRead != NULL) { *pFramesRead = framesRead; } } void opus_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount) { AudioData *pDataSource = (AudioData *)pDevice->pUserData; ma_uint64 framesRead = 0; opus_read_pcm_frames(&pDataSource->base, pFramesOut, frameCount, &framesRead); (void)pFramesIn; } void vorbis_read_pcm_frames(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { ma_libvorbis *vorbis = (ma_libvorbis *)pDataSource; AudioData *pAudioData = (AudioData *)vorbis->pReadSeekTellUserData; ma_uint64 framesRead = 0; while (framesRead < frameCount) { if (doQuit) return; if (isImplSwitchReached()) return; if (pthread_mutex_trylock(&dataSourceMutex) != 0) { return; } // Check if a file switch is required if (pAudioData->switchFiles) { executeSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); break; // Exit the loop after the file switch } ma_libvorbis *decoder = getCurrentVorbisDecoder(); if ((getCurrentImplementationType() != VORBIS && !isSkipToNext()) || (decoder == NULL)) { pthread_mutex_unlock(&dataSourceMutex); return; } if (isSeekRequested()) { // disabled for ogg vorbis setSeekRequested(false); } // Read from the current decoder ma_uint64 framesToRead = 0; ma_result result; ma_uint64 remainingFrames = frameCount - framesRead; ma_libvorbis *firstDecoder = getFirstVorbisDecoder(); if (firstDecoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } result = ma_data_source_read_pcm_frames(firstDecoder, (ma_int32 *)pFramesOut + framesRead * pAudioData->channels, remainingFrames, &framesToRead); if ((getPercentageElapsed() >= 1.0 || isSkipToNext() || result != MA_SUCCESS) && !isEOFReached()) { activateSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); continue; } framesRead += framesToRead; setBufferSize(framesToRead); pthread_mutex_unlock(&dataSourceMutex); } ma_int32 *audioBuffer = getAudioBuffer(); if (audioBuffer == NULL) { audioBuffer = malloc(sizeof(ma_int32) * MAX_BUFFER_SIZE); if (audioBuffer == NULL) { // Memory allocation failed return; } } // No format conversion needed, just copy the audio samples memcpy(audioBuffer, pFramesOut, sizeof(ma_int32) * framesRead); setAudioBuffer(audioBuffer); if (pFramesRead != NULL) { *pFramesRead = framesRead; } } void vorbis_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount) { AudioData *pDataSource = (AudioData *)pDevice->pUserData; ma_uint64 framesRead = 0; vorbis_read_pcm_frames(&pDataSource->base, pFramesOut, frameCount, &framesRead); (void)pFramesIn; } kew-2.4.2/src/soundcommon.h000066400000000000000000000161551457005474400156300ustar00rootroot00000000000000#ifndef SOUND_COMMON_H #define SOUND_COMMON_H #include #include #include #include #include #include "m4a.h" #include #include #include #include "file.h" #include "utils.h" #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef MAX_BUFFER_SIZE #define MAX_BUFFER_SIZE 4800 #endif #ifndef TAGSETTINGS_STRUCT #define TAGSETTINGS_STRUCT typedef struct { char title[256]; char artist[256]; char album_artist[256]; char album[256]; char date[256]; } TagSettings; #endif #ifndef SONGDATA_STRUCT #define SONGDATA_STRUCT typedef struct { gchar *trackId; char filePath[MAXPATHLEN]; char coverArtPath[MAXPATHLEN]; unsigned char red; unsigned char green; unsigned char blue; TagSettings *metadata; FIBITMAP *cover; double duration; bool hasErrors; } SongData; #endif #ifndef USERDATA_STRUCT #define USERDATA_STRUCT typedef struct { SongData *songdataA; SongData *songdataB; bool songdataADeleted; bool songdataBDeleted; SongData *currentSongData; ma_uint32 currentPCMFrame; } UserData; #endif #ifndef AUDIODATA_STRUCT #define AUDIODATA_STRUCT typedef struct { ma_data_source_base base; UserData *pUserData; ma_format format; ma_uint32 channels; ma_uint32 sampleRate; ma_uint32 currentPCMFrame; bool switchFiles; int currentFileIndex; ma_uint64 totalFrames; bool endOfListReached; bool restart; } AudioData; #endif #ifndef KEYVALUEPAIR_STRUCT #define KEYVALUEPAIR_STRUCT typedef struct { char *key; char *value; } KeyValuePair; #endif #ifndef APPSETTINGS_STRUCT typedef struct { char path[MAXPATHLEN]; char coverEnabled[2]; char coverAnsi[2]; char useProfileColors[2]; char visualizerEnabled[2]; char visualizerHeight[6]; char togglePlaylist[6]; char toggleBindings[6]; char volumeUp[6]; char volumeUpAlt[6]; char volumeDown[6]; char previousTrackAlt[6]; char nextTrackAlt[6]; char scrollUpAlt[6]; char scrollDownAlt[6]; char switchNumberedSong[6]; char switchNumberedSongAlt[6]; char switchNumberedSongAlt2[6]; char togglePause[6]; char toggleColorsDerivedFrom[6]; char toggleVisualizer[6]; char toggleCovers[6]; char toggleAscii[6]; char toggleRepeat[6]; char toggleShuffle[6]; char seekBackward[6]; char seekForward[6]; char savePlaylist[6]; char addToMainPlaylist[6]; char updateLibrary[6]; char quit[6]; char hardSwitchNumberedSong[6]; char hardPlayPause[6]; char hardPrev[6]; char hardNext[6]; char hardScrollUp[6]; char hardScrollDown[6]; char hardShowInfo[6]; char hardShowInfoAlt[6]; char hardShowKeys[6]; char hardShowKeysAlt[6]; char hardEndOfPlaylist[6]; char hardShowLibrary[6]; char hardShowLibraryAlt[6]; char hardShowTrack[6]; char hardShowTrackAlt[6]; char hardNextPage[6]; char hardPrevPage[6]; char hardRemove[6]; char lastVolume[6]; char allowNotifications[2]; char color[2]; char artistColor[2]; char enqueuedColor[2]; char titleColor[2]; char hideLogo[2]; char hideHelp[2]; char cacheLibrary[6]; } AppSettings; #endif enum AudioImplementation { PCM, BUILTIN, VORBIS, OPUS, M4A, NONE }; typedef enum { SONG_VIEW, KEYBINDINGS_VIEW, PLAYLIST_VIEW, LIBRARY_VIEW } ViewState; typedef struct { ViewState currentView; } AppState; extern AppState appState; extern bool allowNotifications; extern volatile bool refresh; extern double duration; extern bool doQuit; extern double elapsedSeconds; extern pthread_mutex_t dataSourceMutex; extern pthread_mutex_t switchMutex; enum AudioImplementation getCurrentImplementationType(); void setCurrentImplementationType(enum AudioImplementation value); int getBufferSize(); void setBufferSize(int value); void setPlayingStatus(bool playing); bool isPlaying(); ma_decoder *getFirstDecoder(); ma_decoder *getCurrentBuiltinDecoder(); ma_decoder *getPreviousDecoder(); ma_format getCurrentFormat(); void resetDecoders(); ma_libopus *getCurrentOpusDecoder(); void resetOpusDecoders(); m4a_decoder *getCurrentM4aDecoder(); m4a_decoder *getFirstM4aDecoder(); ma_libopus *getFirstOpusDecoder(); ma_libvorbis *getFirstVorbisDecoder(); void getVorbisFileInfo(const char *filename, ma_format *format, ma_uint32*channels, ma_uint32 *sampleRate, ma_channel *channelMap); void getM4aFileInfo(const char *filename, ma_format *format, ma_uint32*channels, ma_uint32 *sampleRate, ma_channel *channelMap); void getOpusFileInfo(const char *filename, ma_format *format, ma_uint32*channels, ma_uint32 *sampleRate, ma_channel *channelMap); ma_libvorbis *getCurrentVorbisDecoder(); void switchVorbisDecoder(); int prepareNextOpusDecoder(char *filepath); int prepareNextOpusDecoder(char *filepath); int prepareNextVorbisDecoder(char *filepath); int prepareNextM4aDecoder(char *filepath); void resetVorbisDecoders(); void resetM4aDecoders(); ma_libvorbis *getFirstVorbisDecoder(); void getFileInfo(const char* filename, ma_uint32* sampleRate, ma_uint32* channels, ma_format* format); void initAudioBuffer(); ma_int32 *getAudioBuffer(); void setAudioBuffer(ma_int32 *buf); void resetAudioBuffer(); void freeAudioBuffer(); bool isRepeatEnabled(); void setRepeatEnabled(bool value); bool isShuffleEnabled(); void setShuffleEnabled(bool value); bool isSkipToNext(); void setSkipToNext(bool value); double getSeekElapsed(); void setSeekElapsed(double value); bool isEOFReached(); void setEOFReached(); void setEOFNotReached(); bool isImplSwitchReached(); void setImplSwitchReached(); void setImplSwitchNotReached(); bool isPlaybackDone(); float getSeekPercentage(); double getPercentageElapsed(); bool isSeekRequested(); void setSeekRequested(bool value); void seekPercentage(float percent); void resumePlayback(); void pausePlayback(); void cleanupPlaybackDevice(); void togglePausePlayback(); bool isPaused(); void resetDevice(); ma_device *getDevice(); bool hasBuiltinDecoder(char *filePath); void activateSwitch(AudioData *pPCMDataSource); void executeSwitch(AudioData *pPCMDataSource); int displaySongNotification(const char *artist, const char *title, const char *cover); int getCurrentVolume(void); void setVolume(int volume); int adjustVolumePercent(int volumeChange); void m4a_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount); void opus_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount); void vorbis_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount); #endif kew-2.4.2/src/term.c000066400000000000000000000103101457005474400142140ustar00rootroot00000000000000#include "term.h" /* term.c This file should contain only simple utility functions related to the terminal. They should work independently and be as decoupled from the application as possible. */ volatile sig_atomic_t resizeFlag = 0; void setTextColor(int color) { /* - 0: Black - 1: Red - 2: Green - 3: Yellow - 4: Blue - 5: Magenta - 6: Cyan - 7: White */ printf("\033[0;3%dm", color); } void setTextColorRGB(int r, int g, int b) { printf("\033[0;38;2;%03u;%03u;%03um", r, g, b); } void getTermSize(int *width, int *height) { struct winsize w; ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); *height = (int)w.ws_row; *width = (int)w.ws_col; } void setNonblockingMode() { struct termios ttystate; tcgetattr(STDIN_FILENO, &ttystate); ttystate.c_lflag &= ~ICANON; ttystate.c_cc[VMIN] = 0; ttystate.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); } void restoreTerminalMode() { struct termios ttystate; tcgetattr(STDIN_FILENO, &ttystate); ttystate.c_lflag |= ICANON; tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); } void saveCursorPosition() { printf("\033[s"); } void restoreCursorPosition() { printf("\033[u"); } void setDefaultTextColor() { printf("\033[0m"); } int isInputAvailable() { fd_set fds; FD_ZERO(&fds); FD_SET(STDIN_FILENO, &fds); struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 0; int ret = select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv); if (ret < 0) { return 0; } int result = (ret > 0) && (FD_ISSET(STDIN_FILENO, &fds)); return result; } void hideCursor() { printf("\033[?25l"); fflush(stdout); } void showCursor() { printf("\033[?25h"); fflush(stdout); } void resetConsole() { int status = system("reset"); if (status == -1) { perror("system() failed"); } } void clearRestOfScreen() { printf("\033[J"); } void clearScreen() { printf("\033[2J"); printf("\033[H"); printf("\033[1;1H"); } void enableScrolling() { printf("\033[?7h"); } void handleResize(int sig) { (void)sig; resizeFlag = 1; } void resetResizeFlag(int sig) { (void)sig; resizeFlag = 0; } void initResize() { signal(SIGWINCH, handleResize); struct sigaction sa; sa.sa_handler = resetResizeFlag; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGALRM, &sa, NULL); } void disableInputBuffering(void) { struct termios term; tcgetattr(STDIN_FILENO, &term); term.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); } void enableInputBuffering() { struct termios term; tcgetattr(STDIN_FILENO, &term); term.c_lflag |= ICANON | ECHO; tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); } void cursorJump(int numRows) { printf("\033[%dA", numRows); printf("\033[0m"); } void cursorJumpDown(int numRows) { printf("\033[%dB", numRows); } int readInputSequence(char *seq, size_t seqSize) { char c; ssize_t bytesRead; seq[0] = '\0'; bytesRead = read(STDIN_FILENO, &c, 1); if (bytesRead <= 0) { return 0; } // If it's not an escape character, return it as a single-character sequence if (c != '\x1b') { seq[0] = c; seq[1] = '\0'; return 1; } // Read additional characters if (seqSize < 3) return 0; bytesRead = read(STDIN_FILENO, seq, 2); if ((bytesRead <= 0) & (seq[0] == '\0')) { seq[0] = '\0'; return 0; } seq[bytesRead] = '\0'; return bytesRead; } int getIndentation(int terminalWidth) { int term_w, term_h; getTermSize(&term_w, &term_h); int indent = ((term_w - terminalWidth) / 2) + 1; return (indent > 0) ? indent : 0; } kew-2.4.2/src/term.h000066400000000000000000000022571457005474400142340ustar00rootroot00000000000000#ifndef TERM_H #define TERM_H #ifndef __USE_POSIX #define __USE_POSIX #endif #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __GNU__ # define _BSD_SOURCE #endif extern volatile sig_atomic_t resizeFlag; void setTextColor(int color); void setTextColorRGB(int r, int g, int b); void getTermSize(int *width, int *height); int getIndentation(int terminalWidth); void setNonblockingMode(void); void restoreTerminalMode(void); void setDefaultTextColor(void); int isInputAvailable(void); void resetConsole(void); void saveCursorPosition(void); void restoreCursorPosition(void); void hideCursor(void); void showCursor(void); void clearRestOfScreen(void); void enableScrolling(void); void handleResize(int sig); void resetResizeFlag(int sig); void initResize(void); void disableInputBuffering(void); void enableInputBuffering(void); void cursorJump(int numRows); void cursorJumpDown(int numRows); void clearScreen(void); int readInputSequence(char *seq, size_t seqSize); #endif kew-2.4.2/src/utils.c000066400000000000000000000130071457005474400144130ustar00rootroot00000000000000#include "utils.h" /* utils.c Utility functions for instance for replacing some standard functions with safer alterantives. */ void c_sleep(int milliseconds) { struct timespec ts; ts.tv_sec = milliseconds / 1000; ts.tv_nsec = milliseconds % 1000 * 1000000; nanosleep(&ts, NULL); } void c_usleep(int microseconds) { struct timespec ts; ts.tv_sec = microseconds / 1000000; ts.tv_nsec = microseconds % 1000; nanosleep(&ts, NULL); } void c_strcpy(char *dest, size_t dest_size, const char *src) { if (dest && dest_size > 0 && src) { size_t src_length = strlen(src); if (src_length < dest_size) { strncpy(dest, src, dest_size); } else { strncpy(dest, src, dest_size - 1); dest[dest_size - 1] = '\0'; } } } char *stringToLower(char *str) { for (int i = 0; str[i] != '\0'; i++) { str[i] = tolower(str[i]); } return str; } char *stringToUpper(char *str) { for (int i = 0; str[i] != '\0'; i++) { str[i] = toupper(str[i]); } return str; } char *c_strcasestr(const char *haystack, const char *needle) { if (!haystack || !needle) return NULL; size_t needleLen = strlen(needle); size_t haystackLen = strlen(haystack); if (needleLen > haystackLen) return NULL; for (size_t i = 0; i <= haystackLen - needleLen; i++) { size_t j; for (j = 0; j < needleLen; j++) { if (tolower(haystack[i + j]) != tolower(needle[j])) break; } if (j == needleLen) return (char *)(haystack + i); } return NULL; } int match_regex(regex_t *regex, const char *ext) { if (regex == NULL || ext == NULL) { fprintf(stderr, "Invalid arguments\n"); return 1; } regmatch_t pmatch[1]; // Array to store match results int ret = regexec(regex, ext, 1, pmatch, 0); if (ret == REG_NOMATCH) { return 1; } else if (ret == 0) { return 0; } else { char errorBuf[100]; regerror(ret, regex, errorBuf, sizeof(errorBuf)); fprintf(stderr, "Regex match failed: %s\n", errorBuf); return 1; } } void extractExtension(const char *filename, size_t numChars, char *ext) { size_t length = strlen(filename); size_t copyChars = length < numChars ? length : numChars; const char *extStart = filename + length - copyChars; strncpy(ext, extStart, copyChars); ext[copyChars] = '\0'; } int endsWith(const char *str, const char *suffix) { size_t strLength = strlen(str); size_t suffixLength = strlen(suffix); if (suffixLength > strLength) { return 0; } const char *strSuffix = str + (strLength - suffixLength); return strcmp(strSuffix, suffix) == 0; } int startsWith(const char *str, const char *prefix) { size_t strLength = strlen(str); size_t prefixLength = strlen(prefix); if (prefixLength > strLength) { return 0; } return strncmp(str, prefix, prefixLength) == 0; } void trim(char *str) { char *start = str; while (*start && isspace(*start)) { start++; } char *end = str + strlen(str) - 1; while (end > start && isspace(*end)) { end--; } *(end + 1) = '\0'; if (str != start) { memmove(str, start, end - start + 2); } } const char *getHomePath() { const char *xdgHome = getenv("XDG_HOME"); if (xdgHome == NULL) { xdgHome = getenv("HOME"); if (xdgHome == NULL) { struct passwd *pw = getpwuid(getuid()); if (pw != NULL) { char *home = (char *)malloc(MAXPATHLEN); strcpy(home, pw->pw_dir); return home; } } } return xdgHome; } char *getConfigPath() { char *configPath = malloc(MAXPATHLEN); if (!configPath) return NULL; const char *xdgConfig = getenv("XDG_CONFIG_HOME"); if (xdgConfig) { snprintf(configPath, MAXPATHLEN, "%s", xdgConfig); } else { const char *home = getHomePath(); if (home) { snprintf(configPath, MAXPATHLEN, "%s/.config", home); } else { struct passwd *pw = getpwuid(getuid()); if (pw) { snprintf(configPath, MAXPATHLEN, "%s", pw->pw_dir); } else { free(configPath); return NULL; } } } return configPath; } kew-2.4.2/src/utils.h000066400000000000000000000017031457005474400144200ustar00rootroot00000000000000#ifndef UTILS_H #define UTILS_H #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L #endif #ifndef __USE_POSIX #define __USE_POSIX #endif #include #include #include #include #include #include #include #include #include #include #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif void c_sleep(int milliseconds); void c_usleep(int microseconds); void c_strcpy(char *dest, size_t dest_size, const char *src); char *stringToUpper(char *str); char *stringToLower(char *str); char *c_strcasestr(const char *haystack, const char *needle); int match_regex(regex_t *regex, const char *ext); void extractExtension(const char *filename, size_t numChars, char *ext); int endsWith(const char *str, const char *suffix); int startsWith(const char *str, const char *prefix); void trim(char *str); const char *getHomePath(); char *getConfigPath(); #endif kew-2.4.2/src/visuals.c000066400000000000000000000270721457005474400147500ustar00rootroot00000000000000#include "visuals.h" /* visuals.c This file should contain only functions related to the spectrum visualizer. */ #ifndef MAX_BUFFER_SIZE #define MAX_BUFFER_SIZE 4800 #endif int bufferSize = 4800; int prevBufferSize = 0; float alpha = 0.1; float lastMax = 90; bool unicodeSupport = false; fftwf_complex *fftInput = NULL; fftwf_complex *fftOutput = NULL; int bufferIndex = 0; float magnitudeBuffer[MAX_BUFFER_SIZE] = {0.0f}; float lastMagnitudes[MAX_BUFFER_SIZE] = {0.0f}; void initVisuals() { unicodeSupport = false; char *locale = setlocale(LC_ALL, ""); if (locale != NULL) unicodeSupport = true; } void printBlankSpaces(int numSpaces) { for (int i = 0; i < numSpaces; i++) { printf(" "); } } void updateMagnitudes(int height, int width, float maxMagnitude, float *magnitudes) { float exponent = 1.0; float decreaseFactor = 0.8; for (int i = 0; i < width; i++) { float normalizedMagnitude = magnitudes[i] / maxMagnitude; float scaledMagnitude = pow(normalizedMagnitude, exponent) * height; if (scaledMagnitude < lastMagnitudes[i]) { magnitudes[i] = lastMagnitudes[i] * decreaseFactor; } else { magnitudes[i] = scaledMagnitude; } lastMagnitudes[i] = magnitudes[i]; } } float calcMaxMagnitude(int numBars, float *magnitudes) { float maxMagnitude = 0.0f; for (int i = 0; i < numBars; i++) { if (magnitudes[i] > maxMagnitude) { maxMagnitude = magnitudes[i]; } } lastMax = (1 - alpha) * lastMax + alpha * maxMagnitude; // Apply exponential smoothing return lastMax; } void clearMagnitudes(int width, float *magnitudes) { for (int i = 0; i < width; i++) { magnitudes[i] = 0.0f; } } void calc(int height, int numBars, ma_int32 *audioBuffer, int bitDepth, fftwf_complex *fftInput, fftwf_complex *fftOutput, float *magnitudes, fftwf_plan plan) { int bufferSize = getBufferSize(); if (audioBuffer == NULL) { printf("Audio buffer is NULL.\n"); return; } int j = 0; for (int i = 0; i < bufferSize; i++) { if (j >= bufferSize) { printf("Exceeded FFT input buffer size.\n"); break; } ma_int32 sample = audioBuffer[i]; float normalizedSample = 0; switch (bitDepth) { case 8: normalizedSample = ((float)sample - 128) / 127.0f; break; case 16: normalizedSample = (float)sample / 32768.0f; break; case 24: { int lower24Bits = sample & 0xFFFFFF; if (lower24Bits & 0x800000) { lower24Bits |= 0xFF000000; // Sign extension } normalizedSample = (float)lower24Bits / 8388607.0f; break; } case 32: // Assuming 32-bit integers normalizedSample = (float)sample / 2147483647.0f; break; default: printf("Unsupported bit depth: %d\n", bitDepth); return; } fftInput[j][0] = normalizedSample; fftInput[j][1] = 0; j++; } for (int k = j; k < bufferSize; k++) { fftInput[k][0] = 0; fftInput[k][1] = 0; } // Apply Windowing (Hamming Window) only to populated samples for (int i = 0; i < j; i++) { float window = 0.54f - 0.46f * cos(2 * M_PI * i / (j - 1)); fftInput[i][0] *= window; } fftwf_execute(plan); // Execute FFT clearMagnitudes(numBars, magnitudes); for (int i = 0, k = 0; i < numBars && k < j / 2; i += 2, k++) { // Compute magnitude for actual data points float magnitude = sqrtf(fftOutput[k][0] * fftOutput[k][0] + fftOutput[k][1] * fftOutput[k][1]); magnitudes[i] = magnitude; // Set actual magnitude if (i + 1 < numBars) { float nextMagnitude; if (k + 1 < j / 2) { // If not at the end, interpolate between this and the next actual magnitude nextMagnitude = sqrtf(fftOutput[k + 1][0] * fftOutput[k + 1][0] + fftOutput[k + 1][1] * fftOutput[k + 1][1]); magnitudes[i + 1] = (magnitude + nextMagnitude) / 2.0f; // Simple average for smoothing } else { // If at the end, could replicate the last magnitude or use a different logic for the filler magnitudes[i + 1] = magnitude; // Replicating the last magnitude } } } float maxMagnitude = calcMaxMagnitude(numBars, magnitudes); updateMagnitudes(height, numBars, maxMagnitude, magnitudes); } wchar_t *getUpwardMotionChar(int level) { switch (level) { case 0: return L" "; case 1: return L"▁"; case 2: return L"▂"; case 3: return L"▃"; case 4: return L"▄"; case 5: return L"▅"; case 6: return L"▆"; case 7: return L"▇"; default: return L"█"; } } int calcSpectrum(int height, int numBars, fftwf_complex *fftInput, fftwf_complex *fftOutput, float *magnitudes, fftwf_plan plan) { ma_int32 *g_audioBuffer = getAudioBuffer(); ma_format format = getCurrentFormat(); if (format == ma_format_unknown) return -1; int bitDepth = 32; switch (format) { case ma_format_u8: bitDepth = 8; break; case ma_format_s16: bitDepth = 16; break; case ma_format_s24: bitDepth = 24; break; case ma_format_f32: case ma_format_s32: bitDepth = 32; break; default: break; } calc(height, numBars, g_audioBuffer, bitDepth, fftInput, fftOutput, magnitudes, plan); return 0; } PixelData increaseLuminosity(PixelData pixel, int amount) { PixelData pixel2; pixel2.r = pixel.r + amount <= 255 ? pixel.r + amount : 255; pixel2.g = pixel.g + amount <= 255 ? pixel.g + amount : 255; pixel2.b = pixel.b + amount <= 255 ? pixel.b + amount : 255; return pixel2; } void printSpectrum(int height, int width, float *magnitudes, PixelData color, int indentation, bool useProfileColors) { printf("\n"); clearRestOfScreen(); PixelData tmp; for (int j = height; j > 0; j--) { printf("\r"); printBlankSpaces(indentation); if (color.r != 0 || color.g != 0 || color.b != 0) { if (!useProfileColors) { tmp = increaseLuminosity(color, round(j * height * 4)); printf("\033[38;2;%d;%d;%dm", tmp.r, tmp.g, tmp.b); } } else { setDefaultTextColor(); } if (isPaused()) { for (int i = 0; i < width; i++) { printf(" "); } } else { for (int i = 0; i < width; i++) { if (j >= 0) { if (magnitudes[i] >= j) { if (unicodeSupport) { printf(" %S", getUpwardMotionChar(10)); } else { printf(" █"); } } else if (magnitudes[i] + 1 >= j && unicodeSupport) { int firstDecimalDigit = (int)(fmod(magnitudes[i] * 10, 10)); printf(" %S", getUpwardMotionChar(firstDecimalDigit)); } else { printf(" "); } } } } printf("\n "); } printf("\r"); fflush(stdout); } void freeVisuals() { if (fftInput != NULL) { fftwf_free(fftInput); fftInput = NULL; } if (fftOutput != NULL) { fftwf_free(fftOutput); fftOutput = NULL; } } void drawSpectrumVisualizer(int height, int width, PixelData c, int indentation, bool useProfileColors) { bufferSize = getBufferSize(); PixelData color; color.r = c.r; color.g = c.g; color.b = c.b; int numBars = (width / 2); height = height - 1; if (height <= 0 || width <= 0) { return; } if (bufferSize <= 0) return; if (bufferSize != prevBufferSize) { freeVisuals(); fftInput = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * bufferSize); if (fftInput == NULL) { return; } fftOutput = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * bufferSize); if (fftOutput == NULL) { fftwf_free(fftInput); fftInput = NULL; return; } prevBufferSize = bufferSize; } fftwf_plan plan = fftwf_plan_dft_1d(bufferSize, fftInput, fftOutput, FFTW_FORWARD, FFTW_ESTIMATE); float magnitudes[numBars]; for (int i = 0; i < numBars; i++) { magnitudes[i] = 0.0f; } calcSpectrum(height, numBars, fftInput, fftOutput, magnitudes, plan); printSpectrum(height, numBars, magnitudes, color, indentation, useProfileColors); fftwf_destroy_plan(plan); } kew-2.4.2/src/visuals.h000066400000000000000000000011251457005474400147440ustar00rootroot00000000000000 #include #include #include #include #include #include #include #include "sound.h" #include "term.h" #ifndef PIXELDATA_STRUCT #define PIXELDATA_STRUCT typedef struct { unsigned char r; unsigned char g; unsigned char b; } PixelData; #endif void initVisuals(); void freeVisuals(); void drawSpectrumVisualizer(int height, int width, PixelData c, int indentation, bool useProfileColors); PixelData increaseLuminosity(PixelData pixel, int amount); void printBlankSpaces(int numSpaces);