kew-2.6.0/000077500000000000000000000000001464150306200123065ustar00rootroot00000000000000kew-2.6.0/.editorconfig000066400000000000000000000003461464150306200147660ustar00rootroot00000000000000# 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.6.0/.github/000077500000000000000000000000001464150306200136465ustar00rootroot00000000000000kew-2.6.0/.github/workflows/000077500000000000000000000000001464150306200157035ustar00rootroot00000000000000kew-2.6.0/.github/workflows/ci.yml000066400000000000000000000010571464150306200170240ustar00rootroot00000000000000name: 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.6.0/.gitignore000066400000000000000000000004671464150306200143050ustar00rootroot00000000000000# 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.6.0/CONTRIBUTING.md000066400000000000000000000026051464150306200145420ustar00rootroot00000000000000# 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.6.0/LICENSE000066400000000000000000000432541464150306200133230ustar00rootroot00000000000000 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.6.0/Makefile000066400000000000000000000036221464150306200137510ustar00rootroot00000000000000CC = 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 -O1 $(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) LDFLAGS = -pie -Wl,-z,relro ifeq ($(CC),gcc) LIBS += -latomic endif OBJDIR = src/obj PREFIX = /usr SRCS = src/common_ui.c src/sound.c src/directorytree.c src/soundcommon.c src/search_ui.c src/playlist_ui.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.6.0/README.md000066400000000000000000000152721464150306200135740ustar00rootroot00000000000000 # 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. ![Example screenshot](kew-screenshot.png) *Example screenshot running in Konsole: [Jenova 7: Lost Sci-Fi Movie Themes](https://jenova7.bandcamp.com/album/lost-sci-fi-movie-themes).* \ 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 in Debian, Ubuntu If you have apt in your system use: ```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 sixel (or equivalent) capable terminal is recommended, like Konsole or kitty, to display images properly. For a complete list of capable terminals, see this page: [Sixels in Terminal](https://www.arewesixelyet.com/). And for ### 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 albums (plays all albums, up to 2000, randomly one after the other) 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 the library. * F4 to show/hide the track view. * F5 to search. * F6 to show/hide key bindings. * u to update the 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 ."). * Esc to quit. ## Configuration kew will create a config file, kewrc, in a kew folder in your default config directory for instance ~/.config/kew. 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.6.0/SECURITY.md000066400000000000000000000013161464150306200141000ustar00rootroot00000000000000## 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.6.0/docs/000077500000000000000000000000001464150306200132365ustar00rootroot00000000000000kew-2.6.0/docs/kew-manpage.mdoc000066400000000000000000000101341464150306200162750ustar00rootroot00000000000000.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 q, --quitonstop Exits after playing the whole playlist. .It Fl e, --exact 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 albums Start .Nm with all albums randomly added one after the other in the 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 search view .It F6 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 Esc Quit .Nm . .El .Sh FILES .Bl -tag -width -compact .It Pa "~//kewrc" Config file. .It Pa "~//kew/kewlibrary" Music library directory tree cache. .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.6.0/docs/kew.1000066400000000000000000000072501464150306200141120ustar00rootroot00000000000000.\" 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\-q,\fR \fB\--quitonstop\fR Exits after playing the whole playlist. .TP 9n \fB\-e,\fR \fB\--exact, 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 albums Start \fBkew\fR with all albums randomly added one after the other in the 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 search view. .TP 9n F6 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 Esc Quit \fBkew\fR. .SH "FILES" .TP 10n \fI~//kew/kewrc\fR Config file. .TP 10n \fI~//kew/kewlibrary\fR Music library directory tree cache. .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.6.0/include/000077500000000000000000000000001464150306200137315ustar00rootroot00000000000000kew-2.6.0/include/imgtotxt/000077500000000000000000000000001464150306200156105ustar00rootroot00000000000000kew-2.6.0/include/imgtotxt/LICENSE000066400000000000000000000020561464150306200166200ustar00rootroot00000000000000MIT 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.6.0/include/imgtotxt/options.h000066400000000000000000000011261464150306200174540ustar00rootroot00000000000000#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.6.0/include/imgtotxt/write_ascii.c000066400000000000000000000111431464150306200202560ustar00rootroot00000000000000/* 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.6.0/include/imgtotxt/write_ascii.h000066400000000000000000000010061464150306200202600ustar00rootroot00000000000000#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.6.0/install.sh000066400000000000000000000053431464150306200143150ustar00rootroot00000000000000#!/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.6.0/kew-screenshot.png000066400000000000000000005543121464150306200157670ustar00rootroot00000000000000PNG  IHDREb/ pHYs+ tEXtlogicalX2438x \ tEXtlogicalY200l6;iTXtwindowTitleUTF-8windowTitlekew-2.5.0 : kew — Konsole8t IDATx^eGq˓/IT$Mɢ* BF,4b1F 4?f=C,.;mwcmC3C3BfiFnn%d !H2y Nx8׻{YY,~SWn܈'N=q_1bĈ#FG?{;1bĈ#F|1E#F1bĈFш#F1b0E#F1bĈh1bĈ#FQ4bĈ#FFш#F1b0E#F1bĈh1bĈ#FQ4bĈ#FFш#F1b0E#F1bĈh1bĈ#FQ4bĈ#FFш#F1bM7n_P~V}3,Eo!U@7݇` Fq >3!nG?CDt"jޛe:Mf(upZ2َ3{j6m BJGOOxjd%n*Cp)34:bĈ! --n \I)=it:6):hTɚ:/_Qi.'T%4ה҈/F +EW_UhK*oT֏g9~%8Fu1b@4qmHX:74&uh.jEP5mtSjCWeN.\Yx_.#wFQEӵk AZ/ XV|S SZ{Rn18p1~obԈBOߩ}E>#eˆÌ/)]O7>ZE@!z%LesZ#FYBS!dP!zS_X$#+4;N꤭_v*(d0B TJdĈBǃRSE> ]a:B̘Hz(4^gTkh?hx+yU)9 *mQpd(YZD'M|UնG~huyTbsbԛ?bQD=OZ"4M"JB!)UIz Su6ECqPkwJ~atTۈ_TlUYL6<p0`YfM4,vS7$nսn^-D(2)G!K?>}!ɢyM\2v4{~5C4ٮ,آz堖WSģ ܬ **82+ T>m]UM(ox tr4aCqtN֗#z{(vGf}ՙtaQ(YWҏװ?e{nq.42ꮭ4'& 0*~y^+/u謃HbJI(jyhKr%2sgPG `4PQ|p_uCE3W30 $-b(>} D5 ?dI@bciPL>љ%e-f-噅0~UjYdMCaƱW%Eь&ރR@LmLu֞_!hf2Va$є9]p SmyDwG2(f$&ʭѺXx%$.v͍H2( C^&V(J.qc׻L?kW_d[ ?`_xr~A/o3*lW)˚:pk#` sTgHFi~{M8&cs\ yKuXm!>P_W;Q^drӯTAxVЙ_&6}c eakAp;ŲK.GX2>VSl|P^L> =G&H{x!  fSM&/2yy #?b(1Lq1X /'y4*9bMMѥU\7V_m/N!m}y4:i=e闞!Y|r?WJ\Uvsq`e)AoS­b㕋|L#\A E4Z<(,  V&Pnaۆ"h}f0<0 PnGž| 3B(Z>Frk⫺.v;|w 0Sd㽵`Jʽi;KOEB.WGX@v{R]F&s,n2π /b/E/`ޤ>G6-héɨnW2r0f< HQ%nfYMO@PHoҗ=,"C4\.P ѯ:qiȂ a[nBռCՇn!tX~̔%}I¤@Qz*ө„4Մ uэ[*n0q(kcQBH_ Zb=}o_E1r בWgw>(W3A<)}I_uU~xLnzV7GTK4=`nԈBLe.D0OվAeiVBz};;m&<]%8 +u;y檘q;Ј,vq&BZv#,Tk5 ¤; ~(|dÖܴ/ }[,!*;F[yVg:3HBWgrmd>7B[6DoBZQ cYcf7nj;Cg ;E%gAEI$8)NR^+DxK^~E$WMWyrant CA@0w9R$<̿;\&dP[4d\g4ľBA"qI͖ќ Ψ,MH NHrh}kmy)cc)AGSҋɫ=z]IN(p~lҗ\SzVݕ[ 34w\pp5E`ڡnJWE3~7? ;&ʭW*OPDLC$NDXV\* tl\uu{+O'}1ԠեFOkeA4$%]rImr/raVa Xe,Y{ؙ̆g OokMv& 5nu㯃!w丫Ҝ= T6nAޖw'$}!w|ۏ^4)x\lygRrG^]Z+)ž  X?aٳc%y*vi1}aN N8Iܳ/G>y?LCyPxlEyw6-S1b&vP0ӛP$WF*^b%c߅wNinz&Z' 4TΞ&B,]!^M0\Q.2I IKM\gX_.ڐzΒ,vMWFg+WOwe=k^mPB0r>ljM܍t7inJ ͽ_x{zku`KŇS&}WЪEQz\>=vP:JFO{_T c(ɼ5]*zRQ*}jY2 $QXz4Mdo;7lS)VTM&z[Z]bݤ RSTKSw ffs!_^]8&٦_@!̬2Ja"̔4 6œ"z/ׁͧ? aJy`7PP:<ʭ}pʳhYg/ Q'8j H*յbW 採Vs<BS'\/ɚrkA6Fl'k)@XxiM.jM(@ců=M"ޓ&Ė~rvܱ,,OfC4pUO2;PҗZA_MevIv QrWvx?4r]17{WI0Zm"-njF@m$zN{>$eՆQGÐ'RA7UTn-ުUrC5!:rOg&x5ϑ=MOyې]} 6|UzsAl)fuېUm D&xHfO3Ǡ?2ĝ#_gbޮ3v5)<;2'uzЋQ , ޮd e,zVLۑ8//pSE;LeH0k_o\4.0uښmO,@'% DŽcpV*Gb܅؁; ź NiFl3)#q}0?l}udhishk.)WTpژYcMj+h=2rO-ۿۡ~3EPS U}uāf)'"(*[tWGʹkC[r#$D_9jbc6F`;ozbf *uN6G:Xr<[ qiC `/cZX.{'iؙ5C!ov"m|Ėգ_6(1u,v}o5 amUg0Z=`fuXnٻσFǭl{P,Mˢn@P6/D([\#z~4V+&)0 wX'9GmJj޳Ֆ7)*'VdIе2k%ȫw(zMA&с[fyóyäݴsr6ǮAKY*.V3[+R-zD-]y:q(&pxیJͲʞ.J'AsyzuÂJ&H'ީ`܀t ٻmgEĄ(m) pUyuD7.DFM|nFQ er6ɫY[,&w':@KAoA\`u\aZCЬFOϣ7lK ؒ\Ižm6GoUe1+\)ۓq`$}0\49t!r#+Gl.Ͱ19fH.j"\LvS^m'd ѫaO]5q$^)ilFh.|][eVmciiz.Gl[H]}EldzIi{Z+6 `~PV{[̀Mhwq&.VO-DEFf lasFӫ٥iƁ]`:$2#3d[]; 1C6rm:(&]eV$&r^Su%> A"HMMzMbpu[YDCX- >nǿ -U V~ ׆\Ӻ\?=#)jNG$yѠQtƿ) Co7.1w\DsQW*֙^6>X9U65Lea(W4.+#gT>{.ϨB*Cs)N&./r]R.X[+ߪlI<}F[ۀU;n\ߙ=aEzm]j ),>94p2)OV uf鹀N[ GB(.Y?jWY|k=SI$2'reϞ ς#CT}Qɉ HC(OrhD!yJeߞz_Rb*$ͦ0Y*^謊?w_w_|G`SJC+f u3H}pZxJ&DP?Ά "!+0$UQ3&r7XbERhԷW]Y,#bqD~\2HZH2nZo+VCv*)5JƷAc_Ǫs~Y>g郀O빯mg?RxxkI2RT3,emlM{E FiLV3~!Z{xF4@emL^\6?FrY|2}dWr636[w_j Uq}X]/N-)un<,N0:Ũ* IDAT/BY{z{!ѥZ 9q3K 4s4FpeT߫ל}y&Zwv9}_E)9$>FEt+KKb|doXU)Τx)Z1GV&זj'AqR8$UTF$Py^2! 7ohY:JhRGjDS;MN+쩨vv/)$/Q/$fN˹ET\HnA1MNabޢn \# Tƚ3W͑9љ^5 fۓ,ޫ#h]eU2/6/%k_|NFQ Ѱh+>tNJcSB98jQ=k!c1k:9YJ˸h)zKo?Y˘ f5U`SxWqMxβ6#Gy3~,:2@{Mz$Y[>F3F|aD|iZ4M4 4DڋU_MYX'#[We Fa)_|#jem`r uYW܉&?B@hJ}yѩ֛(K). ydZڲJ䡖K7?~Z=-aaYMƺmEu?uAYC[y\9MO?_ Y.k}/P{&v9 = ZCտT&} _5jګJU(sʢ)e{@N4̿;F>eg{aM:t9GgL-Y _~h]wѼo>i.V޻8.q7iҺF1bĈ#\܍!W߈ܸ߾#n|p?wrFyq1#Q4b-fĈ#.=۸Fр[Ͱl&kUxYXE󪸛K4ix۔O"˛ؙW{]3M-EK=G,*ze|3&WO笣q$|/ ~ y4f;s?ޮ>k^gFc_inҿO碰!l#Iw==wUOyGm.EQo)bǙ8fNѝ#hĈ r8=&8v!DʀN"bJ4AW6+[IZ4(=ީlDžj#F8>7Oѽر{( a_NFÐ6ǼA><1q&lv83}oiL(><˻i`Ǚ3[/1Tj ۷V st[7~+[AΎDl @*vwZ 8-g_2%Ӣs7Fb*B1 ϱU\ORȓt"\E'j #3GMK9̑bM[;x׿Ef1y,޽ŏS%5~ƝO'I3owy'SvXsC<.}e hwa!ݺX%帆]2E.˹ 0z /4AIQvw!?҅* Jk].p?AEZlOcXEa?s yvāQ287?{bQLB-}1.Ц-:Ҹ6źiy4iSړK~_Du3x]v?bm{k? (fH8(.= /~@(<> m(R6ļPE_12P h"xΡ;_f7q_O^pfmOHr6*&o3ÐƵ)iOJc5qׁsuf`1mm +0ekO_fd/N.arprNhZx# &RL)^~Ɗ3?v ܯһheF\2h+㚫zTɁyW5 "' @!T@>Cx`r7ɿ?]{>~yȢP&՘(֨3/ /δQp0j:h75;7C-=t$C߬LCڤ:H&\xg^l{$C&bsq8a0eӔ,[K AJԱ("<kB􊨰x7BǺl_&[Z}Tgg.rT$T hſ"1[}`gIa.P\El=yvv9 OJ!ڻJ.yz]3i` ~Lp4q6C[B?5D/z93k V"w(ߘ`}aC3|{}S_8!˴sn2L|/Al3W0}hvvQ()"fY=W9|mJ5=67'e* h;+O?xB6,*Wt &7_\#MBc2 f$@Y?=Ͼr=4^@4 D PǑݧrO w羁Kr0=:?Eb _|l~IiސE"g">ڪ6@9|[ |MEj]aC54"m yI?~˳TJwA;pI3bn[$8&%/lnݧw^+*o? Wf, Q S~C]Y>?U6P~=E}p6whV ^_S!H_b\tLаL i()ɑgwpQ/Dup3'Hr蒷a[phͅxN,yp(%%w}У'v>b{KKQ7,skobCnKmw#KO3JodOMz':i&Nahxl4dt4jNYC1Tq?|嬆qodu3jՄOChCX'ٌܯ_edPՉa@*2zmP$sѼ}ma9]7FN[uw&e]NnҥѦY&ݐ8,\1=r7KF[~aC~2sښF3tU0| IGD,,AΚj+*vNUVҬϦʹ-e Ot>wWVZLY4%y^Ԏ@ tCMǩib[ϑ 2テubt6AfsP<`Eg7U#~.jCۇ4=o"cN.`ϟ~-n!E=? cX>kh*Yצ 0-/[TmN+3WXlT;֞Ӵ-ט;vkW"S^]ǥ\'!W_uu;." -9Ҩ.s!Nt6r>]c|qqf]|Y ơ0#r :څW6k~"T!ij"a.ID[Iit?g5nCۡZPׯQ<87:PHD;<;D#r׮8w Q8|WL7jx6R-4ﳈ\&$MvCs ^~uG? ޫ/RqL%p90E4gF\xd]k!op!a% :]tY"ę}2/ω?[nU+E)T^N-=h򳚷wNu /m_*(Pxi>*!CCaxƿ?pƍob(bD`ˤ8^Lj>\}f-mj'k*u%ծсL s`br`[9wk}26?b=lBʾxߔMMJbj$>{鷾(;lST_i5`Ze;$^?ٓpO 4k;޺so`1ۑsDCuy5TlS*[7Ca ݲM6X샽8=2o濸$woG[ Ϋ8]+gxw+ڤՕr8\CsbW֞?׿W_P|fEk/.Zш'džjtĈЁ|%Σ(8 qkCh\ĆBas8rgS=MD(3hK3 Pq_|4E8أ,=֕dhp`^nGԔK :f5e6;<G+VsǶhD@@vl E6M75y~IF=4?jkvSGSӃɴ]L|&P ]Etg";WGhĈ_cX7,N7ll4i MCg(8ⷛggؓ`/\a{qMQ# LKQDo PF vVW&*.]_!{h|Or6'4A2+xpigl 1jkJq!vUEXk4)]6|9|:]'6o3#6lm8G֙]:r^?nG }h+2 [o@Q/gٸ,U(:βO튒tH$,+&8Wm \LCMt7|E7q X#Hvh{vVqZ|!4iv;O=tPQh_+3+.]]eW~"w;_ukEt, hl"0M7J7фdrl~1 2玕CR^?!bgyaO^.\62Źn?^&1y=3D⬑~tVy_aMjh^Fy`3P,h 7$/#v(o]W hD1܈3{%./;brv=wu!r˫|kW?_U֚f7}NX_r㮌=D=w\ݸu]'M߽N2ż;6 䩄[1{ @ 7 i%嗸u"d]J=TlZ-ciC@EJlk;d{$6ͷN~_%;`͸mi6$ul␧p?6 P5v /%Ӓ!.[H_FY!e:57,Uy6DZx3lS1,EsRw-i禈W#ᅧӍ;i%ܩ1^ųa&n<_Xw 1//߫W ^2o=~e&+y{3`޲璗nAhVp [*L̀+% |F8^rqPMt`VH<~+zefX/!kט_yݧB(ma{f/ CͫU8l֫iu f/}xT#")_1+D9|WorTD$ Fffh~͆`FoE2|Jٗ+ׯ|b4޹=:wm@؊֙=Ǧĭ`A2R ~vV_ bͳ;TÄ6gVKq7Vigʰ:kuS2aYܧ_i5[n⯇[U_M ֏.j?0Z5~ _y'f ~ɕ+Lыl{@N9MUhy"BZ$f4weXoCYYaJu-KŮ,rkpj+wIҖATXتݧ6 (yɭy^[>T5'd7#a ;JLhDvffl?Cy>5y?4f6qZ8YpK=Ml*Q snZFnM`\RnsEƙt967DmI5W 6 ]uܼPݜ7ݥP[Ϧ4su`}qWS.uIn.=Bjy4CW]wPxnvLí F{9Orx7x^St-`b3WMĭspjRm)6?xL:N:z~81{mD֊@nPH{/~O;H5ʲ2I<DцAC(<\zp}Y斊8V: )D勗XxS7YO>I{'& R%C%Or}~OKDmJ+?H=|}R]70Hgv^VAuVѡ/ac~7xdI lj =Vps޳R5nCWg.ӼI:N_jmwtؿh= , 5b#­.}]6k{R᥀o-Wv:2q~eJWZA~BTf{ %yRg~&Sadȃ׺Y{}p^N˚4xDM.;qi654fֲ:('^@0O(u2:)ʹy<_{7 -t޴?RL!ۍ4.a瘒V8>?)w? {{Ƨ}Z[Eh6,!`+qtڸN;Efi!0 SԶ'֫/";Zǒ-5٥x|Jcqyw%C1^fkOT.r_@3wΎDm vԾWlӑgNL+ͷz:>ڰ٫`?8SZVsV b6L-CuqZa].ii6ݳAu8-:g8b9y3"&R=*C2kBU-y)USryp_|>bl>]X;Q8rfVP7Y{RGe[1}jutC~~5i!v!<)4/i uY)Gزa qp }ikZ޵6JQ b^'N ?$Z07/m1R-{%;jե_MT7s | (!znCk~.SްvgBv̓^xWv*9=g%wc?6/r.у!83>=Wlc76 Z.A!dC7{8PS3d2l &lc^;/3MOk+ ٰ}b;_O.#H}9ukWwu۰Y8ܛu_ړbSZ֓W*κ" ׸h\` '/{̜yXkĬr.TO pq_WʿW^az2egN%q]?o/ElmŐ/_M-konU`(l*m۫Q WUڻLҒo 8lSkg)[E1dP:iUQV_ayh6r~k\gR9 lњ毽ku(}g݃;O3gDGΒIFEJf-#eVUZ:0VRE%y#J^)ZҊԲѢ)0q1҆ a#h01ִl9?~ܾ}vO<"%z==%+it(GL37J-D$3Y Ze*U!8P1{vSG)>ZФlX2.yb,onx=O|uH&gv_w7B%*t-KK="+0f nT?".$o\c/`nƿ1=G_%^B}p*䟂#1iuL۩pFB`YK僡'"\)-V4 X c=E+C )9܁@$C i]L'̴;7Yyp.>(Õm3}Q&Ct'hn0!څZQݵ?-X-mY'+UA_zSeCJgN2Zl_mͰ \ADZR *ҡ,w:Za9+yS;\%eypX-4b''1oG#*%S~>BefJUV|W0s-:E4eVAg;|9~ُ?ugAGs亨;i(:Nb5rUS vA0>U^,L۩MBBRT$>Wޅ'rTW߸>EHʇ]YS7pCWV "&\n>!,&P[8qjqB% #۟yw\hd[I&@/1^<upmRl}j8Sc%h%&.Mv>W&D^tkzƭy-BtlK!~FwȫGW.,^-ډqXw$5/CMGw(WGQu|c$~ܒ'D|l VP0-7h_n@0?=CyE J4%WMDS'iWZ8Cɵ OĊGΉ0;w@b7#S{%cbK@h`ּ`_?EлeL4n%p:u[xg'gQV#+,˦gUGZ L: rS}hFLaIaS=hUnU3㊹ٳS&tx׍ٺ;:.SX4^i!03w54ͯ%mp%,b9yB7yCidĺdO^D^W'W ZOru?BZ0\8( qN3䳔[+fJw.:~gq.Xykm6? f#mrV!oݨ*kmN@i{1UKBFK#6NyDtdn1i'ohK_y$xbg>/{v\`4;KaC]7E));h0:}*A J1%z&+ɵb?V l 력\)J=,зOu3UU"5aA=|p W/mǭa ὅ[#~d`7SXCTuqA:@#\#JKF],'Sy8s |V q6\O#;PV%[d}T܆ Z ;펶>gN-o"+u8l@;Y&H>՜hޅRsCH/2 h?4cGP?M#l]k'ºz1SԈyRTАˎם<d/. jb: =hkO7V>"[>f,70%>К8xZl`YDl{jL݋}4' Ǝ"r p@~6ePA}7ِi'Bg3`6^D tXqQ9cv&4..:8H0 Pͨv;ژ .0;m[g? Ŗnȵ]дi6E(Pڸnkk@ B}Nc]s3JUH' -im *[psovȱkX/[w=ÅXSS'Y^9aϽzQxw@NZk7Sݹ^6_FݾvSf͗Nj'cBas݋Oi(#^ٱ^U3 NgbjK@4]`<[hWgT@ [?$rdٮep yz(@!r] !}*Z) eksB?S!^&mQRÎ+4:qc͒|̳#G,JP~9/?'Bʇ|A^ _2ׂ8\H9c!T+/1[=$D'~i.+8DJ/:i'hO8+%)"guqGوTz &3\镓WtHt,{sJ@n9%ayJ?"ŭSjt^k`R`#+!9zB fLpK--koQ0fb!Xtgָ 5 :- !cSS4$J iPC ]4L"^ߛym{[:lysz0GM{As1U7n:&l#>yƑCr׆4Gdxٰuɩ yE ?o!5v.PUs@BPOW6 zCka-/ t*KJA)_OoӎEo31m?g⿺6rPPd]n.(տG6y-D|^Ioz)#pҷtno(Y;&2=cm($eႥ?=tˣ ]iQv$#?(Я~5K !TQ.|a_]W+~šic%Zʣ  8U]⭛r#i{((h[/A\47Rܰ!?Rvsgon)G)y}vwڌ;MܡÙr\gFeKJQɿt EhkG|͉kB / O)--;j'vȣjbl Qc̟|U\6!UIzakq 52.}g8IIղ@t>84C4 U$Am6\o)8+т9 DS} Хb~uv$ %+&\^W压+StYaEʅl×aaHJH{Ŗ-?yqs&1w;-,u?Yg۶g V$՗^榣Gh6M;wq@nLqOP[!8_zʿhυV^>JʁƳ{qLPdz<i ^w> ̾z}c[b -\md(:K]" bu[Vv%XrnPyy؂A)1acㅨ5s9STvlC Y'vtg,M͋`BPcvo+4&j>*j!oLWXrZo (JwX]1 zGJC<=զ<橼tZhʄ*ph(p|ug-%#zbgX ‚K_ͧм} 1T\| = χpAFG[0($fLC>NJ 3gۿ0<8nE1'V8ю 4uG};C/z5fOS[e nX $$@F_b?)Y'88]ǟ|z0ʘHɠ 91X.wb k/ΑmOLh%WZ卽."5낫p=MwVQtجSKky?J$Cu&!"`F|B@c/QJxw?(]>//)|lL7fћ3ޖm@S@E- P*wq~6O/ E[CM,./s~򨂴o-sBI*M^ ץK4n5:._"ٲVRK T[" i]^{?kc*ė]֪ ]@4q}[1fgE Z\-Lܷ12v{Σ w<͓'6Zd{)np3~M7ӾhVZ[/g,FE޼K?˩KY䕙d×(y[ $}uMV.tx@bX?DٟVo4uprtku+,%!MhN~\1+)Qe44/=Civ@-J;xP޵[`tiiXwֲ qu\ęP"<]!-ǎ`[b(ISoo"^jF] =OejM1җk(vf?s_y<% vZ;HY͒!6r؋v\5ٹf;W5'2vD&hfU.@6O'>6ҬPo/"nV;7dzPBL&hY7c`\7Ɖ$ޠM}P9W m̢3qDٰhG+PϿ_͗_,cg$7aDSCLr;;Kkc-q/Lѥ@p!..~ȋ^KưN.<5T  hU7['Z"5Mq&&ʕj۪)5{z޽̳{p3m;u[`\0=2D'[)LM~pdj߼˛o^X~맼!<V5O|nc }FXl*(pq.po4j͜&eC'Sttƶ P&y,qE-(ѯyuK7zA;7faʭkn̖L8Wٗk\*Im)Csf;MZRi|ˬpxg+F+1lh^`W h'-¾o4]Ɓu7g<7"ç.5-Y[%āWB(D^N+0`q73 d !SXm>YhZ!-X7;H\y# ߢ8g9pwP iƻ1q |0$WYuxEL7 C,_ReeY t쵲zyUB8;^oi$?Hkzu:zBy ddgG,O鸫 B6RB:}6A  B@qJ/]Wl ǥdY#V!M7h%V`[9`nj񇘻\`ɉmt;(ZM *bKMV%_yje+'_RHTٺws"EiQMGlX_k,]Xn7m4^أ`Aѱb}q\ S^}_4'ծGيcY34Z!" f7=Cҩ$a+Clض{ :EF3+cK=vg?}*H2dKZK+ݗ_=5rwhc<)YO@9mi~>r—EGv ɉ2DenTjF{^p1q%|\DQ@y=Y _K Q  0@Yaj٪@*ʪg7 v Kj$Dw ذ}m8\ L`#gyb?d#0wtSaCrT~ZwSu^eóO?d?PP$ ;oa݇+%ntNM+|c͌*2JxYm۶S4'Jb۾Q0llf)V>+kx1Q^M_!'AL]9Kt׶dM|/~}m%!bDd0N8Oj8`g/ԏ} ؓocLd>vcpqѰ/n<w-nKyuq䇯9; !t+poݑ($8|nFm,YJP^8Mv] cRmɇ0;%_pʁW{׈(R(0= \ظT^ShbIy]޵򇷠E]v6^ xYyZ LC+)Վq7S_K!l\Z&*e ؃/?8j}IKxFފzk4I ԛ?ZB7"6bPyW"a뽚yaN}+%LfѲDjss9)N 4lk7']H=h+LK遻C}DI"gJ?G4*p߼X ,QH?0ٿtWZl䗍S~X<O8rŠj}G$`u ,1!8uTIU7؎ VVP|-[#\J.˦shiYpayv92q1h/B ?$gcg0VY]#oG6X-[|1[Vy9yV'EvGH(0s- /cq( 4A3Va$:3/͞V@ cT?P<.a:s[> wسNp2"7$@߻G>g>ZV09*҉.\ڣk=M689aOXd-K$xEa>`A]R6x^,-or;b-7ji.p.|css*UnN'b%^B?B3)M T-;&":4 h‚jM^=LTرm+[̇?ڦ5l`t _krHcԱx`y#niNjR,p: |f{4޿ЙkX0X>䐹MGRTwNNJV䒚ߢn2ItC?9i`{mC0ך' ң M(gKc-&ū]ņCyO,"\&Z3LܕBhi rS|x {G mb;L;!ZN5|Ӽ^>*~{R+y~tVfmep=o9s$ ,}8 /R/k2K!Eˋq%./] Ѳ>6n ̄0W8r˯`[_~}wbG֝? IDATY$Z1{0t2\(1PPz~x+ 4fIA1/^="兣1ڄK_ R^4u\ t-:הAe4訿NPϾܓ_y.K+WjVFט=~o~8_1n.yܹsFk->×Z jRkM O#'Qk,G2V)q5j0(98٫rIjk} Lž]jCtXJ5ƨuW Hf% O? zZ@/AtqQ.ݷ=(J"e!QКC7ְns4WB/p()9MyRZfN;b{'%QMD σ5M7 igzA f6xt8o/"]RzN2~m_)9οxߞ8P:lQ ^KaEz\Me~> Fty]I8IL-t^N셁[(!&>gI̬ʲl5[PNQ>"VMg/)ݷ ':uT*59$qB"j>.wi0XN?=}0l/.9b@b%d1nX0"|l,P{QwOl/r8C*5G½l6TP^8.h%B4[B׹/qS|sMft-V|þ_׿ooėgߤYۜ3*Ɠ8r.Ίj\X|g#G{Zsf޺p!e6|WZfFMƅGksT#5kkr1l.F=.LQRc;;;Mh5[IkaTto/7Qevaahr/;=Ù)ĎXT´E,Otr69Yӊ{[Wq f_==]E|pX-N%~D[O$q $[{ȋ#ȆO/:qv~XHKA4#.:Qmq`:Y<âQ 3V~uM !t8'zX {hMNrl "! ALÓ${27{lrݼ|ߩԺ4"@8"U$=9N 'OVcxDw=:7s >H  `Jõ?>Ͻ7&oClu]Q˟3viAsPy.Hk;UZsܸzGjhS:qΙSOPܷi2 {sFWڋ|BM ? 9U$BLUmpӈ='n~{c?!j&(fmHD"v[+s^&g__yP^|wE11%Bތj h'NAbSQlb sc~zՊf ЊUJ 7`xֶx|}͑G]Nyw @Bd{V Tkrn-tN`FP;?W3<[|cs2d.+}^Cv=kQ>UY2H?~3/5Cene [JLZnq&jwOB$ v*/r>5bE\pS4|/ [^;=.h'a, 6~? Ms`• a[VBUE3l1XCl51sҳZNgډR"Qy$5<-\4˸RI`W*Kqc* !P"!i"'!vB4AwG%} *rQ`+Ҧf`b2ǫk׈18NlM&&FB9{|"$؃/:k6JRxs {o?r'=:8u zC]l\WWQte>|婩d)kN007R`ƫ9'8U%><4| ( w<*)mVԟ?5a!oξ I!c[8wN\oT&x[ÂE4zV} ad5D0JKՙW*(ɜ"(_(]8ʋfuދms ZB$6*<'ٷi} 8@%t6܉ NNhNE^eKQm|E엾&!By!>oT0`W?d >9Bؕ[/Q~o/ov=ŇRkH5e+G0CgE-^׺,Qʛ6 ]3iW E͏+q~I2ޏ MQ!lQspp5ú;62~z'oDkCepkJ&ss^k:9 ʵߨIڧfߤ5z`shoʮтA;-%]~yӸKSo߯zX\r,ã g{)Rbj ftK:}x/D VmGv@mƼ ڍ.y hÓ_C(]A#ֲ5X+0] h㜄敜nPb8k~ejݚ/'An 呍/Ay jG{wP޵ rw;rE˅CJ^UUҬp뉓x *] đ¥n>X-q$vbihZK;(S-Z;9'忴 fq*^BKEQk ȽAO|;oO/񹩭V8:[u=UmC<*?Q8Lܼ8ɫy 6AGʡX4jsbOE\{(9SUTR#Eh$:2Z(v1f5Nf=x6qW#`9aH@&}v0$(f ',;:x!f"BBXciHQ,Ԕr}޷ުn~1\u{ޟϯKV`Zaݛ"7kP(f! ?J[L]30=|)+t-ͺ3E>cfPU\סtshϓwvq˯?q PuK}y=?wrCK*l_;&Gʉj)jME%S9x8?&rBӟK@BDDIB%}NӠrB\H@H' :(8%)x(H\ J1d{V F[TV=s4fPLگtD&c\BsBO:ޗpB-{~Ms! biQ];i?-B32;w*O%٩̹RZFN|N֪&|Α9- *QYsc >Q d`2tN $r|n A'.Y!y06akyu?B*+5OSD%NIiEȿ1oYN% ̆EsٔdJM_2"Z+D=W[ /!'ΜsR6O6і[Nw ♠kᰈT&R?^#Q95U@b,@+kE(oFax`E>j4bL8Mbrx--<9#!%OѸql͈9N(xEE}^u~d {f($pe?Et:׎{?"eэ\2g_:#H5%x=:C- *8yUa_hp3f#GBh1}CcbhzIh%\8fU3uCv^"{B_ hX^6C1&ɭ%sOK>>{<5#S3Ս4t@ۥFF}n(ʂXEN[]vK :@xRa,.O(6}bS B~27Yt7z0K]es [ɣ?=,"1_ 7_1BNcVp{&/K3m=L#T_C0Nhkoo8R%nfx!jR7`-]fNoh\ze3v}B_و. Jwly##x-T1#}L]"gƙ:܏){jk6kǁՊ7֌rA7.;U+bgu@`k1$Z`믯1;vb^!e҈:Yzu3X]E5[&ۗNWeD<^A#Y y] }c8Nj;{P2F_-d ۘ8fPZط)%UDp\E$0M/>iъ_Oc_J~})=z?q 9V; @1훰kV ֍YJ:LEnlT5p<>3UB4d֎dljcC %ED5ȗ~]+ qWYK ij4h'0XjG/~E+eqxĕGӗB@cl dq%#?h(~n)[ꬵ=Pފ(ݱIOlj\3vQfBnByCl]M δc_} @Ds|\&Aw0Di2>{f*ո㞛;J^WxGlQN#gX(J#RYTv$)9QZM/f ,d^@ơw=E!Nqq5 >p( ǩ=̳߆48/R"251n|`54I*t\e>K^VYwC޵˦v~Az Kd-(m=9/N<.x% \u9=8 xX@O'~Dn͛ɗ;`'H8T^g)X $iM!Y.!xm!ƇZ$ ōѥC'Q@#!!zwͯP-u 89y߽2ZSo2\XRz-k[=ϴ[QQr_6Pw}nlAxvZ 8Fw9I ; w'|=o6O]),~2% * Le H.a,ci2(Uf{AqN;9r:`$alOXZz7k2,v "r9?|sf`l KXBtK$H֮d?!z:/'aTXv"DS 0)ia-?! uA<9-:ne4}6//lu!v7T2^'>KBk53"HuPNoZW`|d1S>OAjQQ#1Xd'P!s9 ډ1Fz3d!9.x0>ޛ;6<}6զ^X߁) w­% `h/|D;.^] +AlG[+IateOMށ-Ua[/Mp* kQGoT̳Xk\*/S-sM /}3K8n ). kIp}(3|P˵#@''XV$78@"i,+28?qZug!w^I_ۈ{g$?=À՜ +^ZMt 1Oꑽ7\K BpWp/I Z]u K,eACVCkHB)rWɟ >y}z3˳=}3^eSĀU(0KIs=u}Pt*gv'^yW\NS^T9G5:\:ZHx_O_!_fsZg^㙳/쫯5\B+r(/uqiO۳+?MQk $nĀꛢX̢Ԏ|7Ł jB>g?]5_:cAuW~#$dg e0w0WxNA3"=-׃_ g /w{ AzOE[3+"Q2;"c 8 ko ^Q'ҨsחS2а8,9vhM)랁G8W{iD.$Vy$ K<6r>:=v :jZ;z&!efL\ ~o8ar(,Wx4 S튴j)ITF-pDl~bT6KaZ 2Ewd[D1K^KDvy1^s^hJ!pzut% 0ݏӼS9PrJ"''Ds>N;%,A'|jw?D;Ğ '^m EuKo('0p2EM}J'[2r: B9T_-P".X>z3'Eqy> ވ+pxHHѱk x*'?#G;C #dk-CwE}tOSc+NBv†z 2 4)hJz탌bdefRߦ01]TrBT'URF,+nm-;]P[Ѷa1a~p8b589{ʎǨ9ϱWXk=5[D<Ǘ'Nc\\Ȟ]cCٽ,_۸t?@x۴bu<-D?{+LL³EaM:qx^-},i@t NSǩn޼pP{9 ϔ bP6F;!.M b-R5} +}53Et; y;O\-]W& cLN3lZZp(tv&Ӡt'7z h,Ƒ6"\q6fJn6\ l "%hoM,΁kK7^7邡ҝ\򡕑ʢFV6]B9]b$:dtxO1[ː=ښW^1hI~n޷8}g&iP_?Om(.eH󎺆OPk%hwLOi|ik,˛lX>ΉM>8 ut9".7?D0L[8]L?]>zj N1@pɺ0N(/sGڙ>ysܹs2х^.ȋwa˸Za81NIK]vAᐽЬAp^B>g?(lXO}{|s[d-R&3FK40G@s12wURkꥌ׫FR#}6s}on佼L@Ri(K&# 8QWrˁ=޾C?322&QiU4% 4728dudjqK%Ɩ<3҄HxɁfG8;9JnF2NU>uw`ǜ.mۨ6Ϧ^vt`"ufjO-`]32ub=bа{,YŕHVl.ȁI@q0 $Ʊ}tZ631"cm4Lw–boww(Q%GdCQ+yY͈BR*lduiv'?CX >p<}0h LkBW3m5kh^+"sB}O tB_AԎdўsb9_(lKb]j b{nt](F:0-(IyZTy9CwE>޶YDV0[u׎ewR{ZH4"^FNV+g9|qp˻Q;#Qkdk;nRRi-kW6v #+˔ 3tD,h̐o9ѳ}8,Tzi{/o-nZ^tɓlSvࡷȵb' b=cggOMSku4({?)[V*#َg\Lpljc-c9 1<;S1\@#6S,CpXy%sQ#c ]?D 㠐ziV[%E ӭ@q bAv̺E۽_]$E Yt Xt/ʇT;FD!|a//V]._O :qM/N,ROx"C;bhT|"157bu~t]С+O+L=<;K H Hc.Yl(KS-z]~/mw"W9%oZ-vAr+zÂUQƃ(>0*WZ q[ކ(FT2 j9X/XhtW}toW^I_fԂNalY1U\ MzP/vԮj}mXS?@cul*L= ߗ)Tѧ9*(s7OR͋K?,^2p.q|6eσ<=uCq@$*˶%%~'Qn߈_"_^6!B,в_& */ᇸvkL]D:*>Bʂ69pZ d|u: [V*.ϞQEJ`xe:ch=ӧ xF"[8jdg|h_o\$ӑ$ӳ=v~\W|>L GSϟO=V;-Ңl!eI'"XC^eڏGd}e(5#^bRr^*,&Cݝv|)*V{}?3ɣsdJXYcp!fυ~Jo;ϟ,ڟ ;ބ1 !6&ԧ%ɗZ"Mz}qbhz)J2 AS'XyՇ5sE(<(EE֞O,r)a.@nj;Ⴘzm;5j=@ ^{l'h<3`P̴A.zI)F֖噝J҈(ɢzaF<zgł)FRG㲼j7:{M|pdY#ANTx{0V v8F#ز,I#Y# wfZ?ev~rX#m_a>Ff<Q :!R^`z`E]VFO%֞ a6BVj{w֎8CѼЫ Ct~#bks;/Ԍ$Dc58q&\h4ds6D06XkO1̓(\ao9ñS>ate+n N֞E P^[ 3 =sRm5CTP NU)ѽ5LŠQZ|tBdR|Z0KW-RbmuXog6{գְ=5⢉$k{y$QYz:Mi|/ -16 H> U&90'0چ(AP9A=M78^VZ9oH~^R''3vq/;41BtkKUGz->(Ct7VR4v1t`<41vX\v8'){*==1=%u7|y#HJ˻3BYX'!7ÓE'˼4cm )jɰ˓ǃ,0GNg6b|5 gxs1F9֊mOTkmқֺ+vS#.Wbhp 2{36-_R D H|DWp-{OF:\{!ޛMcC5B]R穹iN? cL|1*%ܷ%O;4biJ[ w2!ehog{ꖯt^V(_<7b ouG6r-{=O6,éW9k9phӬ)_7 !/=4Jah=q/t.g-Obw|3ˈ^OĈEʥs/MLckoB@Km|U{H*vL4cҢ5Xʰ@wo=wGf&x5lCqxٮC{Qe+ AZ.AJK lnO"69YPV6MS|v;[1l% 0h7E;b^ɨ)w*8D%ҔW~ѧ#7#U^B {p2۶5DvyFL0SiI*R7sstq42 !WFsO92s[0b@/:_ŦE~˻ px4V{o$ v8ՆR%OׅB$pW:%ZW bJt(0Dc/s:1Q0QKz}L.JAv 7V-u Q~/k( ǿwA9|]RPH'QA5[egwX5|yxxr.d1ƖgomO'(B# IDATĖhd8f${?8kb><&;!c/FٗҞ윎Q+CSH?Fc%J~YbNJ~ _'cA{g /Sq"t#B?yz9/! 7Mvp (K+) Ҋia"aƮ.gls0= wuȳvI~\w{~[6D?[N{id!۬]3deA٤58'dcC`ճLe}UK^9p=ˊ7ܽO{*>{V*cR"F9qϤAk="{aqa5:Kc~ύ&3"ghi3XKNʯ/Qć0nyJ1dsVvOuFݦa4dAYD6gW'Rcs{xUN|,` ֭6lH >-:](ޫ兑/F2}EQe*EAC$S nѳ^AE3p6cVba]'DWG˓KPڳ:{lXkWo iӻ֮i &lX" }(Te~h(A:Ut]-֗ 읩b˿.? .Q>ۦEBg56k{R_aps^hͣ4&륮EDZ$B>D[gއXG*T L܏9\0΢8c8O]Lᗘ0(ZC݈0l0Mbc09 'WKZmAzp Q'[<Μ/vUz*4֧A!(蠻`+,t^L5.D5R NV?GƆ9ٗid:+G;Sdv<|ˍt`FzRN;Ə";Kk6G)ضvs 0 *[ˌ[o(M>g^G-{gǮ QoQ푬׈H82oA5?m/,͏,Tp`ڰacGGz-չ^9H7 ق o͘{7|۶G1W"Ua5nff:F70=9%?U/OTE (oRY}ݔ޶IӼ1sF95Ȣp$rӨDg?dxܗ̈́gi #vlqlS;R'ı?F!hje[w^am%La$݂eJ#)cӕ 邬 f='l릮]K좑^9\c:R dӆi2Gi9pA`89qGi$ZM6Hy7 V]cP>Jv.ۏ;!#qvN#k0]kWT8ͤ MEC(UkVC[B̓QqC5]נ̌a|NvxD{YF6L5U@a2]3 V2 ܊^!I4ˁO3/Q?xdyB)l-wC0^ ЎPxWt=B>6W?~"nċB;.@ao]Jw~8NvQؘLCnSP<8~UUpDbT8n} {n9|^"]axJ'(?Qƣ8 c;' ?REPi< ,v%M(qWFJlڙV,]hYtˋQŦ#Ɗ\y6Њxm(ѵ>5scB>R!ҐKvt5۶/Զw6 aCJi XT'P g(+ hWeaʛnF2.IYI4c++-띶 "8޳bo7U 1w jȚph8}*d!i2-FV30 CX׀x~V,  ʒA/yQ8Y[Klb`|L˸3 XD13cx`Cpw@6s, )"Wx-<:l n蹲izઽxb] r=D+?PM6zĥ/>)Iٙk0Hi(2c _o`n\ᒢb?4}w(CsJtR |3RzMY&mak@P btA8GW!xF"/篷,M%52{,=$:l//>JܡT! h^R) Yg9^Jcx*L Ž0W}b¨3[$R`x v+@w<;;\x a}D2+ZH%U='#z/ՉA|waw.?Eλ:j<7|1S5Dqז%ƑJ'WzZ݌}rxCv֝v~n;۟TNҲ. <XF5VAu'de_ ޺[žnDĈj@(a8@!nAmZ|dl]6)b_MHU B/2=b ]2Ew]q}X-ẍ́}=nWwc0ў>0ܓ^FS3P`_e/D6so(X@y5wlIhN\~vB.nBwIs]'] ńUik! wE>I'=fͥkuOY/ )C5" W x p{S\S];%|F.N*DTO)O1dBT唁[P3)+qit5 RdS>03Ėy+ƶź<âR8]U玲A}LYѮW7@Ε?b!YE%&#o6`TiYCo]vZ$1pߴ 7W;ZNbhd=Qeߴ|jP5:2DWV+j$գ!:DsV1mv`t/c bM{BbH{U&WQYI}b.Jk1z'a֗R]F)ELʋ{I[^O=B轕טQw"%c{Nlz:o;^h LJi9)#ӫ(M<Ռ@]i_x2H E&EEqԨǾ޺.fO$fOY 05,}F"hZ>?8-%Q>eERTjymKe[ D{@6-$oID۟vs|~o3c'xSٺY^"TJ 'q8ź ǣXi;cB] FW;%9 -eV|3Ƞha,4-jIKm0Jq34ͥ`#!zi%AԼBv} i NUE hIo1eb:N |cc0^y3mq5|ƛ[_nF֠p֊̌CT1N{sڈD͞Ԉqu6e'd5oDz6펋Nswsz(8q SS8@+#< y~Iәhqɢ1q0^Q3ֻYjVS3[+XF,o1ʼ1*>_iK|Xp80כҘHKr =_L+ү]e da֟EqRpĝU_UZ<N;qGfYf] Fa@KYTO>W*۹-ٵ4k7x@ WGlފulO18x {(a3kRAal%x5 SxSCЫb8 ջncò9Z;)7ՠJ_7׸i2$: Ci27U^5'^ƋQHޤ巷!x Nt+x+ݗ}/h۪E?9y.ŇCpMݓW'Ry3qx<1脨 XYVC >pO*HM{xGںOq)'I4OL5ݿ9)*Gl PC%9+p󡬘H% C9y@+p3(51p믑LL $IxVjw IN|@..'Nr4<ë~s*Gqu:vo쥎$40|F X$o_UBY"{s۸f䗢5poe\z2:1I ńF-Gc4aשYo_È͡"УaS^oݭ=S0nQ$4҆$Gf{ռ#i+|y 9ť8!ԩץmRyv/5o1?m:,Vqn2E!^L8LLe_ZAaR7Cr9pD&N<|Bb$א)r $g}\Gy=U}zzzfԣHcY-KK- aʼnkb xM%C%wo~v$oHq'$@cE~g,![~39]UTӧG=l>9]N/*]DlNU@rϑ16:#up7ʱ#:#"cyWj)W<@{O`耊l]6:kǜv(\`Z?.\T,sqq1,bmX`*4!>*q:Kg_X_^geeZKсq?Jh/{r ydۦ)XF)P4yQht|sT)gu O̗100>FITgB_’Ar~rNhnuF+nbI_)B[(@{#tPb 0Hm`U#AQqdZP:g>^Ԇ(Y5]hRO$Kb*vj15?HDr)q64rch62_R8-ڡ4di,Y#.U"IclナpJ 1ͫb0 dCN| V]_ݿG-h-Pmrb0Z UoO-UО7ivmq Fζ#kǡ@l4Xdi12ZBDldh%UKY,Ksqb%;"}|=g7韦swu]V Hdo:Nf}WKE9YpWs2d7~+u'QrL /R IDAT 4/ Mw:d qQ=RAF/YLHd毌D)55/o|ٿߌ}u'vx_2@Ф T@=pچ$2(^jYr N{0.áj"WcvA&>Nc`owa'V3鞅dxB! )t-oSq8׉8az|'?rE,JÚ<%wEヿ'KkCDgr=5ɇk;f*O3ٹ>\p8t0՛g%9vXz5f uJ!REPzް&,Ds9Cƛ`|,aj0 AOeCa2.!@{)\&+;mBI 7ShrF[E-9pһN13䍦U[DZJ"fB,_ƆMgs ͒ y_R54E( ~4ܥi'm;B/E|sˇkwitoI0~2sk?$E+@cr8j=q',]J@@y(s>Ǿs'aDyMPM||{已Α$44' S2+.PP$Q~O@0i9 # ^u%< Z @NR ʩ7ߌ96-i#sBMP1ۛ UMSKЙM^D _on1rYHLDC{X3!T"Xtؒb\",ԠQF}z`/m04V|KYƼ*Fk5=0n2{Fîɦ bTj! C|rmBS#m}嵲CI>:& ÐuKa~r*Λ -%"n Y<N>iYr%4|^ZIV'cwl-P48=O-2-C=!_ ΛPVvse#Y$ZKQղ{q{k凓O=bn;7\M܉F Z5G=.gGLC$_B_}'nG 9VZwYzԱp׽O ($I4CO#pձ,sד; hDgK.@a'qҳfC7z倜 tM;!oL(Mh:9pڑs(S؜AqN~(2^It  )&͔2:YMQ8%v0jrZf+?~t =%f]]KBR;40Xn!8Sjr]W.^:J07;9>#Xn-wQȖ#misv\xd,:$95˗u@;tErۻC/4z"ƎP=xR쟝5+wpvK7dUgou6- u Hb۸rͪiI H;\oG.) Q0Z8Ś|>l.]!SxmBoofMGA&fKy ?LE;ԡe7Іh5DҿҾ׿sAo :l"Y\ ;V;f{q5}̬4sf.꣐2AsuT%plrtѡᇝڗUv~O|R4E/CHϐIFԄ#IS=ҥ3”$ٜ0qhQOj;%q n3pu y'EA\Qu!:i', aPJAv؜#oW򹿻7maѪb|=7 bf u sKcε XI]zE c򹀷Q#M8@ʌ[;b'a`p"lD9;ǛZ=N24y޾be0ڦ (4oX:TЩz01s 3|Gx .XRfk*B˜Vbz3H]Gkϣ?@-'Hzf L?&d Vtг?Hx9’xFQQXoޏӏ-bўSs >LNP"A'BFj9Gif,gɐ0s#Æ]av:eM~T#v7]cGpW7$3&{d#9 nVyqS@A8" αQaat\;ѿfXZ!@>|w5\'>Ὶ[gc_5y[@YlkM,x|%85(PMx?[x}9#btO*l2L0RؼsC{yX"T.v>ggmj-sƔv"r^RHRMKzB/}+ zNDVwɷxpq%r(o1Np8{B-]¦E+ i^8Ow1eA8 t̉w'v(bsFByУpsP;,[\>Cwee|Crȕװ)Oϴtբ[{(9 .^#Qˮ#;Tf]Їӊz>9ª8,<$  KZlŠ6yL-SM2xI sW2{ VP5߻W_*8Z ̟W ,\(Do:wqc~,z|?(G L -sVz1D5# OgڲEXlz(^p!u=`sbDZf?,r`4:xГma螎/:|{⓫!#o߷\|* %N$0] G 34@-R'}͊KV-j@lt0 7va@"ETqidrtizɌp \l}2k> bZujLK^t@lYYy1eYϏ?o{</auGXWzqfV=^7wo\AVA='!cߦ3^B u)'N%]WԵwON,Oxr<V/ /%hHfQ;Yf)T_]*l`q{H]V1`aE>L%e~±O}ܸEj@g_,%cIB7N 3Ҏ@\ NN/ Y롼FU'3c-Z{Qݹ=.VCJﻖ9ĸUyYw0L!_47ɱ}(HKY;qȊ%E'kuY0ȱ^lnW׀#w0?kO^HfYZa?j._e]qɘ'Gܰc '$ȶp:eQ v-;N lEl%l/Ƀ@hyѧ6{$B%˗ŶR:1smccs$OO˻Z2n__F '%Bch폫aBiXVL}Q9 c20ĹrѾq4P,e֝v"6k>iʝr:wțW{HE:aDޟZ,7Pmm,CC**T0YZ*Ba@dd]BH#;BK4G?#9nXk!;:Sى GV,usB1 zh4ۧ' C"3]j#/d H"@.u!vW?(_D9;8]6CGv;D] AR~ Ni=jJ h8J~HWΰ_ZK>=·+Al8"t8Gs𙍶M-PI9@)^B.i)0pѧ<59ɶ-qOma)/ -@_Lʱ*?ezkˋyߚu/z-l6Pˉ"E^U%tnT{-yx^*sӕ;80q,-_td=orO~~ c;"7`5Tn/F2/*; |IZىoS>W ,T(B+4Cu oSg~fe0WQ޺ >a8L2zMO#>M♅6j-_ܼ1{{NX; Egf|V`nhlǀ2(Fas C+?wudN jͱ0+~LјXbY({q9i_ղ}9)XtkЯkio:47g-^(q#>?dm|䢍gLm79S E@_oAOY\b!H#}??Mk5q 1EЊ(qn^rO ɲ$.gVЍm^SDPKiY꯬L&ҧ?h{h͋) aCȆNqdD7+7P9*q: M,iYhqm'^/7 EH EMWDg"wNi%+G(\{ h =qXm,~ϡiG5gyC7PJGa|8]a]:No:`Ђ`lM 7>u7^ۯ͋Z\j$P?TCb^gv<} \ZݿG;"|tK/[P!Uix$SG:~)0ft.Te"3)|lZˇy9_vJCAz+pM_c4oh S=T=c' Zn~>+v:zuzlhh^ڜFvH章әZjCE3̢0V$sv_CyvB#( o<^t$-%OHfd.cNщ#0H#4n].d6VpaBGrG.6tJ ;/.ڲ"s%EҞ3-NW,שNſ|zoY7[d.yWVF5y޼|/^I>{wo;2{8E]/ipZ@,/_Oo?̭|槂iټZ^[">^ P#993|{}*Z3@oy]Y>'y}rC*ߋo8cY5mC)B{~v|> ,Rs8%g+?<H<_)L?<6dk\SsF5Nkog>t 8.GmA ^4`&&s/.ГHQ[C'7`Z*35yto4c0?< u)jG'#6|]zVK k5'߽0)}SLD^-gK& ߙociwAdԢ)jBYٰLC(ZdXVzX1<C˖SG)LT8<Q |~c>:EZc$J/EH~-gcj0itc]__ZO|ii/{;%r{A}YOoh"Y}F[^i,:b5UǞgC?+zʩ%tY | >52-BPR S7ʴ crMo"M>o|yOR=\'w+ES#m^wBRe|,Y;9 *16E/2ZhsU/?lQ;:Dt8Σ=:m l&mP tJVo Umx|2vhdRg\1r_r_pϓ ͝e; ^6-kƥyt0w׭S>ݶDM O ۳`('\%S n:tCry5_.F54!nb^l\YfKh4_5khlQ0b3Q0. %?Q4lTRj~8SɚOBxE(.ѮӔ: B"vr8[@L ߣ>sU%@ jEɉ*㲿NuLT%g.M|Nvt Ӊp֐F`Yvt:(stߓAY& \Wrd Zs WV 2l%l%o^~??ymq1sV%U" TVux&Yι"Fs"={~e =U|Xdu1=\0P"U9s fSOSσ_ٺ|j{ ?@,f)pdio~{l~. rmv\䘋.>Zkgku"?}1oe"Yh41]uN)sZ:zEhke 9pVSW$휃J'=C3SA2O"A.\wU͜UGkwd ک?gY)YzQ; s6ӎ4p IDATXVlco/i%m|\ˮ4SiA*NiQ/!m-NnSl>w*uk\^ JtӸcpͪլj8ȷEp/6[+VQ'[@h Kr A&o!ns`FiCh\R1`_ԟ39\΀;.D0RTJqJl(r,NgP9jA&>r*e K(jVN<{Eu-3܁߹Mav"W R>8dFEU(-%20bR5\l$x9^3P_T08D8\; exR|sPN eMӱDpMgChvSm; I0nSœ8MJ76!oRwetij;"$bekQ)pY>d+=ZU~PĚ0N;x+xƎ0k5>>cgev"z'qGYr̗7~Ɖ.H;97 M~Iű<4G@$KZP:)12av|^ϫPߧ~Yir%K8+ׇ $]]v|n'Y yKiDׁCEH+oڴ6p8-N>^EXC`k%x6>cvK NgÑZ[|oڷ=$;/ 9*O. r߈>giC4(~VF;\Ρs*==xcػ$Z-yo>xU(l/,ėsB g!r# Y+ݱ5 s>ubH0XSh>W^k&RPjv=H^!EƗ^!7p0_ғB6r0ʒw8Gtۿ1QrXSLY佽vF::h.& 9tzxr:16Sα#,)Urh~NiPk;G'ޡ GNiz$y4Rc߻|Y\B-KҙjIH>w:S/o^q9E=^,tvgp@~,?9\v4wV6Ph q^Y/pl]g6$<9dmSgMo\U4@rPzbδZQ0pILO)'0ϓK+D 4vN#F]9Z\2hw5!m) Qos֓<*hKkd\CE ,B;͜6)~EQ 8mV.p ;Q K˺'6"8X3pc38@;˱X& WZl@%=x:P8ۈ[֋=cccs(vijf\;۠J%Ew?2:t:BQ") ʉ;P.F;+|*U@NpFVckP=2MhNTQ7EN*UEΊiN.'3NUzɅ}h%R}Yj6 Hhu#r\1>xNGkgp)M{;tвN9윺sM۠N8+5߷,ebq gبkAםо|u*pH|ukB6Uso1/)sHC0y24-edRYW*5K!~S==? |ГDֻ C[czFϺyYg-糷~XI~N/B54hC=phdK !4dps:A_zh/cUcy g@ |DOֿGV>RlB7d)uhhg}(ӿcu-e( ?c^942f`ft ǯ EXP-3KBl'( *d)8\@amR=ET. ^M EYE l hj&>43߼豽D/g:K-VЙp@nG]hTis6#Ma$ߧw:̩wRoe5TҸ8&FhBkC.,@wA ws!rlC9=l+Q^6ŇCoh۴LK{c[b\{$hu0$;l:@NƂiD6 5)Վ;'чy(>#Tb _9v AVcmV$#_?O('Z) \y{L0.R e.uL~nS4%=No]IU(" UEm3>Њ9 {lEZe :nqugZU>zaE(7F4ɠ׶GNbk^$hPU(æ|0ջwa!Zꨰ/S7Cg,Z)lƞeG})68vŧvBV9xӿ4Ü^=AtyoE7auZ&< kjG)T@|c6#cu\Nq5_$ #c, o1N;@|K4#@ty'OL@b(vNO+=4xlpӃ|~\C I Dicj!& i-O@L'a.?K?EQZN|rphl4V~9qf$>]*ACg5f&{xdĿ}<]=CqM$H~ˆo}Igp޼ 6BE{U.7Ɔ+.¼,c$:(|r_)Bi_*h atcVl(QhV݌?! ?ۼbp6z.چ<ĭ;s=!)k*V2/ERIR ‰X`g7G*CU` _].!3rzXSJqS90.> w:I$}OchNȊ=|VW.i/k'x 5} o@+6ZF-{q;$&o9[ ;|+~]ndvELhi(IJo3; oUB6]-BO:NOrzӓ\V,cTvU,d}$t ;Y/MB$6&]`[ 'U0|/i4˗6P@_ ܷu&0`EYE!W]!5A·~L5 _hK+u1ŷ_#hͺDJS:#M6"a2Mh!Ri9!ZE`n ?]\o'oǽOTQ~dP':Gs((^!I9;6nsqt?ԅ0( Tjw^zAK MWweca6)D`ݶ4B[ݸEG4~B[Z$$ߧ D/䤜&~'=>Iɓ'Ǭ=![6w֮1x) }{0##h:Xtp!r] {j(yG6Q v*؅tefn+S:gV#9&5B]jDAm ~+Goh,,I**ҰoR0DXj eʀ2hwV!5DbD DOtw}Zݔ) =Ý :\}[/kt V㵈_=8ȼ69`,\wh- C`PyBhT6#c*/U喉% .߾ێ)Z"HQ'%56+|8qx4J;bPN._W*gㄒ“NA{ >$PiEqf0P?LZKfh*QL|3HƾUN]7ũ'4Dhl9CE g.[RBeXBD82ɊO~;6C^hٸl0xu @E^/_=W&sJluGM;wVa蔗NӪ۲vSӷ|v rh!b]?[Eiʅo{ Frqk(4s!|iV>`" Om!X}~)rҞ}B^hUZT}g/^6B5ZϊmI7)X_ʗ6oP&q|c: NMU D@0T|Y"%$ e`4TnXV5#.d:_`>^y Y+<a` 4@,VVD:VP@4DM۾&_7l }0~*]V+TiDa斻 ԰[] G?84 р5D¼z%ͷZ)ۢ2l(؎$?#*`#(#?~ Nm01[hz^G5Y%ZQE]p o \7BdMwV=)uC+U>'rv'wIw -k7蜏Ny9Q>': 4Ӑ#YRPVS)Bq[Ck<u| 2Go.td9Y_Vٛ)(+Kg[y_˂I#KV))åcp'$vOYq&!Ѣa?qλxc,)hp Eh#'ݯx߄ݝ w%e5սcDO F/nLUцaNHqCa^m2% o_8߼H_~EJf}!}L|3cFYUY=Ԝ%4TBDb>j0Z 3)^%5"Qf]@Y(^ U)l,yUh|[q@$V&B6<[LvN:hm]r z3#H"tӇt>|^~ ɣ/|ߴ󋷻khi`?C R) KE@Nx"t_]Dw TvᅝPPn4G}%t܍]h}pddɂ!Ve 5Bs$m-U K~L &eR+ko 4Ѧn%;ai +6Q0hC *+Q(XMO=q̊P֔damY.|I;QW^ϔh9JQ.*[׷]"PYՑWhc)rUP6۝EF'"f*ndžV 7? D*V}Z%jKeġ߼oŎ? F@*bB#ZSAJ\6^}ҵ]g \jCYf8c;0Q(̫,քb5TB(c+T㫤 ^Y8q41NNo~lj:Ӊ% GA EF аT?e|-L?"Fgh{1?] f4=q*@tͩ DDh"iHW@_ҿOTVIWoB#aV}b_cӒ*H#{hc&%[hdB*ڑ_ri,iv*㞉*37؇?v߇DY%N˜.~' -ZHɀ`H?JA;dߊ22}0dyR3'&ab/f/',w'm',$nH~(K#zoۣm7_X0:A7O:bx)&ʷY(R=-Kki[#h~^&[|zSSbOD9Hҽ:?®8w@04@4N^ϫ0јdd|}(c[ŬwK=-V @dcWi:ͯPd3}MCQV0LGpo&ztjY-B)9(!B nZa%B#kx;)]P$pC3SdUlL1E?ݺ༿npj֏#|'=1MNjT M+: *_#kJ8Seid`|VZH Ih|VXH:p:d8aUCDFhnG'*I8 Ȯ_B=Y!Z1Ʊv1oLh[Chi/Z女xPV`aMaiD;8QZ'z ;tr2uYqtW2\9{ e9@b"ڻ]Ӆń͓ڑcFVX{ʎ>u[w] b 9KKf̩(1$yaHHbى/E"WlZ+-W^GX\h5!q H,f^ܺS{} @s_w_'PcɅ  (Q'E. @yhY9itzl_1a+j ErZ1 Dmol\w*Qya_}=|y2Ћ[Y6M>v8}Jt3Yyh%{_#X5 sxf}vҴ6Qԩ1Nya'J<?WGĠvw*ԹWӷ SG V0!m`KOi's}꣊1DC( @yJTP.}ܧ!#!52)ӱkL$(@Hb334X`#(F"i(;MT_-'/6M۶DdYyǧMBr/ 8Ї*c˔gw-9qDĄ o{vXp#ed2vZ7j5>,-r|nNJk;0!P'A@o jThuFӐm[gjQL_Kq^AS%W-` iF4R݇jl Z@~/"'R/{ae ]x+sI˗ۉ0wNV^u'(V6ί?3uL ;H.bڑ(0߀SV6~:'B}Zk]3#BBMcK<ԮX2uPUDܞM>QeBA&!HOu&[8>0k:*i /NA(7 9r&N!C߬QHqqi>O h V#+{ ε̶3{{90>J1~$0;ch jWMY4כ=Z 0؈\KO㏳>7k eD7 ԇ)"zAj:1ai@ĨQᾲw{PG~`ȐcQ.R_g2pXgݒ>41+ ^B}39a^wxwN{PRGKjzm:H}*7lH"D5V}s٪z[qq?$U>onAu/cS|x*>~@JhKL7=MNUDPwm[\{'⾤\L~n$xw6']Bk% jp}0 $^n= ԁZY ehUDzF+p5Y>92}c'Oytk =;yhԶw{%Po=wxGiA~$8(}$@qGJ{Oʽo9>]h&B^7 , Cb99wOQ<~ tZƤu gB1=Q/dMLC5Ő1j~+C}S$jt{c! ]S/Τ.eBb;%uWW9u;z*䀺닲0agy>i" VN:ݽvfQ /kL4;}rVwv4Eq@01@Q5Sl0K&">K.n~։D'm' :}߯7%P%u@$!"{puRa21]ϻ);O7F!/,U'oݑy$|(0û*"8}I~exwؿ8u?ݝ>W>=HSXyq1{Ɓ4w/rv!*_Uf.Nu|wDJ>5?b?}I 85`ж/~zͮEם>oA+llSʒ5U֐LM (G%74 "b(؄g/Ѕ틐ũ|oz/ME|7Nr7eYB;ѣ1K83wf>FJC !"XތJRzjr?zVqx#N EN=z`/7}|I_TcXې(MJbiKըF,hcﰿtrW=zA@؟](J(p^37cqAԣG3zcU@`#^=:gѣ.Pgc NNѣ5=G=v,%l&7˭GcW"mȐUBAil?paC~;l/ ߎn򟇛ƭvj;n6n„s[U4mnFtO7qMy.NiGxv?YmPd}+UvuKԗ<»^ǔc~beњNC'~zyȫA~e~YΰCR0=s#du#o얌v+tR"̳"݌kգG=zA(ѣG=zzFQ=zѣ3zѣG=QԣG=zK|U21=z_oö^m#$湷U'ۭƮE=ҸYѣGlZeL{;v6n5zg=zѣ 5qѣGmخ$Jwfi`z3Zr 6|I(c YI-(6[UюF\/JM'LTc\cy0VLͽ`Qu^Z:U䇄8ϋl4H:?Œy<>Y }Aoas Bw?u: i}|LezF6ZQԖ~}`e;pG+YuTSE9t!tdqE)t5oC=nbW!Kd!qev{du4#QlSP@Zu$bKR8Ԡow ,5˳Stl]'GI˭1PL1 Ш6o@13_ѲL: 뿳{]5_YGt(a-&OZciqRo9tܷͤݢ%ՙdԙvFV`0D$:MH7n;N`Qc;IYKNd;meҲ#q: ߶1H1m?t)Yڷ`Ґ>꾐%"7},: 4(d%[IkyBgpkVBmcY73{]5wN;lt,~籕40Fu}c/ky6 TXXp/ppn:Nݷ:-ԛ_V /GYaCQƶtS^ۭPnNGܶKة#S6^9CA:-K;6i2.b#oӘmaI~:Wlʬ~hBFt~Lҝ>߯'2AG 7H2t-ӟw$\ -IY Nn 9r6$/b';K~mGn`Ϩg)n`e{sW}'8w-vu(.[(SO@SLϻiuϐ)M.Uc_r܁g &\w3+,Svϧb.8ǰrTO_≥@6]:Nil9,bo9em7 nlAɡ^ r_bP/KJܟ_Q{'qFVEQ<3f24KcvF+G:2CYPM?/̮E{NN豛t45;L^Zܳ<2S\}@&:ߝkC^y4k/#P˟!Ϝ.0$F02tN9=7SӦ9a^:,T+TxRZΦZ+UU*k jjh$hw?HւJCk:ˣ:T`/R.MٺҊ,1$&tb/oukM豏pg+ Q߬x+4on<`u$kYo\[aZㅊU1He"rsjui{lS,PYKo7DkQjsNy`CZ*B~J!Fs.C-P(ظZKZ.@B6T7*T߈h4p~9{;O:k̜Y/Xc a=A״ llԷ_5tr;JJأǞ%cDФ.n k,ײvG&̆3EIWc YB7iZ/ aSdkTחX_sfvY^cRؤX# m'>tś"Ld@H<@v{vB CL*EZ7md aoUn'R><$ mbLN4FZajDe~Pg[egՅ Hnn*z;E>vN;5Ȯ7㫙ٵz2'kɺ mldڪ k~ ydNVy~;!O7l<_\QOvtgq/da1a7D6"b-;u:a !褒KnCPY:M C4Ϳ];¾_hX^9Lfg؀z?X?(Op Z?dJߔ<6eHWGQ 7Ng ̮I Kfjz ߔ?Sԕ*W57#0w(dϧgRu}e~Ƌ왤aП64.8vo':1p6VjQ* 1psɎ`Q(L閐r2"2r vܦv1~pfc~f M7:_.Xf_,$'?)4t%q`wvkHvw?'nPp -Gb gf9\_F66;_>3"IA@5pRsR447 NX5f]aiYN̨K] LD\)Iҩ+ ,4qCpG! D>OϘFE,i2j2年S4|n>&2W| =#,čF>:~΢24U7jNV)P qew;acD(¥Eo}H 떁 5%4rׁ9i(`dfWC#y}~ w1o.RzHȷ~!<{UT}eFAh|qFJrR(r3ҸB􈡾VaT/U>Nu̔xy8A-sYPeWe>mCr'o;dc3=4r` xeH7_u awRG R7>stYO ȷΉ:/&3tc$^&kі6%)C6J.-f3H$}ˏA>1k HQ jŅd]P2t2l! ̝(w~Mi!yu~WOiT`y 5Hy}DiiCPBzD*k=5<\Nxk4'-ѭcG{}K1+O7h$)|Vt+3pR럭@ù]&j/.ש593iwA8JY GP>mF\Mw(\[o̼LZa}i/Q";;) MӀգ@:\ x췿W3c>N<3\hWwp7_1L~rVX4 4G5$Tkҝ8?^Cl:'jqR k҄oJ<ԁ2wvpd&LGS\&ɺF:']Y ‹\"5)gfF:#m8L "n #?- H,Mg'Sjj$Y!2INXӏGn[~K s)yT @'=ߝO|+83_ Bx01+đ2n FR5 3?q;nIfDv ]bFWVY+qx]o0e&?FN$% Y+d AUlܳe5膡QKWڟ극!3o.N1dنOO ̢˹su^,Q}m@mM_}rjWYWr:2Hу]UR@@[n! ,U,p~"UԺKIZ]yd"һܝM֕o>)}D kE_ⓟil'b0duJ_#TjYee${Hk}4IHKo&/R>}>mRSL;XR 2e6.v #4Hg{ tkR#w402D 4}/Yvu žh9:hEK :DP%5l:K!ZJϗS6,CrfE %V6U3sd=g%Z]Q]qfHX669v͍5^Z Lf&Jύ+v1Rԝ#9z GLLݥp D-ki0u :`zE%^_/\`m6φ͵5sSGOFyl@o_MUhw S!a8K6VXe^ZZ5^ciq1Sqn.O]r=fv#qe֬Ac|ʚYcX+fKv:,s.$/f"Wu&rC_#a5E/&xq(֜b[zGe&o"G9C!Hѓӓii鵋NmݤY3g/2sn7.shl!귻;ۀqۺ;C6SLQ."^MSޡ] M5/;@]e޸F_ZܼQ-򍙗9 __Fm%!( AVg;C}lPYbB)c1xV~< 嶼Te~5/7^ASw5!zb_z\\Z|2S'yש v͗T&5[eCwY/ ϟ{='V>vȘ-##NF뗭gdߑ}!ǧg;uZ8SEno3/=sH?S<&1f?T5x3稼q-7s|Go=?roGʈ@&]NZM:e&>: 8Htda6E?+ɑ ~N3} CXWyR4dsPΐ=`Ӕ7Kvμ~s9G~QC66) pOZ=vJ Z2w/L2򖖑y@I㧧FvV2Jc {6v?>&[U1rЉva$:hvh#_ɿA0f!zo:9ν wN[aH9mǸiݙF.<[dXrƽ0&k%KYϼp}Q:$)DouH;7O˯]OrhL09;R1>ξxUPpsx*YGBUD"OĿ;C[ +snA[l5wDyPF(R€'z#6ۙAZ)ѦȖoTXxy5h?vw yOUON'!4ZהJO*w&35Y./K3"ήZ#'Hfd@h({vM2.m8SwLS\.wHt@ӔIcU'VʫF:ެg}#&+<_Rx:M~!;13s|cY~[EV: 1o;ܲpy֬4g6tw = '* ""s|sf7GJ4!y>"Uor~vA+:m?0(4=g:#v0CI<`Fm(R 5MA$jΌ.Ztu S#` +\lkUCVXaQ$mSS'y F%Ԩtiʔ"&duM`C2ڜ|:%BSWEfzy=9z%%YPc͍5eZl} tc61ePWaft쮁ԝuZvy#'Sܟ38yPzcyy<M;ERfUNo%֓/<8{ĭɒ~*dEOH0 p?)-@jrG5bGR8M5:if2S<67ApkWCza36& ^ؒҴz$SI>O0'Nܫ7+^)2"/~7* Mq0Abys۠oۃ@zKKvRmr0b>zKhdjZOcKU_4Yo&:e(CO:s"U<4%QqYiKMg_+gFCNp?2z;zGNjS;c:|FHsG\Y;R)&,FQS&n8_DWWR[u L{3(|v[Ta(T};946C: ÄD#YX>LQ{HZYOG&+?Fy1㔓^z\9EAJ3sZ\QolRmYi obYS#ؑ]j>0ccrdNTٻt[ # T;EʫZ "k"g6X:i4KOggD@}jPq!d^XPa Kv׎;0%9"_Cɾxo[5glN\Xԝd7t\ovV$ M/0Iʿrtjt:T?O<Ʃ|'5Z<)DĒ@K`%.")'Ťy.m]n>RF')7$FRYZגW9?`7pFDR`?+EML8꾅Y+NF}"tCLx;Mry:ar^n# ("Vq\u["y L|P``I^ЊNWtö?/{(2 F.-pnMtkt&Uڣ54x1b{sguTʃcaTfMFfl=L] I3]gq}{gxu~aiD $Tbq P'ql5INqv(|%4+0t\Iz~'oQ H GS7o%Lba@7z0kܼŹՀTvIHTUMof-ƔttL?q^g#M-6yBqgM~FB] =Y 1]TyR/1 7yiW)<:VW[j`n+s4C4x}LOg P>2bO;0H^cН(PgU_5fγn>~{һNrW ׀bCr*?w~WHlΛ2݋QdN:6+[܎;Չʪ .qLoMnvTae'$-FZ mIɾ Ze jE^: YJվyPu/i0̎pQo?/S%cAb$hkn 70]/R:aⳲINwxZk_#cSo哟xH)4+T9sj%oݰ:DJC:Ltj\}4u8ֻʟVW AQ!Z&X)VqY3myKI]n-!,PGO/K^,#Ff_ǿLś"knXR’:îظg>SV`Z&<:<Λ:z0'O=̉)[Sq 0RIHțt[&7儮{ݓ6HrˆkWaygw"%"TOSʒtf^|qJ\7{O28(M՗t[5dH<^nd*}NRwSOaqa8qu^{NBKMK9SkWx<9xS HFIĒc{=XRjG*'/7x~:G$9ޜ}(C$uOH/069 i <8<L^7QqIz%S1@g# hJ_k$HXduqUu Ϯ>{۠y% I W6~8Rs9 ͜*KUJ>wmn:VJ֮5hlZueR3F"5¨E1[ϟIG7o噭W{7|(ՙ:|rLZo!Y]mpaix;+G׸P9Z@~+㖻v9Yʡ-s}Ik6i]#tm3y&)7::\tuZת/~xʞm·%o@pHos2_}q+5N?y;Sן)8z5V-ZgNzevPa9=c}-r9;G[dwfUe"ѥԨu&<@YlwE{mq` :)6o,XW$uO!~Enn0}[G>!J1(0xo`n*MVQ<%d4~-^LZ%˜}g OR/fy}o'JJm*{KֳY[S eqNӵO7+?ׄbQm4=LVIgDHB)4AdnKˠ9(;mze~x9~N4xyw0vh@w1NqxM_$'<97ˋgY\_'0Px[#xFs:s|vC&GFtzңyVfgiJIݲFfTS>p w0rkLJu)LuԽw,/}oSSG>.v7Qd3g^7=C;z;>w Y a1:!~V]Qթi\Hއ>ĭü9:Zлc,,E|'Mt42mv;~׉:U#_v*@3 ;M9ەM7Ĩժ:-}^lpyaד%&nNufٞ P J@DZ;ڑ2֘4gRhO T$6X\ѸjWB,0֧3tˈ]/53>c̘QMk IDAT3 Xwp`YtRSCԥ`Ҡ̓Sx.r8?{LJ*l| 0lw2)LM'G] ^0Ә9nzNmSշԈ{^ 5 bY(eb}cc/.FGK"_#L;xϏܕ|# 0O ZUItQ@'ʗ ѧ!(!nT4Voad[(KS[IG0q` Ob}z BoԖ|?ؾ:!9DȰȟ~;n.1Qj6" g/UŲjyrS/FnLMFN>_G=[F S::bdf̈]s_yr\*l); @?eN=EP$Hp2G"s'nwjMNERa&Jm?Y~$V, y:hqvczDhȈ*MGWT|+?OFvw5(K Kuz~'(E59sBG? ieT*Tf?c~RD 3f0%ҍњUҒ6inFQ }K÷c2)+NN7Te$KuzJRc!mi_ϯ^`Rט9lq!M-sd.@[q $je d*^i̓sK=uG:|K3}+ְ8"}c9:-nfbC"РT% ,\ZlHTSSw)sV : N:̡!ɇ0778ezxsbO4PszgƕA Cuq#B7nAnKZΙCЦ\cSjT벊=m1(#vw[_|.F+d Ja4Q׾4a7UTPjI?.)~~::yu̻ ˀB~S'y[NZ"h Y"?ޜ0Zt\+|Qgz2 ^5ml*?(/ K?vGph&/>>Ց"QQk.̶u4I` iE]U)=wPRkpHYϊ{=}uo<`ϧ(8U4oRgfWysk_uF ,\Z>ǔ/~JyZMџ]6N;IRӿmǿ;I./sԸނ~'."E(0+|O%㷫L wuYTڥSiduSxfKH)ІV $Q,!')`GPՊć9b #ކ 0ܗE`H4ǎPM-W+|/ra~'Fԯ|MBT#: @R~M?̩ӳE˴@-Fs!Rx ^K1赤8 5*HBBGT З"'=97@wa'Ĥ%'4J;Dt_ TW9 q1>CMGE:L 6? H"]Vo@5HSLS7*?ܧ>#Å6;=RreCèZX}2_O2T։2`F@3b/0Uǟ9GZQRD +vKʻn@vR݆]ɫOj{Ӥ7dh VW|%u Z: >.νLRp0_ʧSMAwi' ?&[IMO(~~ZO}Vr=fv#[Fԯq*Ub{ @Djyp 7(:THmv1oԫf-~ ;ɎvijpGڛ[w{BFܩ J"55IJK;@OVx~Y)wE\HAE֣:P wUWi S͌ȗFn 򀫑j;j7` MNHbpSFyS˷Vtqv ϕ:=0DzTO-9ks,WjLicSgFbNHUH@M[z209]1 M&pta葈ަ8?1V9R?aefr8O" 5bcri^ܬ}TPwO@dzT+VrQfR͒K7dt)ɳYd=|}4FCE87G+x~esc嫑]t[TeeA2R:l/Q)j}ʈ(0ToFCH! 8B5۪v,Uc\;M!!i (4 D̓PMq7(RsIuLn渳×K7;P)f8?{h=Y{߁./WjɍZM R`R@z7RlW(#St% N;1Z hOF]%s*4P4QCGLfPqzezFQ&o)"U*CsgTz:V PV15ξԽǝQ!K,8DeFuRIv$C({fݦ:a+1}yrƑvB ?tÏT6c;~p +j7Q惔HqI<(8x5`+y3߂AMI9V͑Y+IB]`f|GSf.^aТNk]RϺ 9ꋕ= \G6IJTGnԛi'8;z ~O?CoYy}YjUR[U#勩餩;946d}Ei@Q߬2JybTY=tUWPܑN3nRyn:Ϭ1vuM+vmWӍPdPwMA u- ]oHuǟL4 SeJ;zchOmdʽoHA*yv$:oB&+%u!Ǖ#HczHD&߹ٸ?V6ZIcw_~"0~fK+˳l* hys\[|?̓&H\вQiDi:,<Ob6o6(4`9pM$|#"g$op el9#=W&]8uPAӮz#|bGFJ^_]jAUO%:8,ԘC)!Hg$y*zzO?}΁o{ף?P,ۑ5ȋ1V5*sT.̱y,jXXo2PLr9>} 3^@cwyѷԢ<ۂ5:iUw/ͱ']?YG+uQS%U㵫jr1\ q~ W̩Im /']WՙJKWؼiT!q)Ԣڑrs?:ZP k|n ?hEio닜T8axop~a;6y w04?k uҮ1;f-rB'~:uT7lFҎŦ6S]:TA$ 9{F=dO4 ${4(4ġI!dp" 422{mk2kqlYX홬c!Z%~l<Zܶ99"b>|5Gu~/6%~߭[uשsO:pSDwWFe(~ fwWH6_^aPFO!@c R\Rjćq̆ 4o+jSN65la&sýu٩sj#/(6-`f#_*Uxἵ?9:Fe*{z(K Wkڭ5F{'A9Hz:IEH$.W})_F51t%hPfK״O!\hg ϴŜċZYp2N JD($mwm.m(D{y;)sLr8Ӡf>Ǫm铩 Z/ O=G(+< DW-G#̀&sb 8q0cGXjI "F1^Bxb@X-lj.TQCg>`&6e&PcԨukʌ )h +۾~1+Ӷ2WR'b%H$Wߙ%Z\͔Zxs$|o8u6/~:UKG76Ғ$ Ec:Ppjvl96ホkovxmp >%5 |yz.&zE3'.SJGmn}3Ss@;p?~gp폹gsLLX};cGKД1?5w4fJT@;^cL o_UޥN]Jip}LU%yGE.@~m% <=2W#km,Q́ ַkfLZ>nD2Hcݦ͝EݣfQ7 '(&V^)Ѻ r E/y˓zd,g]w;v +HŜo|yNf n Ei~P?MS~Ԭڦ1{FMٵ@qhP)hO`83HFcg/{Gp:U^45Idzrj=|Iy7w̶n+o-}=|lߤvWkʗRHXTՇdQ7<=so31=˙I{p`@r:@whHr C;s^ok5 PN0Ef-P:UBPC226amΌcbjkl[= :FJbHGxh^Bum:[@tE2siA  l\J PTf\g,}akRML|yNfpS(J~neb w ODmK5mo# $h E~1SMHian ?N;p7_vqi2E#G[xXH ba^)N6FT[$B{|4=A򛂨ge GeM fXcӼ>=Ǚ3g)ͨmBWho^SdxF  {91^ D:F^ VQgqWuʬq;Ah?fT;|LYjPR6 ܝ";mcdǍ:-IJ Ejħ̤4xH BzIt{#zq³?-G;q6+< m'Bh$ERz+u|۸꟝bUl\7 IDATB@3o47ڡJ&gVh|cLap&~{x4lu:Q VJWB0Z؉h„ur [/>t 0WIAX4h<7OW [BGȚ}lGs(l;q44?M(ql{u U(J g$z 35 $D5*H97t4)1:IIx6L@cnY&Ԏjvhq_m쇃\_cNT*Wv\ 0!!MI 1zZrxKL8=0cE5)B`[A/@5GYHȂc38٫O߆~#Ԛ@ ۓBIB+U"ʡ"gv2-(n{K' n^h$fd>ѠJPmo% ƕUTG|muclފNZ?4W|ڭ4MY]XV6ێ\ >iU o$:}N;i<_$~|2hLg H`46H/cqPa BcڡaEͻ:LM[RQq~*cp;q }]lZJB= B5d5bvpl\cn7FVY!k}ɂO;ʑ~=E3퓏[ŋr5 B}^8BJ `J`2VuM#sJuhN jsc48Sv0ӏ_6c!\5nˡ4 GwlZh_I^cwleP?_|7 PCJm Im]:~Ϳx@JBP{g6qUPc2_N?~Zи}/k;ev`aaB;ҁr*CAd ؊ tz˨_uv78Z@Nyꉟni@0r Rfbj^Vt$Cw!kV ȮgubLw ԷgV!3Fߺc3+N34v, \Z4B9e}ݻo'/#T2BvMRf4X̹j"AG,]PPH[;sfln>l YE[e"H enbH2f:ӛ5Jf x醻dS݈hk@Z\#1 |w7j1Tf`D?yeY.* [mVxEdFx6${ u\=@" AڒlɢF/?۠0[yY^zY:C;;[c9VK DNszQߎbk{߿X: m1~3?@9ӶRB˼RJҠ_ԟPcp'zIwr1AZ/$ SnF).^ͶpanN=oi{YM^5uJY5Y4= mLA.:NH(aQ`Ap$oL2tVqoj+łظ |ipFOK#ܻ:ث 4kGv#-V/Ci1ݾ3i_1=N5R? Z ǏFk)^s3\,U9v拏~>njT;~{͏WC C o#mxYϝ[͍И5TN/5"&nG`9i_Fn @O{H)cW:f^Mx%7ϼ}^b kit,P 9\Zb3`<-,2ц ґ8m$4]-s^i Q jBc"qbph7z58sF{W.![66j/.b71vmB ĺP/TRn#K/LW\d, N?CIxul[81ɀ6WNMgi}|Z D/9CYFܵ?d *9сjIp8 ׃'Y ߗ@ x4e@j$񝂝d] g/W<{3k91+} oÀKJ5$H S@b*OU>oC~ W$:y K?@KI鶽yY~N-qvO?ƼLӖ V>f8Lݬv84VK-NQB-t.pG_][`Ks!R EZɄg <+ y bf QmiZT~ 5N70Xejāzз?o8G^_ PijApF:ӰS͢ ]ilcf܋?1}iὬ%z6Z͒sNSvC5u|A< ߦ)QGE@_a4oFNq s]l]n4rKcd~rRxԬ55__k?:)AHpB }a.a:4C*.8ck^W$iFh$aƻLMg}NGAjkrl}3|!!뮊`ʭ#,D3OjmEHm-@q굘Y9ۆgt,WHT+%t`icᏆ1)MIi #QlTL+d hz1xaSʼk*@7G,sszΩHMXy"K}/!mPrLOO^Ƀ<]/,Liz*&g9s\c)mce&B-YTݛ&Zd,XPdTfT!oĨмoN@z|caM3@s'ʱホvXCmsIEJ*5)qya԰WsH MBulZ:."Uxv^Ve&r(ΫzhӭO#u FiGI8)HUOh ~K$5b!7&뷙#/ 5/e,HB5!))OTͼBQ#f#mkwIIŽuUP\<g޾brJQnonuW;w0s/bbG?I@ 83Xd4F!FZ͂}̖C6U0ւBh*#闎fzj;n ?~l|:pUׅzo!~MNR(:vwƐiw5v~ yÄ C ,_c` ^\oOR!{rՀ:ǏN`pS_Skr|Y_ .O6oGR~NYFvqɩ^rCWcuܱ|>OTZ~PZvQGjm{ i *-DUˍj.^`S3lŰ)uȯ޹BVl-,햧-[^y6v޻ Pz#t4ӿ2w6YnWz U/(["L"N}^T 99.Q bM z1•:uw2Ϟ䕗_cp&>v~ S,tS{D(;=/rH"J]}mBmBoNpT^'\s~Sܭ=jt ᚐ6R\ 9@1, 6 "PbfVz fS7S/sz-RC)Jf1[ՠl!WiݦA;7/(]ؿueCBvo0Ӕ-L߬.L5ԹpaOId5}V|([Pc/ ]؃-t8-ݢ< ŷS: $ju tyƖܵvݍaȄAcx3q;P4FƧ9rS3=5<ħ? *.2 1WWE`bOL601Xc}#r]U*;F9&޾L] [͉/,/h#f[-kc'_ꤋAܠGь'uXe|{\KYqdܘ~J;G\F]EBi\X?cYCs$@rmC73GL)X`+Vp6;Kfsx1|9 F5&WW5oC5KCx|cq1ᴿJ }4v wpucϿ-e>г<ɞʛ-h'v+u$0UE'C;gua@O1N̖\r\/Wk\醙2Q943ݜ㹿s5V\|!np3g);[C شnÖz+̅|a)?=G@]dxWg;aAwH@1 8Y/F1͛BQ QFNcld Ie /^WL}٪/6eٽs C2P266tCpGLoFʑ1naЀkmI@iVߨMl05tv@-DLBh>hʬ*ߵiW3V4NJ`Q#˼;iDn Q!NV?̻6q).4C4mFsM W\%#;\Uk@ v8ߵ>=j߅@]KS!kF^Bp~۱wedpq/F1E(ZVBoC{8o/}9eI4ζM n`0J<؏2Y7 uTWk 62 Cix Kɯ-ƚjz;kK9KOw{7~bMk.֔/-}P@";v ~Nj?>J:^@*(_VZ[81v7'2 w{ՍZc~5AI8p'{-lhz :kb7}$i7ZR/^X 0/p '^(Mįo@c)[ii_,(Q :O% 5yd49JJ/QE京 }kՑ]# W(Hp[?r඿w3RH^Sf[ 5e;*[p`m@W)qMLv w =E'|5߸7,_݁_g5wNPp~03WguٔS{r2P_;C[kEϖ[׶g7*~p "kԻPF^_Ωbaw}pz`V]j k֔/%S?=^_Evl?+\ԄנV'nYPB\~NQ~*[W3ߗD4BOR胰 @CJQ"!&F?mbA@gG1=Xcq=V EkN&y7qV\]wYiW0W \^V9AY(4u)g~辥H(({+GXO ig &Eh)wAKX4<FxZ|bLC|bhY]( v/*cB3aէ#H^ Rqvixe5pn` nmԨTNɤgsۂQ;ɟLJGwf^_v;f"#ppg~z:w0LOS{Av Kg9>&\i0vw{)8 g/Tq$hGGPäϊ $Y %v̥){Hسm[ص{:X5J8l]Gg\h< | D4O|N`%hrJXr=U\6/~Dz EIHN_PqO 7{G__>6ZZ3흴O'pj߿ Yp:o+K=w1T\WB2tr~BEh&!g#slĨ LΒϯf6X 4f)D pt7 UJgl$ӹeii;0.^BH50*_1:4;tb|;z;yK JkS n2t>s#3Bjkh-co1$~6K di4Zip㙼ǿ<-_ Ξ c4h9sn޳VôCHZzN pe"nFJ*Qz3 -ޮ=,qsۿD֪3`N|:ٔO >> oGOy^EnbxCnvl .Q%JPZ>m[tO/۸{dt3LLP@q<|Cܳ #`Z@Z@ĸXǻ; IDATYA;4\o>vHL\_n~~WQ oh@M":2JF&8>&'ϖ!WQ#)LB>$z{q`bT_0a )@H!W. ~"&:/i1Vk@k$ED }bfs˰AOwtԱ0ji(O vA]MLssDtwٲYMХʜ?d4 i~_,Y0}j^~|7h.xi.;"vy1NÎnsKA\4c4U'6H޸wwbI ѡ`[4dY/e%Ux|ǚqgR];>bʍ8fmS3o_~z0=(7%Ca_?)ͦƧ3U[B}zmbVs!iӐF/- Ɋ)[VP8p'$ Z&QNz|QNot hJL}`Fqݛ0Pzl'vbuʕ7^B)uUьJ^ncf"#A8~,t ֤'zzsG=R_qfS3c+÷ػV \#RSH&?bS}Hu_|S R"tw! ꨺v<ƭ{+SVB( f' fa2.`YN|jAKDջB *AO\>҅ q|U&6duJ Ks[>Oq'mwBtonV3q;JfAm*탱C| H3U'bz9hvl'r-[ru NUJChο˓$y}-r%\b?P팇C ;?= nZ_/Yei'nZ,i|Ҭ BQVfw,:Yi ;4h>&\[_W0M Z%BC͂S+AO>ߜcjv===|y^{cY}D|,:׍< i4a~Y.>ԥ}$Tὑsw/1$7qGv/|./)B[v"DW& h_Y_zCUY`!iӰPzYcBY> BMD?F.XQOMI.;ۗMGRisG;Joluѿ_~c%&.sO/ -WSzᯛPmQGy8XJ,P$X/+~'ye Mn:읁M ó}pU4ŋm s]-31u^ga.+0VwU>q⭆v[,IHbCX,%+qH Yc%FlkyCi&.o=JX iZԚM ⳬ.[AX|f+K8OոZ H+b`.>Y(Z.,-tA/.><4n ' <ڐ4b#լg;7&xKr]컟cXcTm6 Kilk~蔖Zp߸/ t$ KzkUv5 tD3?[~( ~ d?} ~~f,d},4tfLHTAsXf3t᧝|5RŴc|pxI2]کBК~!5]tѧ>M7ۉ Eu7M ڙaeiE?FTEBBc1Qm'J=>0:6Ź3\S~k{r_g ;׆ !yjN~\: qQ&Z$h9 _)Nk-X`ד !iolܐBQW89t2iiLNe F(7_NSƬp_G(d7Ki#nSO3=5c&yVt)5n5|z*sɋ'9Xφ;X=1 3$v op:M6tqvk~?1 _GR-ը]V{ZrIǀ;̽{>7ū&/Uu6{Cwh=_Kh$BMSSY?mk] Dxcs Z+ab2O iQE?W煖o4z~NEZM->U(b#-̇ƉaIμ[O.D .t ŎHgkCh6iuRc`&](ohP풝Z4QSO2{[瘘{y}۷@?~#NOqj]+<+b>i$m*{+^RpU1 g]vQ")ugK\T)X7WFaEϭW)a-tN߹,]WRyw {훭dH~ /& H7/o?eX&Ѵm~ZC#_=}C|CTJS octK 秉t}C"n&*S "z=[G&8>&쾋xvQʅB O,n Ͼ+eϗCafe#& 856ɩ:=F P’SrxqPZ ?$J65*l))5* l\˶M|>% *G`L]J2^mB 9| zs|fKĖ^\,g޾‘S3[78 jhnAk+T= 5^+읁D7~vyЏ>t_!-|(.Όc%}~u<[l?>>8v4~Ŝ{ r~G>sstyN,0 KUF'<띂F,u9:~lgZaayKò E,7&=Q-_܌`M_w[ /K0qf׮a~{xJΝ'~ʞ'K5˗.aw/1w m=H"{wz/ңc^$| $|5 (ȯGx'6w2F؅}Ћ֢rUbnsODx Gi-Q)s` Z+#Gs^8|~56bŜ.uZiq̀H:s.Bꇳg~BY͞|#F06mV$T^iePP kts|31ē<>v~+pJ9(I5bYawfdd#VhX a6 mPl"_6"&X_L(K#?ޮx!̂by)IUqwv,6oUmgFM>vT4ہAqePWsa, @8o\I 7 `pMPc'FP[]/xY&NsfibdRc[~ HlIΌbBsvgp0ϔE'YeQ-9Eѻt4$g'.*чAe@meGv:|q~ZJi3:;A;ο׿/'?xAXVMJ(艦&7L ';l FVFopяLw:]سJ--ۨ5Dn'n݁Ps nȂ$B|,kl* ʥjhv塸_@]VvF (߄ ]F[oGLiq5Pi+!<5HE:2|߃wp }}< bS3=}=[bk}f a7WU" Ҵ]Bf]yafle5DϐLȘq(|?@pp^IFǦ#h>s:@HLB[Fv?DTӻ0Y4\fjtKp85B<>>`\Wj떱U >_λiKeOIfLk@]_ϿU~|i4UXB;H3>Y-LX*jQ9v{ͧ?K]YȪS[S1脞Ocit>$/s%̖ cgg Xʈ\|ij9W-PtsWT*jtF4WZ#U,wN o\ e}jgHoRՓ3<߮O P84!:B'š~ N.)FE'mNʮ-E>q*L E5ÿm\ @\ޱj0e'PL+Ǡ߅6}[љ/|\OXT҃ #_YJh'r3G9BڣC;⏿NFC8ؗ+': =98oX 5μ]br A.ב_S[̽]9 з][nUdzO&PT-5voz< :?iG=̧?F}RFX<5KJRB+@`.Mj/m _HZnDAh#c|m=ry%PzbÇc ;ŧ?a~ރE}ۛBQ+T\6WfFחjqvƧiPJ@H\_2er"7_H}lޤBֻ3OV:& O;<.7  <A=LIQcc!/I"1vRnr!MSṖjg_KO;aOi~L<]麐TjwHI `ʁLp[Nx;kEr<1b;M©4?i&+]|7_ZdbgiB٧J&h- ݬr#iig'~8w|Qu Rz4 iyf!+NhaMH$fu Qi TYf҄ r ݦǙ~ h`@a EGykm9B7=?xC|k  BiH [g^G)Q ppߞ@ekLR Rim5$t뻄H]y| Fa!Ϟ >iG(NV]dm,g6|Rc[YXv&nT6{e\ijs1ϲhNfNuVRt%M!ޞ1 %$40CCZmD8ZHمcdަ”QX~F|Z |~^޷?49Ů/.Ra4#<ws'g'P*8o"> In\:c[yH%Nw.ԋ}_ڷxꇏs4͞`"i ţ>OgvP-Fg, ƍPF50pf Qo=}OLRj; wJ9tZH ;~6rJ+~T㫄|nOߠvLÔ+F[L`d4rW~yJF`cON IDAT }a!OƵkGx{:T< 0dVG^{cWHi?H( `+ׇL@RS -J3FBs5"\=2On&-idխ_,o|P`;kI3N4 ZJ{pXA< N!y o`2Pb[G.kY.S<~r"-uw~Z~ԂMx%w}rn"~oStop'T$s"0{FXkgJSة@ຽ0ٹc\!s^&sɾo fXaB;i 䣝8g3T'"!ݟpi|B{Pm>|봭;*nˁtY6+|aC8io:@GcG%@a 洝/44’Y$ЯC`@?}6Ёαip ?( ۈ5>=|sʱۣKcخ,t|뜅Ŧw„Ȇ|KB{|>_?]rapl7>Fk[ IM77!@bϳqڎkLL`VV2Vh~Њr/q4ڙ9.L0B. H'_'6FTϥh@-Z]HFpbt 15yorm蹵>@aa蟁D5%X:CSdaۦ.^[JEF^X|:-roqF ighw`/n>Yi8)o)ß"DiTVezN7|IߴQ"@/&RuE#[d#u1b:t=G VrlM[8z{Uz]` `CĤk92|b$P..`'o2Y_=w"_( /sWR+wWpq.GO(LC!g-wKA]QIA˷􊛗tP,^E9됟JLo~D1?zygϻB`g;i8svXPN3lvOcp{W7oixW( ȍbn'WLǒ64gO@_Wqn=[аg(&"ku7G=;H2'`b'Ű AϢ: `xE0w0gaP +-ܷbM.A9\y\j aakk nD8a%GG,mf:l&NC|˙B.Ta J+9^޼wdUtm2A:iطE!nk$2J>ZRgg_i`W7m iڦQ3lSn9,"?g|xfWXN7NpKOfAa(bf¶wщܕtw'ĖJDqOx{*Ư|uHJw9('<I9:yZik;ah9#rNνg8?)(K[8֫tY˯.Sfi0Tr Gq ;s{859~p=GF݂SLfF2^ݟZ@!a gö{1't4.G͍ki8Ps1_=ǡukx)& Y$+4窴K,>͏^C&ɇ 8<A>*Zb""ᱟl6S]&Ϝ&g`~uMSGl+TZgk6eog&w+Q1d-4?}?xZyfYCGq gy<{r˼h*'+fJL~!uF1{rᗒt˧P@5AQrt|Q0-G≆f~GhmJeMY (0mmܸCZ:̆εܬ!L妘W޼[ 8h`σr5Cܵ x~m O1ŠXyj-<@(M]F&qa(-ΎvjF1۳@;zEv.?Yp^@IJGpOG)ۯ\1ixv'_/&ee?3>C3~<7g/@3>’30` i'_w\uJ4w:d2;?=g+b*dip&u7Dfߡ/+9n\^sFWkT>+D&p,N=^:|g&c8M$;_H__:̩b{wp 8_u;O)r6~ =d_Ry=2uwoqty :JXΒ֜n*{IYV L;~Y)^qqWW#^<bx7{T]n%W~dt>*/ܸ{=-BB[Cm畷ͼgϽ݄[o^uoT!Zun$oau9B&cܗka˺(HEwmNT)q2^GꫨoH5"pb[ͷɖ-b+PAeƌEF"V+rRo_bۧ+bLZ2ʐ Il2{~3]3>w8[U\cK1&*ga/^bWWrv^~{Y(+ӉggjǡT^ K1xTU̮KOu$ǁǨGixUaIju,[p[ ɏwFb/VZݴ$lmm7>GX:~^˭ՒM32w\>̨Qd!N]FnAaM,CO_ &ΧE4 zuiYc/U xܑN¯.W^W,/[t:8݋'~Jm%[-ӭ6F}},FHaՊƢLP p>cӖ:Gy|JJᆌӁLok(=ګ,'ntuvS:%3EgHc@=EIma"SA&=-`?5E.PPWaX3~(I$Ȏ%dݷR=OP-jmaY !"HAYc|Q~7gpLѝrpgfLcVWL@F@$,V4 Xb93I}ϑӜ|V!5k9X??yQqT/=Aqw2NJ{]w@p~}/(@T7O΋ w_|"/fU(Rx}CQd?c= CoB+UeN9 귘\^O Tov3S|-=Y&ppfs7x/IAٺ+Tf6&W 0Ńq!(`*hI4&J3մVQyA!9qfGλ$`0-9r3}0NjE# :Zj["N;e.+=}~aD wC:yoО˵7bCZ;BLGR' tʹ{*{wl6|ϣ$R%bzEyaLMQ@%o{S^Ӈ;FU]5LcMAb'd:]x)s#gh-$=;N(}c&vwO^k7.?f( 뻕Jჵqq]wTX(bwp*]$.4iqN fU~ w: ^67N$:ļ-|pLM%$ikNΏCTq#'oh"Sg貟_u=Ifx;.T5dFBG~ˠ4tm]8]pI£˰PC(ibZ.*nlgc(Nu뗊8~Ug  3kɋxi|iZٷ:e8GL"wC[f*tuǿ4ah{80o+ꨋBܸh[#~LߘsHn#95=d[Dv0м2zLE!chx8{w&ʭ[ٺXcѴZww5L'ETƳ{ݳ^$nRL21L$S~"1L*a$ĒKf۸7na5iL>KG(ۆΑ\`I4@+ גɎsh+C_&2!`6¿4΄>S|+ W4sc۷I%C RI2I$>ܷb"z N24QJLxk4ߩ/\ԭNMy.u^m:۩H^ns($.ze[=yؽS7ʏ_.SRRʲtȇqW9SHѭݣ)_ +ᯐfI4$v ` N9<~VbBE8(b8`-ݡr.mL`]&4 &bXA+ g_YpDfX`9E(LhhFΧh5nnb3}]t*62~N 9cy2wU IDATW]O$\piXP5C>ML|n5FAMKJuX/Օ+.$⪈?رXd/ eX8VG!k !b'iTo$3@wkcѽ&1WJayIK,ƣ5;^)qq*G~)-!?N,7SQPc+a)G`7?)`Z atw(u4 μqS |+c:'JS@0VTӚ`×A U r;eܧJ9:q/A SE[rhn,]˷w6wR%֙.Dbɛ8rk[7P$B-L hx4J8pOr:SR|~74W>k/z=Hdݵ$0j>j}qQ(4[PR))P#lI$Yn Ξ-c0I u|F#g5WKb"w&;NapykՊF!Q0;1F^k_zQtDd VMh:]mb( h:Ȱ znK8}VaH^-JWAV:i'[J>n5!uN,[cX+&mĦ;[bGӴ=5Bg׋ge%CGK-꤫#?ϕߕԳRuw]ÏKh9*X/7^O}G?wwrNy W|:& sլA,V_VtSNoJ]rsSmDŽyZ3O8v ipE!g=_^҆̔Ҝ~fIzGsMbhn[ EE-hEӁzOƫkr2^ud&4 ,1ǽbnrѕ?zv$Oe}r0.rik)?lDEԧgIV3̠iUR,o&rl>ްD!&g/4v XSTU:W]O*cǎ禵(>10t'>~2K"gC[ f* wӂv7xU]YE@K;/(GT'cf|y)Va 7oBA7)[4q+~A*/_`ubzNuux)fT`ƛ}ƓWӒ[Ti Բc{|tBJە R9(N%]̰Q0a+7*)'V; !!ډʹ>/js:)' Չw8@o?,Lt0j\Q-'ޗ=Y\/GXdml26X1~~[bxX1sIyrKGyu Es),$5ބ^?+f ""1GA^֞dz6\g7w}BrjiH833O ė;Q*BLt]#GuRÊ{mLG= ݋gOv\˕6qbN3 opV@iH@;u@])6}ݝO 7ѭ7;$tk>p>koaXw]cӖbd& c8?=|7`i-d]|- !CD.C~^nlk;f:f &92,65Tk8ǫ8彮_i { #<M%T.鐔^xvN5'$+U/RtV};L/rQf7* 7$o+^n?N\ qe14ªILG~HXU{!6whizjӼVP;o0M"6n1FA-4~<@vL-C78P6Uci[T1([ ׷֌R&9@7T&y]n._f(R~1iU-`9C0v{b90<|9t]'{ ygh~q ~uor܇TZNy(|ۇr| U"Ж0My}@0'Q/o^Gq? r,u?@b 糼u"uIK5P5hHPmXaޱ6+!&yəz`];k>nݲkɩցۊ"&xk7P^a9A YzC>TD+7f גB|x=#"fXurA:[W]Txf̒/9TzUd05 Ey'/M2Ay}?V:V_scmb ~&ϙ|c0!@4\i_}BG14_H]u]]R_OwO/z-+{Ejk/2!4)Mo 7)_pGFA#OLb]+fq nنW&C5!`~1߼"#ȏ F/9Rpe-uj6(o!4[Hrӂq~=نMXk&g[:nw}O3OAp`-SFkT^uFRpf73lCB65u SƯTT|7.H7&ՄE!0^3+ W{*~1PPS!1!h涆&Rd謴B t0hKz9$A8 d9׾DROv^]#p(1j$cFժsd ],ΠivI/o^f LôZLi%U-2EGPk0Ír9:]P[|u8>nMIKaX(Ecm<m(TBXђH[;{u_.C4UZN d,m R_ 0PUӞ] Ϣ˛K J0:L 7Ԝmj1O#arf&ׇ kalLfƴ*k4ߤr_X^.dyckD*~V`ߢKXir"yXy(h\7fU1/+ӟ5NU;_|.<fIr^ī~q 3^L0M)'yAL&vS>Ϥ: 2I 8ԦB:;Ώ[oAu:_I:aI*_: SV19q^FJ3᳐Ym"X`( &:V(Fm\ d:pŒhdZXRgwVLa^_~/)WC7U1uMgRMj96di<j`z*ÁGhm-\^釨2)eO4/UUr0jCLbhj8xp%+<&DT*ݷQC]Koٺp$͌E9WQڗ.&ڸĞi*8ZMuiP g0xbWXF1^ܳGXH6\*56(0^{؝[>p="UC5M0.iY\>ɨ*"0|[$ríZk>aᄋum\=(_f(:;sωq3)ݧx.n{7n&g}q;ixLFΩ۲whd߾Cd`h ذ_~DVk8. 4/gA!i6nH'Fim`@/(AA 8|l*I8纶)S, ~˫L#+[V>nQhhdxO3 wqg7UzB54W/77Ad]6P0 Oks&'#wh iL 'S<k^r{a=*]NN5/};r.aKdF׷mUn~13{J3ݘ)|-٬xiV<ηZ+'/uİfz:"}c;;1Ɓ~zjvKgayd^p5ģQzDfҙ:1}'Y\Ol|V6]vtn> b|ppL 7e#YuX&c~L_C ds3kz gCBϑh]DG9hO@ c2ra$GqTm $g;R&G?w'A+l]n LL&Pg/˚iHqD?=?)|p}4D`J*(i3>@c(&c*BWGLɳ=*PăJO :n_/z E_7䂄㤘./0*K[1񪡫֟~=[b$oA9q̚#sf88ՠqkuD"lmq8s9m׳cIvZg*gG{y{]v-7G̨Q胷mǯ8bmܥjzU KU{aȞ)<{U+ն\^{]ЬsêR k?AY7nnzJ/EfiGyĒ: 7`L//^9Uv~ VXR1ĺ`$Ww([֫@/n˗54 ŤDC>[LPA5Aν "Sma+_%`qU@AN'EM891Hm7_o*5n%cp npN=09oSn+x/?FM}fLHg{ {_T%kݝd{1[̟ܻsRO5zy eо^MRs̨Q$0{7A5GFSA6teӵ⢢`o 3x>00AWXCN)sF̳=~mlDAju.GjiILDP۪o̓'y\x-;e~E:$9Uc %Q@9/sx1Fh,]LnThWm@W"M2XG 0Ua[NSO%+Ӈ;ݗj49,Ano1;HY/7H !V]3$nr i[u/?%Xon&jjX<ǩSOyQ12~Nb`hHtp,W.Bï(U_Fr_Lf(!+غ'50^8e=P:°?G]b5䅗]L_HxS'[X74U_zʥ\=ATjdA$d+\:O/]5E۹Kx]l\[QsL {M}<ºV㋬Mfb"nZđطtvYw]7Rعiɑ3b$)zY\σw^`{D35HaL_y8 "E܊Q2Fa>%3>BZ bR~D8zypy9i㋆ܱ!}7l(87aTo-_GW:a(?AqxW9:+nJ-D^[ 4J"}*Pf0pǯ|qq(bWR74gc?WeɋmnypCGzXmm3CT^C6\Hc e/0dw-gG[bb5c?1H{߳0*#m\qfƍɕ!ˉW%[/_b#JNE\OWXJ_`d\qiKb)/y//|w4kr_xPnZr)T众/ i쉯hhޫAš*5 D!??-_϶';#pz>IqD?Zd>1EE[DдZ c4GDa{8˯ɉ_')-[31ӜMfPBt!nrMJ "!% wxMϊz~߼=*Ugƍk٪#Y Sl4&f݈z !CA MLLv]N.wv yhsT{n/ mnDW[ >yNA4]!S/%'<9t*ׇ8)zqjb~ jGXP?_yb͉4b'6&jj8:v2;Ɏ0zvQKXȺ/mh?_}';% 'A$as :+Zukx;7GXX%~q]gY{(ObI)y=Q47|Plݾӵ0:;0p?Gu_Rrdr0Nʉߥ@< +K ˩/^p=T&nP${$q.\ėV rC<5Ze f]eNrd^0! mih x4Z0iql2)H卓bBw,&pN-:)yKkX0JW:Lc [:kG֐ط/Lc0ux4Ji1:8ƋoAor}Ģy}_uK9q6tvXe(Pu@t2 " .+J8)>LElaL6kAFʊ =8w݋rd_>{yy'tCa_of}L' כ_|f ?7?hv=h&ǻ^/[ 8&;pC6t̓'ɘ0f~},{X>k:xv{{ׯaGXپk{!$"޻e;3^(oʖ+yG'Ȱ- pw/wVn]3rNV]EsBox[mDIpCҬ^1.Q4d׶523a6XJj[a8x~|}9ŮߎRO}\LDd;.ηPwŷ٠)J]R⇗~?~^:;^֫1Z5sA^ASLWqֳb~%Ad(Uw\i S\RMSLC9( a?0ATLEX˱F~ Fִ5jt@rd oLLP?_`JOr՜iAޟA$yC,m\5mMEd"Y@@5j^C\{cv0FF@lfS?xȑU+xZ\FO&$M<~3Y]ݟ:חj[L)O/*ץ@A/й YlJj WC^LP?oP0“V=1]5*Jڔ] _Ax~ҵ_I1B۵8A(&6j-0sr˭܁:o-|@ j9aҵb^^(V. EQ١~.CAs Wϙ?Z!Ɛ5Z#ȷ:i)(z1T4[s,0L km4Q헃`G\n(Lc*3n& lGkYݶDӊo_DG5qy ;R?_'oٴf%0G%=v%5%A1Ca7^C` Ҫ0ZN+|+ iH`燪x[uKU{K[Lw8+OyXq+d m;~{^^x-ɬ(jBIs"JjxHd>p mOL$}Gmm8r?tТퟑHdafbnK&EW 7jt%A髙(0`#tuNC*c`8Mc@*ƐRU?.! 2`|_Z=}];x8xS lag(p,(}q~2bJRfvd /?~q*˭ dڝkXu8ړ)S%EP[7xutv3kNgbb&Ѝi/w:}Z350%yJd JX%䫃FG%G ǎ7I k>~dۉǎ8lQ \{;fc#RBpt҆#QUPTcM Lͨ?VwOOO<[굾իz=M&%VHxM@!~.y^x05IIly?ZVذ~4b) 81˨$1:=fr ]ASN.?l^N>}'B)U.LJ U,F4d=qSM60]#A]YbaP2qΣlQ=[^z/HC^=-('S]IoȘ S`ߡh)ZkciA%o.#M| YQRY$6d%N6Ҕ8ϢL hL !4pXQ!̇Zr9Il41ݴxzbճCKI|(q` eBu20eOfS3=%JtT *PŹHRIZjI ]|IO= H0 IxqӍ_)™/i)2\'i2Pq$h{bX(L\7c=;ߋV Cv\@ t47 :9 RcCOZ=D,b٢06~def3 JX5q^Vnʠ|31WV?};7>~"͉pՉ3S$ S2{D晕33j햛DAFGjkY8pu5#2#Q!.7Qs9rPBAdT&B L%boP#K_*21j]r #> dZOr0BLW ùaΟP]}lc9}uWk1W]Cs$ʧe3V^]'ǭMQ%f;JMhS_=#*ǫ)6<2(ֲWÒl9/w;N?&n~rmvK'd9,X=qeiEmm$e|ks弧?ȸgQ, =]J ьr 8 ˯\rsߩI1)-L!q>q3uxhx{K3}nxDR2z^.QS] Tc ,;v_w|?uM@qd3NoʧA,FRy-+cNY2r[PH)C_2{-V^ϝvԼeŒGޢ` @"bhDaq7>d]GU ;Y.&"OȒGusC+PS SEL ~^ɗ=tl7'402 YrxNe>iBZJv3&Lca ,eǾ# opSMkó1s{bQC#H~c6S]byB$cbx1/  R Cn { 2 "YCm􍕕YiaQd-kb4}eLŪFiCps|2Fy3ך:W~%L5/4!# *8r]_27,=,NqCp0Q _ιKc1GCPdž-{Gu~L3>osokeLBahhq8lX±O.8/qI76pRS]i,&1zO6;w}Ȋ4- X[ݼgߟ >D 0p6}`JbH1YLQT//w/_ (vtr4N/!|o4Xq)&3|G!rW"׉W/TPl:a 'R^Ј"GFNeJu+:97sk-` & 8J$ceeZK_6K\:z]5z Hl?+ `N=D^R*I,$+np? כbI1r5I O4\ j$W.2d2gbsoY~&oܘD 40썾3}s*Xv|?Oo.,cH2Ɓ7f}~N~]K} zLC*ΐb7x8de:d~P캚K{굷9JhA=Ma6ޱ\Dz̔#q"g/Ũ;ZJg0y-UIj4a1ܽ;zNwX 95tǐ2Ktz齸*`2[-d._}j<䒛%?x(DO ˿IjZM1%tt$5צq1|xM|w|h!K^AQ۰ Y`W/ωE~@ @jkidu ?O[oG'ô]WmFC_ 2zr;gFX+ym!V޲[tM E.q12 DF$`\olohqÜ(6YMB'Bs);N&I˗lF+㽼 "*"烞J?06>,%xB/ M,D< ^s|.ZXь2ws"lL IDATq~"x$'S(s_0JU \ηv]5 " 4r0{̿M8`^k+ii 1-\{;[2>Eה3`GLj|,14ށ>~bs Tίj1p%1\)K1&)ݨ:|,ic10р]rgQ*Sg4B"PJb$N4:B$DSUdJm`-D@_=-VܰPJ9]QIrԖ|c}@GJ3+ ƹo ?:,psN&}~(J7a/\ar+%)YVյE!qC2ذ )C7ܲ6_JwLvnТb3zr ϳH(Q8?@W$nږFJB_Q:~Oyu|\J%AP-^ _P d~T) .cGh/<2< 5@]eEOH6'83̮bYD F eWR?vMZ*yN_H^d6SWIq1"U5RZ׷Nv GO}^ύ E::6urc'Ј·} ?P]+'!:ښ>+;llp ~Mu%cHHR1yܝRfLQd,셫:8 k!2KC؇D-qnzqdy?}kʙ&^_+ŤKbd棐8~K/"Xy&bf!յƇ>g.Gb܀"zΙJor 7YךZzs*"&EY!\W)+xˇS4E2뾗{(ɇW8|7º:!b%OtuԄ8}I~g Ⱥ:0 u<*R;;vN(E IbVPt?bkFH~:j]tUl:KI^ }.-rˆ8؍{ jC~k=OkLE< zzdEIOf_adsx- Jz [wЈXǷ~~yY 23o2=E(Gd/5_J_JWvu1F,Ӊ5~m40:^'>@G I&_4*mAPpKU55Y>sr)0+/_r;e9;pctuoG9't/>$34^=9&vϜ ~ /~M~VzzoG`wold5Ɛ)[#z !# 0/R˾oF2FQ+ȳ[ńgOD'' 2D_oDD|(>˛NbtLzMg;u`<ۙ)%- .tP|H;(qi(-$QNy8g!'2H1cֺ 2q=<o|M"Iܲpecc,y<Ј¦jh\X͎ǬIҹttR66F_ig/3(W#P ..n}ˎ=- i1-[vÙN4ЙY)#IᏭ=FvgN6ʏ㞾bp2~yyXy<ӎey)۫ Kkڏ9䛙ne͎3N;vMNW,i "OzN|c٣cuuTw|g/er&Ncne^n4L^[669]6Ҭ_{8;i˴8i"Ԁ|eeɉx㹺F%˽T䒟^RėBdmc)ӯ* $MQ!?k7O$r>z(FaΉJlqqT+WnnPb)oYF5~ -.xI]e{iޖ7P*6}3QvMZ2豩d58 / 52a17Sܼg|NV޲j=ȿmmcNޚ-<6J]fީYr1FD( 18b5ɗɅM-WtχSs_h8?u>oV 1i #:ћo4Nҍ4א%#MH+媔xiǩ[x77;o*ɕ@B1FȨT>d2J.D}|;&oê9>Q: ,w`(m2&JIO՟}٢0kH&8u=e 6Uj IXoNe\Z7AwPL4:ˋ{lK19=,6F4 %sr-mdt$U8}dыtvcxWב=Cn ]`TW2rç`ӟҹxwNBJZf3iܨ)Sߞ*c0. )HGZFP pt5cl׻YնqiE.63)}~1ALYd2eqK\Ln=7cҏ=0^z+I{$v)S^kI}˼~ٔ&/O<Q Ï$n~ʄ~yxnn2ްBd7ļ!\M$cʈ88 >rz͓y&)h'MKX޴0E plm7L ϣegr^|̈́B~66r9Fpu*Q$K(ԟB)>S1ԖxZh؉v:+ ;0Y1~N(*=G4y[<ēAlXB0 u|Z9DnYRx *(J2C>rQx>zLD. yNrsѭ*8nx sjW_Z`}998n+xj12$\!qQtb[XK:4֮z9a`0tt<WzYqs˅B3sR^ׁnν $YfxAQTQK$]AkaĘR>D4F7Mht4E,jX`fL*(S O6Y[^zO8rSHɸR!SOUTS8=EהSf9:Ɏ}G} ?OoMwZK?sh=VH.2Q(tvUv{*Ft;d˿/?^y 2ݽq)+{18*7/=Ć1x"HUMҽS3n{Hvo=z]Cu<w1L(bBzǫC\d:U:߭.t/CNT(q\GOΏ=`?T66Ye L'0<{}hѼвF2GKDKs- y[]lYJ0,YJ#gX7 3sRh& B ROVHVfly < k絢I=g&x{C9}6-G }}U4&mc1Kf $CIqz{B,oywH_}ׇ4]˛d5S^2Ӂ>D;,ʹXJ:XL{{=TF/8O]t*܊c^^dA7ѾZYT[ؕNE{;%RX_ͩ!X*w.ִezRH[j4uT6n00\p,0011x+9st|am*\Ǿjut}buOmB~u,+3RRYN計yAQ=A&a$ӭ"YQޘ1de v^Z.CYMder)*%Z*CM2JϧQ"$jY*?eSLdi9ccMG3 qi;_RtYv߼R:^5vWNEeu4 I)yg.M 77'žK3(iB`HM>?ўO_SN "~xI1W[޸bQi9D2U8x(D1?7@EMpH}Ϡ/S][IZiU>0Ks$IlBr4^/2*bM],ʥ7L9?KX~â cYIT2vž^QGU"J\V3?rޓ({lMOO_[O'zA#$FrmL%h+p~ zIϙ&o*=CBa\Eo*n]"0r%i0lcyxUnr-4J)L[x\:ByqK_/ /뀪hmΚCh W!˄WySvEu[ѿ)Nq? -'S||.eɌ#.@]UTWuq@Y1QT5E<'2ؙ:X>P5Օ/1Әal?!}H$S_ӫsfOa[iY8ں*f4 sWJ}4KlXْ-24ܞt6p+_|(DΟxX};vW"x쾍d6|5rM 3-29&%$H@ m!J\C ̣[x&-~5Ra(N@b4Qy&x<2JM**9>9IHpz>v-"6vV?O.7[uO氖I1d.O&efϒRŪk沬]oBz][A~m^!q{=DOαgv4UFQ7}nY7l|o?{ Y2?8YjYX+ .C8ܽ0Kh@'y``/B(VR{)DH|LDDCSl9Lej/hAE78Tv} MCJFjmj$;[δS @?p2jzCSJ Zw?x$[7Uzzu~G*)ŶKuPKPVEC37?%/ni!v)Ɏ~˨X2__[Mhj UUYǟWtl{y!:ښuI }}bw.G8|x?eǾ#T=WnXݷhTU_Ć-J_o'ئMyB$Ά-6N@wF:V~}OlOUl@tv&uGdJ)= 70p  tдH KQ^6veĚ]=?DbG=u 2=fn /eJu4% ! yD^ˉ0=V yӵow.o+HZ9rv^sz95V@k:fT^ۍqIK v|p=GDC~7Y^oCyOQ_[I(T䛃™ 2F-sW]It:[ (|u8s:Apon\ڶXƊ.1  4t@g n!AM􄐙XO_):VbJkQIbM2uQcQ1[Vpyܼ#@|U//+I$[<@䋎$Aэj~=ve 6o%|Ƒa<YQش&^s[LauX#ƦDy.}s.R!mdf9M Np`Z2VO9몉'X\;ss&3FQ~^8hzY~솎Xv^M:ixYzۇu^8]^L=˛)*u4b4*kXadf?sgVH|SHg'ns#_O5+p<F.]YuӟlG|55W|x/N}x"Ord_l&&qnxߚ,ot֤ΫgygU.yf^tk&b/NҒU~tCI 2`\3^0dJA!݊_3Ewvn 2xV6[ $n,IHum%ȕ ~vG^N4X$On&zP8 <13L"7ѓ)7w[ۖ"!Y(g=' tqm9{O0ufQ[ֶ,^f<{'+mhbxЏnM0ls2Ο%A*mhvZWnB—:YC{wcrO$в6}rxh䪜s1<O+WeU$)xW=Ǩv$vklq7Fay:p&~)#WvE~ץ@&ZmLdauӿ뇚%_3F쒞^~0hfuӏF][̏"ǩ/}" j?v`\+xm7tggE" $H~=#Q*tҋU4{ oh~4Iy 2kHк-'}IlOo={6kM7|1$V:SQPn?1dUbMsu'i^"6216_6s[cL,2 ` K3 RI/urRJc4JcWײ0r5RW^Y* *s ɱe'_°cKNhlw_>}&!!gPǍ$z>>5ĺ ~!/2!)ceX,:.DoCw;&)29*: ؁"qÀӑ}]͏9 @ `XbMxm߷=GzZ 1?W^y@(,I1rı+O+3}N7Nw7o(D6xoNp08}W^y'77[;1(*~)_rYNVci,X0YHE??{3k6Oi7蘥&}/]7k]w)$H{k+XנKsDb(2^Ek 調 +oYʋ/c ƍ1]L{ec(6MSWHm 7Ga#ah\I7ٺVc"5Gg5`ٽQʒwJ.s5`\-X y_KVYIl)0 ۻz&8q>=TW#ǥ 'aTE\Ȁqϭ|&~b7E+-mM<.~F}})BG+-=n .cSЀ6Wz9BhAԸ%uXka1d 2u0cg{7Xϖ;-m1,gqcN(*K\8>`ur#).eޏ_0)aYJfЅ鶛~1lim-Y~FoK[ש ^͍ÏE!(Q;?( p(M_'vV,o$I}sDc-T,.eOlN%C#'ʫkF PU.Yn >Js5qA.u2=EV ^ZPo;KWwKȣ.Dydf 6!' '=<ɛ~2˿4 'l5FϛQ?UPձL!${2?r\J~[+^! M(J b5xҖ/^RjyȊ|Oѹլ}j30n컣u+?a#{wc:a=`%{`+IYL}þCG=L\>ܻ{/"r<1_pXiIXqX??d(r ME5.nfavgIkYyRvṇDŗvqgÜrxF(r`&bhYiTφqEPQ2Ziml$(Fk7]]ȳ1_nv#.n0?!:B$~M k3`I#Ł&m&fZrÉ3]&V:U5S{nϵuFvW|0%I.?o>?M^(%^_0 F,A-HxѐPtSDFlm'CYIX;F<7PGg *S-3B6X!?4ƕk8w=%c+7NWE"(GRRcg2Dc8sݪlxt5h|80cR0oE`E\}f'LPz4dT@t~d.YGuQaW,fR0^J ŗvqI>ƅռ!kҖj@_jbE%cb=O#C3'?Lܾξ[7cc2tzҥJbu֍b{{hj^"bC柸A A,B$0~j9s+F >zTpވz҉(IEA  }$0/HT4ӑ@NGTg-+NnYNyؽq1mFW%1ކ) k3h4 ]? @<;wMr*$ĸ}A7. ŊýP:8w߉5'[yF22&Jj7ʀkH5i`JRץ^j5r{N%{sox0ʻ[t'}̴<τX*@ONWw~,7x0ң&I:S8.nlP,NSO8?˲&$ S#+ K !IfWJq 3M{WYRwG7}_\N};_(>(4e7Ft0+8ltTi I/}Wh} zxyggF>[/v_f>Hw@Q!?$NyFyߤABLǨ+܀t}j#kpYJ ?hD%Ǒ'cQʀ,ᑇw9iz5+S^e rrNMʝ.=E1?G~4Ã=ssS gdX>iN3(Ţ?)헏B+7y]c ]'VbruG[8Y2 "7ǁ9D#j<7–Od!FXV!nQ{Cv3Tt{*;^bmbh PEES]V7Z.՟kׇZ֮>}mjjbe~umI, X R0Fh bbJ$^0Ǚ;w}IΜs<|99hva:$ ?:('+WůZ_$0&e67W_c@WaD%|r 7:F={N͂@C߯e3+xO+L$t8@b:1њ\fdL{@e w?lf\E\}3 IDATBDUtvp;w Xv|GSgb̧KgY^Gj+tKܑ-t0RxN/EJK׮0f˲|QV$ ~a9a.[YKybZ~WxRxłXI5bH+O8}l(FB2# 6D{'F'Ɯ\˿1@C\YNFo>H)n+鞍t:ъ=l&V+gHއC3K@p[o }f[VА5$eaf^I!U:$T~j<9êX- 'NMS_im;},ò ( D]ndIӰ#JO?G܏0x4BmHD 66o'~th>c" -6$gTMŒ5qz1%0ZnֺԻ#gNx=y :zRV^m]Yb#{NR8 ',{)Nh)މ&8mpKc_TR%@ BB#F "rǛtuWE6?? ʽbުz$:x?Z/E5IHqz{x⹗fOmy;£߹HS;I$ MOGc m`XuVp9$wm%7]u/S0-sJ,ʘDWS$ҏT\3xiv)A l!0=@[g?W|a*gPxv!+jda P& >q"|Kh+ӊK\F=JߺV~\6y3A>*~}c<D[i~Sl$z16#G "*L4!T"ORxv!kɟ>vc7{o$wNXgPHLukֽ.GV2qS^pr.5@e $JZB?WQ|C!v4{뚺 旂>䄘%H c>Ox N9Y׀"pˍD# kŇLeΧҙe'@"QPҰ|9? /ǯXCOO01*B"{c3>XԲ2-]ܹbVFc.;w%Q^#S34 4LrX*0GU sH|YMdW'@5̭P\qxD A7ԀCi.ET{0w(pM5ճYYYld, *cOc۫mP>>1ǮV]@v- 3olzYE” I:ٹG]yV(`eU(HK:elnOA, D#CUCUEO?B_gkֽH1*goHY0zuOǁ&Ok.6G?w-$;~}4~.X)rpw ָJum,pzH4%c_#9S٤מ0FGSd{el`p4:x ?bB.;N,8Վ7wg44$}6.D/9JցĎe^b~(6oabuLP4̙D@Rw??{1E 2뷶{Q8*Ş RDQ|ujJΚ|? D`0$:۱z+h Pt S~NZZ#Y9[(QrpB?ί?H> @>\P_KeI%qgqknӣ;7"G[XwFI7ŗ~CqҫIDJ{_疳|R@}h -KXEn2oYˮe݇)+ȕ Vf}5hܑ#HVvs6Yug*DҒAzRŽ>EֲJnCttӰl1A@qԱ~k sv~-Nn>F5i_N( oI hjneGYyid20܋qUc&ǒ'd74W:>£߹E@dY6d);cGR"{uά(h8-{h\ Ź/#"X0/' X0 ‚i~V?U8;fo`,qGq4,Xg½u ùc**Vp9uInV]^HHe:UAon]IyԉwU:߫=gn8- VƂl M!\K-¦.#N.d~V?>zhne4d/+efUpキki`@)Z( 9(vaLa$$.lO;Mͭ4,_] I~UO@eIcfSJ,͉c~TIԭ1T Tv4E;}arj O=-N͓257~zyAs1 ctfP-h$nW%X9*t'e`N6~ ~&HE2q"KKKe3+/O'Ġ/>#qjjfh8mմm'~};ux~C쥈9صIn=WXҕjRӘPeVtKzM;v N3]_UWHĞ#0a4=^FoJ$mJj,IX/:ocfZ"/=©aL6U5m|au57]W?^8W }WqfO%{5$*E'GaEwK%h4ʭ5 `Al͛y((g{C=~_?,ffͬLO:SCI0e7Ze x-lBǞL]Фv7.Z\qɥ xmצȬ21_$l_LѶy6pHyMyDA1sዣU)_4ϗ0V1r|4 /[lD_ ?AMleVQ*]M㵫OM-p$Ύ%v In |B|'!ֽͥXqU#)7!1: ^J_U5 EC@ E"PBR>Z,$ǘ16ٱ)TV-`37]WGӶ(UI|_ZV3E ƽ ͭ"a@lh.o&@b iq^͏ӈ73#X8CϽ-h!hXPEcJ V 2+{Ye"qjnu9% H@~FS2Ek!k]:8WWE2#y-]nnټ͎=N{ĕ1# !6 T6#X3#4Oٞ8.F>X">Eܱe_;(/%nGIStDtK8uc< RKHnJ6aZmejRi()4OLQ=["YE4AOL,!nŊ-qe290lݲT wY1%_z(ȮU"_Cpm2 ̞Q`*1ZEhFc]_4~p׍llζW^g۫mDQ]8tu"q睟% f><<Ɨ5/a=DIWUNl ̮=̮,6[;L306lv\@m?HAԙ8&itz6|vh E$=h4 k20O'HMIE@<cwMi|4՗oܼ1*`'U)v6NadO\5\rJIF Eo+As#-MHp {GK:o4j+~ <,e[*l쁒0{цX|`pY}4,GiV G`0\:kݻ__I0] RS]2?ȲjpZv9%xp?=]2տ3bJ7n^'fM75p+7]W(l޼]Z?W5l7ŝә)_.y88JXE%hUe~)VSwN`Q J u@IhϮLÒzqBb͆l&F{e4l9U7{L1M[78PcFfkF˔АʒJf5VrO]mfT!Xuϊ[87\9AcRزsُ, >9ـUdy쾨d%,O0P?d)39eh'Sgp;|_!dY!{| D}i9F4*y 8!Led (TO2cDim4D+?$ll9,6^}5{7p4*kQY;ֽT 6eWK+.j Mi(H(~bû[戞#3~$ crg&-eDw Uhy~4䧏ޫƻDXCǮNƟejc#pr+.زsO7(^)UO~A.I@F6ǀ Sm:2u##kzOz=-avzKh_0 ?VY7& {I>6M%dgp*cDe,pKo3p|}9 mi+Dh+@ƪD%Jn圊 twBPFQ6o >"i$8Bbg$JAH5LE@㵗ؐM\Ʉ kShLK( ݬ,oDʟ!#ɛAyׄSMԱb?WSԃѡ$%''h$hH(,4aCFg?~a5Ł/$w'hXst G-zsEwIG$}E4Ƽhj_~^ںㄦۘ>zׯc\O>MCMZNRحN5dɏ3FaOqS4+_9Yl1CI`[4io{O(uƔ4kDth-2fR;V{hZi<لss7&F#_ٔ- ȏ[V8/ٯY4۴ݯ{$JK5"C\V|f4qO,>$n'{:FЦB{|Nro?rzVnL/*թĬ6t%4OÒz|6s=S V܄PgX#Uځ8-_F>pbfC9wMu9vY5&7w+V},ÚlwG6{N6ٖdMxWqtem ,~z6s/3NsY}U/E. }f%°t| O)ܴV+9VbzEp8qӦ Ye/ͬݸ`4 !O DQ‘8o}RʄS:'M$! 1 $M2/cCRxH% IDATm$9c 7㹥eĊ'7'D*nq܉\Xd_Zټy;qJʫKD\2LKkY}唕U~%ܼ& ,+E. "=Cɻ ݺT֥-.!+In+$x BObq1Kc  %\Sq*#;x4DȩUD6~X86"Xq ;ͬo`8Fs[ rsnެьiO?έٿS;DA3G9O{°2+swTMgO\#+/eC6X` Dc!?{h( ;z])l{m/7+Va.ITw>p;1\w 44$f:>%/r\XGnG6IǑt\4]~`%tM VY3>P$|J"KKٹ8OVNRt*aQ4>CQ4 4[HhƷS \d%4ÙcO.}iQXS#Mvb#⣠yW;C"̬m\J7o2 T4MZ2[g zH~af"oG9%giC1O>hk×/ UK4^nIJf%us82C G‡YzrbHl޼~*gm3=LӎfBN6{&LSq#Wտ{Xn MK-$>u*Az:x<$.4UBӹt2 j bdŨl_C+dt>AǗ3@Om-DPYP R7ٲsW7.O @:JWw&+.b]W #N)]E§rg$a agͬ`VsXqpnh()o2ژOUW|慮֫:D@{hASk;3K̉[T$drk+@J]$TI-Sc?и81ݫ@e{LGbQɳ9p⳺;SIebEdpDid Gty7=]XjC)  hhC -y|fn~9+W,GV 0'l: UN:37_r""Wl(ARuO1x(A?qV5{2E tK: LA$>lj|7\NcRAQ$~ĨHͬG߹4P/wiɛkU |>+M;? VwônPPRЌP*>vѲs}(:͋q#~3 v9$mׁ>J薕`l+pWN⏿>&T)J0'! !:7:q[4[f~ID79 /(23Sߊ=lz $!9`Y*JhX0( (/: ^fw۵aTV/e"⦍<~3!\ji<ȳ C4VartӝK"lj{4ڏBߕ$ۘXX|?q zT'd="4P$oi L#4OMu9 E.#Ž2.klhtb?RsjW%N/˄{i;Ýw|4ͶŲ+I%[uHBWt&Sevy7۷D3i,Y\ǎ-tkt( j;t+W4P_]G0ϺiY65'˩xu6V]W.=_3ˊغe9,4}颱1%Kj&\k2JS'B\.Nd .t oaR $) EHU31GP~¯'7ANx HFBWĉ^fڸ8Z2 UQ4_KXq']W,tfXZJ&7pJqguKN1+"w‡N2 D?'ZG4w]cfx IfVILDɳ 'd@  .V\mY@(2E ̿bns6%LʜՑ$sI@SJMO^c!7bjtyxGW5Dh!{|PXbXxߠ* rlana;'Jx@cEˈ YE|YE\w@2bMIn^CU?K%lFnܙԋI|\@u.N5N-SUD` - NEuiȪD\k-{ <!O[XXDB/ 4 ؖ䙣b$ '=ĒT1H|4RfLj)V}m\IpPtCGɀu Qc $PƠ'Xv0]\u(~NYʼ ƌU7D+ԼOXt >ҹ4VS|)BAp#} e;`PA!ci`tt"<gYCC"dv\#T.R쥠h_yW;q~KG$cTg/&!֠idaxҹw9Hm@z-65qфdB\߳ (~Q/Al0vDcپm'eLkhgEP;Mvan9\: ΕW .OB =]ШwlEvpqb!K(4#%;ƀzdZ K1";кwݲ#=b6r͇ݿ9Ŭ$Y:oG<26g|2S@Ĭm {~2ġSpvpkUycV3K!P у b!'{$l]tTۀƕ@m/7DQ5*س[-crणr07WDhRVVV'\{ HtB!:>H:+?P#b\$(ԗT$ l&$;{{ݟS US"< uǹsJ5y@p+x7~U"m{R.{`vcͺYË{Jl6F?*y `漎 '.cvOokeْj.qV+r陥o`hQ*=V^ _e3+<~3>|}FhH7\f8YKh%qVn\E3A;}׵, ?)J1dwb)'܉&/ʺj(HnJnȳ3m><Z͡*=E Ce4r= qu .{r|?+tX&KC&p;ۚX 6fSߔCkCk]H,I 71v8˜G#nwL0lw/ g Xq'_v抝%;*Qx ;ƮC}G3OÚVPYtz=- Y"c۞~mqO\i ªZ\u=~YAuvL{@tƒ`8pOe/ B[>ܭ$E1nxVfW"3r20-fnw{1JL 2o6L`dAV׊ng>c!an7qB%$(k]K>V% 13ZSe~0h\tͭp;}mD"#qc~r^q)Ƭ<2Omom> LQ(PxN Cebo+)ʏێa/L/8mJBNG͒O|{<*&tĪ|TX2-{+`)8YXtN #lٹM+9U@ƂӉt$ k>`Kh_lLk%`A_ Kտ?aoiwwi( %Y]Ӿ')7\q$7}p$b=P_K4\Cٗh3oh `f1ڿ3~3c,wMAv/*F> ʣ߼4"Q}oR8'2x @8oO1ˋ.gqmby@e>n |p4+Fj;MEU^&q Urqr[MBEvItxioI9lHR_b*1z)4mLypkֆ[\Vr(1'wg/A.1P吸ߨ$[*e*4čcMCmO*R(ҙB$ Wt(;s@iׁ&KHRRr;cV<36O=4;vpÕ^WC-mʿdky⹗zcBDcCHS햕_Ć.yMq@@Cw\gn{8,QKP@(oʠ%Z6?U. `mLNϝ7@pv?VIud~%M֝[v$VˍhҘjB1 غe7VW ࠈ߭ X:  @!rK!MGjGҦPXT{{{qd3C&-|Î^[3ޤ@)Cf*BØ!r pW h8κwk8tN"n0!59c}c8.}U Ɨ 5ʑbdG|̦zn96&vllқ?Y`۫mAN0O8m|cH;{q?Rߙ]Cv~ ӕ~"aX&LdhY_R޹03|Sc 撽iPM%Cq2@{VV줓l{NJO)nX |1# sfP:ʏoz&Fge{&?):P($ w"ӯA,pb?qqY"Sw;$tfYWΩYר-ɒHS#-d)NXX1ʒ\3=ֺN.\95\$G=]١8V'cpP}DƌӀammμEuάp Nw^8c|cSNcQ%IO>ÞO'n~V){NqӒ+Y6|J*CɳbƍW\`B7JG6 Nα~TfB" A R;L7v'Kz$jױu˞խk$2IOJ?ݝœ'ui}i_S~NuhXROPV0*}7Lq%7$  sfKL|mc:1'ç'ɟp[w H[ (ʀBxpۮEVMͪV6<[pxm ch/<|r}^%[rO1!ވ Cf raw?+EΆya!_ ԰ʿWPpzPyLBn,[v}"O.yrE JϻoGG sgf{eAl柠]&/1^|YwR G[Zɴj|VMw_{>Z9GyM#owm/n|%Wo_)P n- IDATq?֣)g򫸹4P _zD+LZN{s3| oY]q|TuS(H~]^l{w/aw5*O+u>qg6m4g?S%(;~yTFknW,cZ5E޷(D]]gxr[ ̿|$("pS?L7_W[2|6__kFR3vpHCg.J3OD4?W~ŧÓF7[/yxׯYޙ1Ug((3 9o>+}qg^y_ V7keim,8\kя鐱|ғGY!&wج+Dyrzz|mqy5ȐUJ9L).`ޘ. G_B9g>g}{Vx_ņ9yN3>Ü9TݥS8o3; tCޣ:E6#*pgd c]saX]iJRH>` L~ïVX;3dh'LJDO( Nieñ<.Zлl:Lw341̻oZ"{OTquӛ|Ҧ%Sߧoc3+j*>Ͼ== 2xA9qcw裆zc@|:EQygp{ M@f1zH_)Ɵ<<<<<S_4r6op5an-#~ b}?aG\9L b:^e;0?8R}63{QwN  md;>g sR'}PUߟǴ}dիϛg^mg6u$SNcw)8)c˹ye~/Bμԅx巿'q*FNѸCR-5?ESh>:wwFx@:W ߢ#|2(V>ۼro>5ݏ(/|+ Ϩ y] ;=3{GU|s,p]o QZ2yGKBq3{jB.}"vĬSlY/_ebO<ɿ-ǏeMd!/ѳH22*#;ey5^ K^:]%6ryWxxxxxxxLRz6L ^²"i +a*;wnE$XbD['&:ql[c''Nd'q6q|M6޸[DIA{-0E}ι;g^=:t ]I<˖k|n@ /5Ã#þ~UMTz y\3j_~ eo䥶X3X1Y-9HDD}cS$"""23f=E[))AIHDDDPR$"""()=Ff _MuOvn͟ʷ qwt""99=a9>!}%ʧNWڮl| Oƺq>J/kunfEǒqډ|aC|&G苕Lj*Km?9{3Lqq-^#Z%93Xp;71!8XɧƾJDD$L›?x bɥ*+F ]ōvNg4.Z3 ՜oaSofh=f/z|7_o>'/I|5Dἅ,wT6=Ә/{2H⏶WVA$""3hɹejBN,?͟\oc>.1,_H9ڋQiW#on]G"<8 };Iq$tzx1;Ŏ+bfN{.?&vn:[m3xla.?:~E~jߝX/W|~YNO62}-yVI-+S\kwD<pz:WgcdR9F_9_/EBs ϔTRgN~Ȥۧ=EdUmxhk=v܍mVxyrkYQ~WC탍_TUf7XnBNY/dēpKσl/Z7Ym~HGYgjw!(~sgv>ʼn,TOw=͗gc>wꑤ"ג㍃)56>\2|pC{^gRs0F,[{({|>mx7HO|=isXbM"RQ?L =|5)nn'#c%䥳G˶OýN]LWC%ufcR#=||Ń&jm1SDM㙘'mLJ3;xbM^~oj4 \RF,ov^vdLٔn^&?4e4k|<;tefE1} ':'voDj0l})#"kst$(Ǥ϶5EYm)^V=`(wfIQ؂lzw{cl<źݧB慨U:ڙ?Zn'UNOeC55pc8""Iff1 É @4X`chv0GښGb)"";uCǣFr&uk+Y;T&`aցauYk=2n?1yI7J'-K)Ίewi]c\p|eYJB&2y!ۧܯr#,H"oT&_*3/[o{q6؋7&5}NH&ÀtK8 ]<X>L_?v_%8Jw3x?5b|쭮gނul>_Ǜ rW)ZwSܮ0'E7KN['=u&2mquN.-؅|'<_EzDDD$Lf,)MX?,  9[x.[~m8݂\KO/E؟>OZʏu(}9>‡tЏq\oW'jNc#|&eoE'%J"""rž%%/矗SG׮`r]{D8 .n!'x$*ߝ>GEDDd›|v"Vz|nx/  MPt8?_ë;VuӳE8l/"""23 v{ڦ:v6xZS$"""rp8dee@cc#UUUgƞ>deedee1q3q}(--8:::FF{ƣHDDDW222 -QJ}+>a>@!; o.PGbDED&|> -rv&gNvks魸{cZ7~ _Lr'8C= [D> 4o}4M c=sOg|< Kox-Xw0c!_^ܺxrS9&fKhR`?EyZOç^巪b~?qF|6_jdwD[kW񜻃:j07w#4>Vv+z^+\E58)E?v7t(KypLX36ḵ7|:kK7\k>,~S>>R6ٺu?g֭ ? os ^ZAEФs4>1t~#*~`S&e'ًy̎p6~T|ϕ7`+[,i~*\|2 ~\^OWhDߪ~؄oO8~cij>_T Væ~%#u^G O ӊޖ .zbrp1  U^ƙqfe|ڛkx+yח39V0uPӜ}A&7eݝ6%yt7{lk0q,rwrG,[6Ogbcq٨pF 6󦃹V&?m_K^z 2,2kߪ~DoO4~Kfgpv 8\1zdFGN:q,49=~\^nOQ}N nr\xc7DOft%h8 caǛ3kO|*?/1c06m; e+F^k>b3hQ6cݑ,pHHlGD֤zp9D8 .a!'r#V&f{+HJ^οʩ#c[[NYcmkaV&\~,d_ua|~_[v^H""^>E5qSIDATc%._?=[OFopEG 0&yʄ7E8_.n9#YՏg"F$͠^.LmY-U ߪxV \|tcǠ*poZJ*(I,n޼Rͪ8܃;ޫGDM>}GD_Hn#`lfVN;cov%|a!OLB}mTyaa_a~w(i5bx~6> ƽ E;|Fv5 PSiݼSZߏ< `?G0cLƬ:87 X~]q 1o[b˅:jsbnJ[􉉉߿?{OLpeUh̄`?-$\YsLZzCy"ySZ8L;ﶄ 7ZܧHDDDPR$"""()JDDDD%E"""""@IHDDDPR$"""()JDDDD%E"""""@IHDDDPR$"""()JDDDD%E"""""@IHDDDPR$"""() -0"k -񭯅d?CKDDD&"NDD>S$""""@IHDDDPR$"""()=;"""rS$"""~.G=E""""()ᾧ=djr{wZt{ - i]o/""rSOJDDDD%E""""@)~O4HDDD%E"""""@HDDD3("rwHDDDoIDdzS$""""@I{CD?>|hM"퇡Et7ӝzDDDDsOLgAv=}PO"yљݥ]HDDD3={PO)vOt˽A=E""""HDD7ݞ^"JDDDD%E""""" -mZϡcgCغu+mmm$&&b@=E""""" /o4<֢9d5ÞK8:Zqګګj/"֤MX7^S9:8-Ljjwxܯs6szC!gyz[t2ګW()jCmS{Wo3S$"L_^oBIW{Wo0@?!"""̇ QR$""""@IHDDDPR$"""()f\>4 G)i yx83\8=j/7\HT!JCkψb;I:Ǚ6QyViyj5vq>a`}ݽ3a-L?9Ρn^}. Ro?(K}GLz3-#&K)Ʒ҈Y̎V?#iĉ4 RqU*X|gs8b")df[f ^/r26I^H]:ɽxx 7&֩;2FdV`^J4`'5NqJ+Sڼ&Y{z9[$df&)Õ}orj3[WW~]pM*eԦ#&8^WvƱZ-&J~jhMDbc ʎ>NR ٱ9G<i-="#蹬YE4j&mwyB~ӭaaTz,h9l&c_E|>ۊC$U0ֽ"~lY{sfB$•ȒH>ľ{ !av-;'3-I:ȮC-crXu+oszu42mw>)LG1A 4S`mb FenD ZHQK "җ*.^f?#fLT (( _hwe3YT^wIV X]Ȉfg^Ɂubxc̰ǻ{[;ICfn:ӟ@D:[7c+0Sִ9T^4bHOsR_;+ףXxݞ^qm^IONa5iCSo̵>'~7i"K$cbbQcΖɗpb :ny#YK(Gv$?}=5Ap_:["Y޽aN :>nVng]\9>HˀBV&QU8;i17ezlX񐐷 bp;)=Ef+WJ;xhfJp}5ˆaj;;3ź7-}Xg؄?#y b8zM)ڱ+9\m}P&#~Z x,Ԝ/v4Fsneq,{OVXc6X?2INxu0_Sm1Ļ=3/w;Yt坝"UVVMpg36&GwCOZl#tU5ox<# GEA.p.ed00HK}+˲m)$?Pe#F\3ISWҎ&t\=s?ܥF? χlt%]~FOߐۇaZבu 9갘x,ây>.]Jˀ (@HOrΦȻSK">TՕr`\n꾷8j+37zw:dF(N_n#6y_O_elJQ@*>V: 0"..{I%""rsllLA[j ;R5g]sypb1~[g"@ȑ?@6"iIMel."rq=Ee LrN\ e[ZJYJ&am^.{Z璷`npQq+  Ago'w;K~xf-,閰-{o}=^<@ LMX?_gKͧn*:@xL|Lj&nF8RYZIl04?Lȿm)ά56ϚMP gclF<%͘QDFdsn̳Ĩu,>#&9b& p*& VPz} g'`4ڻk^:Άŭ{F2h#~&7=Eŀ1a7m'Fh߿ɤȓ-R>t\lňm(:# ;oO3A.v,#X]ä*%W9NdѦl\FMmh&'-v5?H\m7?}8Am?=0ב:y^k๵Å ۟Ԯ3l^H5`$6.MYJvlνmyDS 4]ٳYFLw}]exq*>i 67.:{q@A5h'q?@;'}pzԴ`޿MϤ1niUFHsG|2 =\ng 60il$0Ȁ?H08614!*%(1L񌩟W8ɃI]4ڻ5Ҽ`gbwY?k??%^z%^2Ƅh5ykɈ%QcrAJ8xMkɊ ܈aˎ+&e󣨯iKdd' mȞA^J*Y,('YdD-cVfEeEYIvvS_;±>2ck6 V_\ȪEÓ̒¹ \Jm4w$3/7"Җ1ca~G8RY~ >a dWqn:3bYa%:qz86Lj`E. ӛvQ_zho{Yh㣡xǗgvoYv;ٰq1].> nCk8^<*O 6R^3O6 U9xɥp^g6 mB񉡥o&t]> lbjD73VɌGY.w^"~&ܿM&"&Os8+7lC )JQ "R._b{0kܿ96?n[kvQ|g,7zrx`[&{Qis}G;Cl_wrܧr/UOR.,Ԃ; OepsM sblBGGaρ"_^Z>`=g% Rq*>Xhܾ'Htox;Uaz6*ZZ*v38xngLo"6YOQFF .ʨ|+.ӣtv`LtzKtt4466 }C/E`gϒEZZUUUTUUMģ"y TTTPQa>{=E""""w""s s>IENDB`kew-2.6.0/src/000077500000000000000000000000001464150306200130755ustar00rootroot00000000000000kew-2.6.0/src/cache.c000066400000000000000000000023511464150306200143050ustar00rootroot00000000000000#define _XOPEN_SOURCE 700 #include "cache.h" /* cache.c Related to cache which contains paths to cached files. */ Cache *createCache() { Cache *cache = (Cache *)malloc(sizeof(Cache)); cache->head = 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.6.0/src/cache.h000066400000000000000000000006711464150306200143150ustar00rootroot00000000000000#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.6.0/src/chafafunc.c000066400000000000000000000443631464150306200151710ustar00rootroot00000000000000#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 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); fwrite(printable->str, sizeof(char), printable->len, stdout); g_string_free(printable, TRUE); } float calcAspectRatio() { TermSize term_size; 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; } // Set default for some terminals if (cell_width == -1 && cell_height == -1) { cell_width = 8; cell_height = 16; } return (float)cell_height / (float)cell_width; } void printSquareBitmapCentered(FIBITMAP *bitmap, int baseHeight) { 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; } // Set default for some terminals if (cell_width == -1 && cell_height == -1) { cell_width = 8; cell_height = 16; } float aspect_ratio_correction = (float)cell_height / (float)cell_width; int correctedWidth = (int)(baseHeight * aspect_ratio_correction); // Convert image to a printable string printable = convert_image(pixels, pix_width, pix_height, pix_width * n_channels, CHAFA_PIXEL_BGRA8_UNASSOCIATED, correctedWidth, baseHeight, 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 - correctedWidth) / 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); } 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; } // Convert 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); 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.6.0/src/chafafunc.h000066400000000000000000000011211464150306200151570ustar00rootroot00000000000000#include #include #include #include #include #include #include #include float calcAspectRatio(); 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); void printSquareBitmapCentered(FIBITMAP *bitmap, int baseHeight); int getCoverColor(FIBITMAP *bitmap, unsigned char *r, unsigned char *g, unsigned char *b); kew-2.6.0/src/common_ui.c000066400000000000000000000016551464150306200152350ustar00rootroot00000000000000 #include "common_ui.h" //0=Black, 1=Red, 2=Green, 3=Yellow, 4=Blue, 5=Magenta, 6=Cyan, 7=White int mainColor = 6; int titleColor = 6; int artistColor = 6; int enqueuedColor = 6; PixelData color = {125, 125, 125}; unsigned char defaultColor = 150; bool useProfileColors = true; 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); } } kew-2.6.0/src/common_ui.h000066400000000000000000000006251464150306200152360ustar00rootroot00000000000000#include #include "term.h" #include "../include/imgtotxt/write_ascii.h" //0=Black, 1=Red, 2=Green, 3=Yellow, 4=Blue, 5=Magenta, 6=Cyan, 7=White extern int mainColor; extern int titleColor; extern int artistColor; extern int enqueuedColor; extern unsigned char defaultColor; extern PixelData color; extern bool useProfileColors; void setTextColorRGB2(int r, int g, int b); void setColor(); kew-2.6.0/src/directorytree.c000066400000000000000000000401751464150306200161340ustar00rootroot00000000000000#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; } int min(int a, int b, int c) { if (a <= b && a <= c) return a; if (b <= a && b <= c) return b; return c; } // Calculates the Levenshtein distance. // The Levenshtein distance between two strings is the minimum number of single-character edits // (insertions, deletions, or substitutions) required to change one string into the other. int levenshteinDistance(const char *s1, const char *s2) { int len1 = strlen(s1); int len2 = strlen(s2); int **d = (int **)malloc((len1 + 1) * sizeof(int *)); for (int i = 0; i <= len1; i++) { d[i] = (int *)malloc((len2 + 1) * sizeof(int)); for (int j = 0; j <= len2; j++) { d[i][j] = 0; } } for (int i = 0; i <= len1; i++) { d[i][0] = i; } for (int j = 0; j <= len2; j++) { d[0][j] = j; } for (int i = 1; i <= len1; i++) { for (int j = 1; j <= len2; j++) { int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; d[i][j] = min(d[i - 1][j] + 1, // deletion d[i][j - 1] + 1, // insertion d[i - 1][j - 1] + cost); // substitution } } int distance = d[len1][len2]; for (int i = 0; i <= len1; i++) { free(d[i]); } free(d); return distance; } // Returns a new string that is lowercase of str char *strLower(char *str) { char *lowerStr = strdup(str); for (char *p = lowerStr; *p; ++p) { *p = tolower(*p); } return lowerStr; } // Traverses the tree and applies fuzzy search on each node void fuzzySearchRecursive(FileSystemEntry *node, const char *searchTerm, int threshold, void (*callback)(FileSystemEntry *, int)) { if (node == NULL) { return; } // Convert search term, name, and fullPath to lowercase char *lowerSearchTerm = strLower((char *)searchTerm); char *lowerName = strLower(node->name); int nameDistance = levenshteinDistance(lowerName, lowerSearchTerm); // Partial matching with lowercase strings if (strstr(lowerName, lowerSearchTerm) != NULL) { callback(node, 0); } else if (nameDistance <= threshold) { callback(node, nameDistance); } // Free the allocated memory for lowercase strings free(lowerSearchTerm); free(lowerName); fuzzySearchRecursive(node->children, searchTerm, threshold, callback); fuzzySearchRecursive(node->next, searchTerm, threshold, callback); } kew-2.6.0/src/directorytree.h000066400000000000000000000023321464150306200161320ustar00rootroot00000000000000#ifndef DIRECTORYTREE_H #define DIRECTORYTREE_H #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); void fuzzySearchRecursive(FileSystemEntry *node, const char *searchTerm, int threshold, void (*callback)(FileSystemEntry *, int)); #endif kew-2.6.0/src/events.h000066400000000000000000000022561464150306200145570ustar00rootroot00000000000000#define MAX_SEQ_LEN 1024 // Maximum length of sequence buffer enum 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_SHOWSEARCH, 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, EVENT_SEARCH }; struct Event { enum EventType type; char key[MAX_SEQ_LEN]; // To store multi-byte characters }; typedef struct { char *seq; enum EventType eventType; } EventMapping; kew-2.6.0/src/file.c000066400000000000000000000320261464150306200141630ustar00rootroot00000000000000#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.6.0/src/file.h000066400000000000000000000024561464150306200141740ustar00rootroot00000000000000#ifndef FILE_H #define FILE_H #include #include #include #include #include #include #include #include #include #include #include #include #define __USE_GNU #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.6.0/src/kew.c000066400000000000000000001132271464150306200140350ustar00rootroot00000000000000/* 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 "search_ui.h" #include "settings.h" #include "sound.h" #include "soundcommon.h" #include "songloader.h" #include "utils.h" // #define DEBUG 1 #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; static unsigned int updateCounter = 0; bool gPressed = false; bool loadingAudioData = false; bool goingToSong = false; bool startFromTop = false; bool exactSearch = false; AppSettings settings; int fuzzySearchThreshold = 2; 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] = '\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 + 1, settings.seekBackward) != 0 && strcmp(seq + 1, settings.seekForward) != 0) { keyReleased = 1; break; } if (strlen(seq) + strlen(tmpSeq) >= MAX_SEQ_LEN) { break; } strcat(seq, tmpSeq); // This slows the continous reads down to not get a a too fast scrolling speed if (strcmp(seq + 1, settings.hardScrollUp) == 0 || strcmp(seq + 1, settings.hardScrollDown) == 0 || strcmp(seq + 1, settings.scrollUpAlt) == 0 || strcmp(seq + 1, settings.scrollDownAlt) == 0 || strcmp(seq + 1, settings.seekBackward) == 0 || strcmp(seq + 1, settings.seekForward) == 0 || strcmp(seq + 1, settings.hardNextPage) == 0 || strcmp(seq + 1, settings.hardPrevPage) == 0) { keyReleased = 0; break; } keyReleased = 0; } if (keyReleased) return event; eventProcessed = true; event.type = EVENT_NONE; strncpy(event.key, seq, MAX_SEQ_LEN); if (appState.currentView == SEARCH_VIEW) { if (strcmp(event.key, "\x7F") == 0 || strcmp(event.key, "\x08") == 0) { removeFromSearchText(getLibrary()); chosenSearchResultRow = 0; fuzzySearch(getLibrary(), fuzzySearchThreshold); event.type = EVENT_SEARCH; } else if (((strlen(event.key) == 1 && event.key[0] != '\033' && event.key[0] != '\n' && event.key[0] != '\r') || strcmp(event.key, " ") == 0 || (unsigned char)event.key[0] >= 0xC0)) { addToSearchText(event.key); chosenSearchResultRow = 0; fuzzySearch(getLibrary(), fuzzySearchThreshold); event.type = EVENT_SEARCH; } } // 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.hardQuit, 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.hardShowSearch, EVENT_SHOWSEARCH}, {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.hardShowSearchAlt, EVENT_SHOWSEARCH}, {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 (keyMappings[i].seq[0] != '\0' && ((seq[0] == '\033' && strlen(seq) > 1 && strcmp(seq + 1, keyMappings[i].seq) == 0) || strcmp(seq, keyMappings[i].seq) == 0)) { if (event.type == EVENT_SEARCH && keyMappings[i].eventType != EVENT_GOTOSONG) { break; } event.type = keyMappings[i].eventType; break; } } // Handle gg if (event.key[0] == 'g' && event.type == EVENT_NONE) { if (gPressed) { event.type = EVENT_GOTOBEGINNINGOFPLAYLIST; gPressed = false; } else { gPressed = true; } } // Handle numbers if (isdigit(event.key[0])) { if (digitsPressedCount < maxDigitsPressedCount) digitsPressed[digitsPressedCount++] = event.key[0]; } 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[0] != '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); gint64 length = getLengthInSec(currentSongData->duration); // update mpris emitMetadataChanged( currentSongData->metadata->title, currentSongData->metadata->artist, currentSongData->metadata->album, currentSongData->coverArtPath, currentSongData->trackId != NULL ? currentSongData->trackId : "", currentSong, length); } } // Checks conditions for refreshing player bool shouldRefreshPlayer() { return !skipping && !isEOFReached() && !isImplSwitchReached(); } // Refreshes the player visually if conditions are met void refreshPlayer() { int mutexResult = pthread_mutex_trylock(&switchMutex); if (mutexResult != 0) { fprintf(stderr, "Failed to lock switch mutex.\n"); return; } if (shouldRefreshPlayer()) { printPlayer(getCurrentSongData(), elapsedSeconds, &settings); } pthread_mutex_unlock(&switchMutex); } void handleGoToSong() { if (goingToSong) return; goingToSong = true; if (appState.currentView == LIBRARY_VIEW) { 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(getCurrentLibEntry()); pthread_mutex_unlock(&(playlist.mutex)); } else if (appState.currentView == SEARCH_VIEW) { pthread_mutex_lock(&(playlist.mutex)); setChosenDir(getCurrentSearchEntry()); enqueueSongs(getCurrentSearchEntry()); pthread_mutex_unlock(&(playlist.mutex)); } else { 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); } } 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_SHOWSEARCH: toggleShowSearch(); 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; } } void setCurrentSongToNext() { if (currentSong != NULL) lastPlayedId = currentSong->id; currentSong = getNextSong(); } void prepareNextSong() { if (!skipOutOfOrder && !isRepeatEnabled()) { setCurrentSongToNext(); } 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 handleSkipFromStopped() { // If we don't do this the song gets loaded in the wrong slot if (skipFromStopped) { usingSongDataA = !usingSongDataA; skipOutOfOrder = false; skipFromStopped = false; } } gboolean mainloop_callback(gpointer data) { (void)data; calcElapsedTime(); handleInput(); updateCounter++; // Update every other time or if searching (search needs to update often to detect keypresses) if (updateCounter % 2 == 0 || appState.currentView == SEARCH_VIEW) { // 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 && (skipFromStopped || !loadedNextSong || nextSongNeedsRebuilding) && !audioData.endOfListReached) { handleSkipFromStopped(); 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(50, 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); } freeSearchResults(); cleanupMpris(); restoreTerminalMode(); enableInputBuffering(); setConfig(&settings); saveSpecialPlaylist(settings.path); 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(); createLibrary(&settings); setlocale(LC_ALL, ""); fflush(stdout); #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 playAllAlbums() { init(); addShuffledAlbumsToPlayList(library, &playlist, MAX_FILES); if (playlist.count == 0) { exit(0); } 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); } moveConfigFiles(); // move from old location to new 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], "albums") == 0) { playAllAlbums(); } 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.6.0/src/m4a.h000066400000000000000000000612141464150306200137330ustar00rootroot00000000000000 /* 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.6.0/src/mpris.c000066400000000000000000001207471464150306200144060ustar00rootroot00000000000000#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.6.0/src/mpris.h000066400000000000000000000013351464150306200144020ustar00rootroot00000000000000#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.6.0/src/player.c000066400000000000000000001211711464150306200145400ustar00rootroot00000000000000#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.6.0"; const int ABSOLUTE_MIN_WIDTH = 68; 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 fastForwarding = false; bool rewinding = false; bool nerdFontsEnabled = true; int numProgressBars = 35; int elapsedBars = 0; int chosenRow = 0; int chosenSong = 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 lastRowColor = {90, 90, 90}; TagSettings metadata = {}; double pauseSeconds = 0.0; double totalPauseSeconds = 0.0; double seekAccumulatedSeconds = 0.0; int maxListSize = 0; int maxSearchListSize = 0; int numDirectoryTreeEntries = 0; int numTopLevelSongs = 0; int startLibIter = 0; int startSearchIter = 0; int maxLibListSize = 0; int chosenLibRow = 0; int chosenSearchResultRow = 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; 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) { float aspectRatio = calcAspectRatio(); int term_w, term_h; getTermSize(&term_w, &term_h); int timeDisplayHeight = 1; int heightMargin = 4; int minHeight = visualizerHeight + metatagHeight + timeDisplayHeight + heightMargin; int minBorderWidth = 0; *height = term_h - minHeight; *width = ceil(*height * aspectRatio); if (*width > term_w) { *width = term_w - minBorderWidth; *height = floor(*width / aspectRatio); } return 0; } void calcPreferredSize() { minHeight = 2 + (visualizerEnabled ? visualizerHeight : 0); calcIdealImgSize(&preferredWidth, &preferredHeight, (visualizerEnabled ? visualizerHeight : 0), calcMetadataHeight()); } 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 albums (plays all albums up to 2000 randomly one after the other)"); 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 search.\n"); printf(" Press F6 to display key bindings.\n"); printf(" Press . to add the currently playing song to kew.m3u.\n"); printf(" Press Esc 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) { if (!ansii) { printSquareBitmapCentered(cover, height); } else { int width = height * 2; 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 || appState.currentView == SEARCH_VIEW) return; c_sleep(100); setColor(); printBasicMetadata(metadata); } void printTime(double elapsedSeconds) { if (!timeEnabled || appState.currentView == LIBRARY_VIEW || appState.currentView == PLAYLIST_VIEW || appState.currentView == SEARCH_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 Search|F6 Keys|Esc 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; } 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(" - Esc or %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 toggleShowSearch() { refresh = true; if (appState.currentView == SEARCH_VIEW) { appState.currentView = SONG_VIEW; } else { appState.currentView = SEARCH_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; chosenRow = (chosenRow >= originalPlaylist->count) ? originalPlaylist->count - 1 : chosenRow; refresh = true; } else if (appState.currentView == SEARCH_VIEW) { chosenSearchResultRow += maxSearchListSize -1; chosenSearchResultRow = (chosenSearchResultRow >= getSearchResultsCount()) ? getSearchResultsCount() - 1 : chosenSearchResultRow; startSearchIter += maxSearchListSize - 1; refresh = true; } } void flipPrevPage() { if (appState.currentView == LIBRARY_VIEW) { chosenLibRow -= maxLibListSize; startLibIter -= maxLibListSize; refresh = true; } else if (appState.currentView == PLAYLIST_VIEW) { chosenRow -= maxListSize; chosenRow = (chosenRow > 0) ? chosenRow : 0; refresh = true; } else if (appState.currentView == SEARCH_VIEW) { chosenSearchResultRow -= maxSearchListSize; chosenSearchResultRow = (chosenSearchResultRow > 0) ? chosenSearchResultRow : 0; startSearchIter -= maxSearchListSize; refresh = true; } } void scrollNext() { if (appState.currentView == PLAYLIST_VIEW) { chosenRow++; chosenRow = (chosenRow >= originalPlaylist->count) ? originalPlaylist->count - 1 : chosenRow; refresh = true; } else if (appState.currentView == LIBRARY_VIEW) { chosenLibRow++; refresh = true; } else if (appState.currentView == SEARCH_VIEW) { chosenSearchResultRow++; refresh = true; } } void scrollPrev() { if (appState.currentView == PLAYLIST_VIEW) { chosenRow--; chosenRow = (chosenRow > 0) ? chosenRow : 0; refresh = true; } else if (appState.currentView == LIBRARY_VIEW) { chosenLibRow--; refresh = true; } else if (appState.currentView == SEARCH_VIEW) { chosenSearchResultRow--; chosenSearchResultRow = ( chosenSearchResultRow > 0) ? chosenSearchResultRow : 0; refresh = true; } } int getRowWithinBounds(int row) { if (row >= originalPlaylist->count) { row = originalPlaylist->count - 1; } if (row < 0) row = 0; return row; } int printLogoAndAdjustments(SongData *songData, int termWidth, bool hideHelp, int indentation) { int aboutRows = printLogo(songData); if (termWidth > 52 && !hideHelp) { setDefaultTextColor(); printBlankSpaces(indentation); printf(" Use ↑, ↓ or k, j to choose. Enter to accept.\n"); printBlankSpaces(indentation); printf(" Pg Up and Pg Dn to scroll. Del to remove entry.\n\n"); return aboutRows + 3; } return aboutRows; } void showSearch(SongData *songData, int *chosenRow) { int term_w, term_h; getTermSize(&term_w, &term_h); maxSearchListSize = term_h - 5; int aboutRows = printLogo(songData); maxSearchListSize -= aboutRows; printBlankSpaces(indent); printf(" Use ↑, ↓ to choose. Enter to accept.\n\n"); maxSearchListSize -= 2; displaySearch(maxSearchListSize, indent, chosenRow, startSearchIter); printf("\n"); printLastRow(); } void showPlaylist(SongData *songData, PlayList *list, int *chosenSong, int *chosenNodeId) { int term_w, term_h; getTermSize(&term_w, &term_h); maxListSize = term_h - 2; int aboutRows = printLogoAndAdjustments(songData, term_w, hideHelp, indent); maxListSize -= aboutRows; displayPlaylist(list, maxListSize, indent, chosenSong, chosenNodeId, resetPlaylistDisplay); printf("\n"); printLastRow(); } 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 setChosenDir(FileSystemEntry *entry) { if (entry->isDirectory) { currentEntry = chosenDir = entry; } } 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 != SEARCH_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(); showPlaylist(songdata, originalPlaylist, &chosenRow, &chosenNodeId); resetPlaylistDisplay = false; refresh = false; } else if (appState.currentView == SEARCH_VIEW && (refresh || newUndisplayedSearch)) { clearScreen(); showSearch(songdata, &chosenSearchResultRow); refresh = false; newUndisplayedSearch = 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.6.0/src/player.h000066400000000000000000000037731464150306200145540ustar00rootroot00000000000000#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 "playlist_ui.h" #include "search_ui.h" #include "songloader.h" #include "sound.h" #include "term.h" #include "utils.h" #include "visuals.h" #include "common_ui.h" 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 chosenSearchResultRow; 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); void setChosenDir(FileSystemEntry *entry); 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 toggleShowSearch(void); void showTrack(); void setTextColorRGB2(int r, int g, int b); void freeMainDirectoryTree(); char *getLibraryFilePath(); void resetChosenDir(); #endif kew-2.6.0/src/playerops.c000066400000000000000000001131541464150306200152640ustar00rootroot00000000000000#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 4 #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 skipFromStopped = 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(); skipFromStopped = true; } 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; } bool isCurrentSongDeleted() { return (audioData.currentFileIndex == 0) ? userData.songdataADeleted == true : userData.songdataBDeleted == true; } bool isValidSong(SongData *songData) { return songData != NULL && songData->hasErrors == false && songData->metadata != NULL; } SongData *getCurrentSongData(void) { if (currentSong == NULL) return NULL; if (isCurrentSongDeleted()) return NULL; SongData *songData = (audioData.currentFileIndex == 0) ? userData.songdataA : userData.songdataB; if (!isValidSong(songData)) return NULL; return songData; } 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); 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 dequeueAll() { } void enqueueSongs(FileSystemEntry *entry) { FileSystemEntry *chosenDir = getChosenDir(); bool hasEnqueued = false; if (entry != NULL) { if (entry->isDirectory) { if (!hasSongChildren(entry) || (chosenDir != NULL && strcmp(entry->fullPath, chosenDir->fullPath) == 0)) { if (hasDequeuedChildren(entry)) { entry->isEnqueued = 1; entry = entry->children; enqueueChildren(entry); nextSongNeedsRebuilding = true; hasEnqueued = true; } else { dequeueChildren(entry); nextSongNeedsRebuilding = true; } } setCurrentAsChosenDir(); allowChooseSongs = true; } else { if (!entry->isEnqueued) { nextSong = NULL; nextSongNeedsRebuilding = true; enqueueSong(entry); hasEnqueued = true; } else { nextSong = NULL; nextSongNeedsRebuilding = true; dequeueSong(entry); } } 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); if (node == NULL) { return; } 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; freeSearchResults(); 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 a (local) 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.6.0/src/playerops.h000066400000000000000000000054631464150306200152740ustar00rootroot00000000000000 #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 bool skipFromStopped; extern UserData userData; SongData *getCurrentSongData(void); void rebuildAndUpdatePlaylist(); Node *getNextSong(); void handleRemove(); void enqueueSongs(FileSystemEntry *entry); 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.6.0/src/playlist.c000066400000000000000000000636571464150306200151230ustar00rootroot00000000000000#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; } // Function to generate the filename with the .m3u extension void generateM3UFilename(const char *basePath, const char *filePath, char *m3uFilename, size_t size) { // Find the last occurrence of '/' in the file path to get the base name const char *baseName = strrchr(filePath, '/'); if (baseName == NULL) { baseName = filePath; // No '/' found, use the entire filename } else { baseName++; // Skip the '/' character } // Find the last occurrence of '.' in the base name const char *dot = strrchr(baseName, '.'); if (dot == NULL) { // No '.' found, copy the base name and append ".m3u" if (basePath[strlen(basePath) - 1] == '/') { snprintf(m3uFilename, size, "%s%s.m3u", basePath, baseName); } else { snprintf(m3uFilename, size, "%s/%s.m3u", basePath, baseName); } } else { // Copy the base name up to the dot and append ".m3u" size_t baseNameLen = dot - baseName; if (basePath[strlen(basePath) - 1] == '/') { snprintf(m3uFilename, size, "%s%.*s.m3u", basePath, (int)baseNameLen, baseName); } else { snprintf(m3uFilename, size, "%s/%.*s.m3u", basePath, (int)baseNameLen, baseName); } } } 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) { if (directory == NULL) { return; } char playlistPath[MAXPATHLEN]; playlistPath[0] = '\0'; c_strcpy(playlistPath, sizeof(playlistPath), directory); if (playlistPath[0] == '\0') { return; } if (playlistPath[strlen(playlistPath) - 1] != '/') strcat(playlistPath, "/"); strcat(playlistPath, mainPlaylistName); if (specialPlaylist != NULL && specialPlaylist->count > 0) writeM3UFile(playlistPath, specialPlaylist); } void savePlaylist(const char *path) { if (path == NULL) { return; } if (playlist.head == NULL || playlist.head->song.filePath == NULL) return; char m3uFilename[MAXPATHLEN]; generateM3UFilename(path, playlist.head->song.filePath, m3uFilename, sizeof(m3uFilename)); writeM3UFile(m3uFilename, &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); } int isMusicFile(const char *filename) { const char *extensions[] = {".m4a", ".aac",".mp4", ".mp3", ".ogg", ".flac",".wav", ".opus"}; size_t numExtensions = sizeof(extensions) / sizeof(extensions[0]); for (size_t i = 0; i < numExtensions; i++) { if (strstr(filename, extensions[i]) != NULL) { return 1; } } return 0; } int containsMusicFiles(FileSystemEntry *entry) { if (entry == NULL) return 0; FileSystemEntry *child = entry->children; while (child != NULL) { if (!child->isDirectory && isMusicFile(child->name)) { return 1; } child = child->next; } return 0; } void addAlbumToPlayList(PlayList *list, FileSystemEntry *album, int playlistMax) { FileSystemEntry *entry = album->children; while (entry != NULL && list->count < playlistMax) { if (!entry->isDirectory && isMusicFile(entry->name)) { addSongToPlayList(list, entry->fullPath, playlistMax); } entry = entry->next; } } void addAlbumsToPlayList(FileSystemEntry *entry, PlayList *list, int playlistMax) { if (entry == NULL || list->count >= playlistMax) return; if (entry->isDirectory && containsMusicFiles(entry)) { addAlbumToPlayList(list, entry, playlistMax); } if (entry->isDirectory && entry->children != NULL) { addAlbumsToPlayList(entry->children, list, playlistMax); } if (entry->next != NULL) { addAlbumsToPlayList(entry->next, list, playlistMax); } } void shuffleEntries(FileSystemEntry **array, size_t n) { if (n > 1) { size_t i; for (i = 0; i < n - 1; i++) { size_t j = i + rand() / (RAND_MAX / (n - i) + 1); FileSystemEntry *temp = array[i]; array[i] = array[j]; array[j] = temp; } } } void collectAlbums(FileSystemEntry *entry, FileSystemEntry **albums, size_t *count) { if (entry == NULL) return; if (entry->isDirectory && containsMusicFiles(entry)) { albums[*count] = entry; (*count)++; } if (entry->isDirectory && entry->children != NULL) { collectAlbums(entry->children, albums, count); } if (entry->next != NULL) { collectAlbums(entry->next, albums, count); } } void addShuffledAlbumsToPlayList(FileSystemEntry *root, PlayList *list, int playlistMax) { size_t maxAlbums = 2000; FileSystemEntry *albums[maxAlbums]; size_t albumCount = 0; collectAlbums(root, albums, &albumCount); srand(time(NULL)); shuffleEntries(albums, albumCount); for (size_t i = 0; i < albumCount && list->count < playlistMax; i++) { addAlbumToPlayList(list, albums[i], playlistMax); } } kew-2.6.0/src/playlist.h000066400000000000000000000037741464150306200151220ustar00rootroot00000000000000#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 writeCurrentPlaylistToM3UFile(PlayList *playlist); void writeM3UFile(const char *filename, PlayList *playlist); void loadSpecialPlaylist(const char *directory); void saveSpecialPlaylist(const char *directory); 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); void addShuffledAlbumsToPlayList(FileSystemEntry *root, PlayList *list, int playlistMax); kew-2.6.0/src/playlist_ui.c000066400000000000000000000121071464150306200156000ustar00rootroot00000000000000#include "playlist_ui.h" int startIter = 0; void getTerminalSize(int *width, int *height) { getTermSize(width, height); } Node *determineStartNode(Node *head, int *foundAt, bool *startFromCurrent, int listSize) { Node *node = head; Node *foundNode = NULL; int numSongs = 0; *foundAt = -1; while (node != NULL && numSongs <= listSize) { if (currentSong != NULL && currentSong->id == node->id) { *foundAt = numSongs; foundNode = node; break; } node = node->next; numSongs++; } *startFromCurrent = (*foundAt > -1) ? true : false; return foundNode ? foundNode : head; } void preparePlaylistString(Node *node, char *buffer, int bufferSize, int shortenAmount) { if (node == NULL || buffer == NULL) { buffer[0] = '\0'; return; } char filePath[MAXPATHLEN]; c_strcpy(filePath, sizeof(filePath), node->song.filePath); char *lastSlash = strrchr(filePath, '/'); char *lastDot = strrchr(filePath, '.'); if (lastSlash != NULL && lastDot != NULL && lastDot > lastSlash) { int nameLength = lastDot - lastSlash - 1; nameLength = (nameLength < bufferSize - 1) ? nameLength : bufferSize - 1; strncpy(buffer, lastSlash + 1, nameLength); buffer[nameLength] = '\0'; removeUnneededChars(buffer); shortenString(buffer, shortenAmount); trim(buffer); } else { buffer[0] = '\0'; } } int displayPlaylistItems(Node *startNode, int startIter, int maxListSize, int termWidth, int indent, int chosenSong, int *chosenNodeId) { int numPrintedRows = 0; Node *node = startNode; char *buffer = (char *)malloc(MAXPATHLEN * sizeof(char)); if (!buffer) { return 0; } for (int i = startIter; node != NULL && i < startIter + maxListSize; i++) { preparePlaylistString(node, buffer, MAXPATHLEN, termWidth - indent - 10); if (buffer[0] != '\0') { setDefaultTextColor(); printBlankSpaces(indent); if (i == chosenSong) { *chosenNodeId = node->id; printf("\x1b[7m"); } if (currentSong != NULL && currentSong->id == node->id) { printf("\e[1m\e[39m"); } if (i + 1 < 10) printf(" "); printf(" %d. %s \n", i + 1, buffer); numPrintedRows++; } node = node->next; } free(buffer); return numPrintedRows; } int displayPlaylist(PlayList *list, int maxListSize, int indent, int *chosenSong, int *chosenNodeId, bool reset) { int termWidth, termHeight; getTerminalSize(&termWidth, &termHeight); int foundAt = -1; bool startFromCurrent = false; Node *startNode = determineStartNode(list->head, &foundAt, &startFromCurrent, list->count); // Determine chosen song if (*chosenSong >= list->count) { *chosenSong = list->count - 1; } if (*chosenSong < 0) { *chosenSong = 0; } int startIter = 0; // Determine where to start iterating startIter = (startFromCurrent && (foundAt < startIter || foundAt > startIter + maxListSize)) ? foundAt : startIter; if (*chosenSong < startIter) { startIter = *chosenSong; } if (*chosenSong > startIter + maxListSize - round(maxListSize / 2)) { startIter = *chosenSong - maxListSize + round(maxListSize / 2); } if (reset && !audioData.endOfListReached) { startIter = *chosenSong = foundAt; } // Go up to find the starting node for (int i = foundAt; i > startIter; i--) { if (i > 0 && startNode->prev != NULL) startNode = startNode->prev; } // Go down to adjust the startNode for (int i = (foundAt == -1) ? 0 : foundAt; i < startIter; i++) { if (startNode->next != NULL) startNode = startNode->next; } int printedRows = displayPlaylistItems(startNode, startIter, maxListSize, termWidth, indent, *chosenSong, chosenNodeId); if (printedRows > 1) { while (printedRows < maxListSize) { printf("\n"); printedRows++; } } return printedRows; } kew-2.6.0/src/playlist_ui.h000066400000000000000000000003731464150306200156070ustar00rootroot00000000000000#ifndef PLAYLIST_UI_H #define PLAYLIST_UI_H #include "playlist.h" #include "songloader.h" #include "term.h" #include "utils.h" int displayPlaylist(PlayList *list, int maxListSize, int indent, int *chosenSong, int *chosenNodeId, bool reset); #endif kew-2.6.0/src/search_ui.c000066400000000000000000000170211464150306200152040ustar00rootroot00000000000000#include "search_ui.h" #define MAX_SEARCH_LEN 32 int numSearchLetters = 0; int numSearchBytes = 0; typedef struct SearchResult { FileSystemEntry *entry; int distance; } SearchResult; // Global variables to store results SearchResult *results = NULL; size_t resultsCount = 0; size_t resultsCapacity = 0; bool newUndisplayedSearch = false; int minSearchLetters = 1; FileSystemEntry *currentSearchEntry = NULL; char searchText[MAX_SEARCH_LEN * 4 + 1]; // unicode can be 4 characters FileSystemEntry *getCurrentSearchEntry() { return currentSearchEntry; } int getSearchResultsCount() { return resultsCount; } // Function to add a result to the global array void addResult(FileSystemEntry *entry, int distance) { if (resultsCount >= resultsCapacity) { resultsCapacity = resultsCapacity == 0 ? 10 : resultsCapacity * 2; results = realloc(results, resultsCapacity * sizeof(SearchResult)); } results[resultsCount].distance = distance; results[resultsCount].entry = entry; resultsCount++; } // Callback function to collect results void collectResult(FileSystemEntry *entry, int distance) { addResult(entry, distance); } // Free allocated memory from previous search void freeSearchResults() { if (results != NULL) { free(results); results = NULL; } resultsCapacity = 0; resultsCount = 0; } void fuzzySearch(FileSystemEntry *root, int threshold) { freeSearchResults(); if (numSearchLetters > minSearchLetters) { fuzzySearchRecursive(root, searchText, threshold, collectResult); } newUndisplayedSearch = true; } int compareResults(const void *a, const void *b) { SearchResult *resultA = (SearchResult *)a; SearchResult *resultB = (SearchResult *)b; return resultA->distance - resultB->distance; } void sortResults() { qsort(results, resultsCount, sizeof(SearchResult), compareResults); } int displaySearchBox(int indent) { printBlankSpaces(indent); printf(" [Search]: "); setDefaultTextColor(); // Save cursor position printf("%s", searchText); printf("\033[s"); printf("█\n"); return 0; } int addToSearchText(const char *str) { if (str == NULL) { return -1; } size_t len = strlen(str); // Check if the string can fit into the search text buffer if (numSearchLetters + 1 > MAX_SEARCH_LEN) { return 0; // Not enough space } // Restore cursor position printf("\033[u"); // Print the string printf("%s", str); // Save cursor position printf("\033[s"); printf("█\n"); // Add the string to the search text buffer for (size_t i = 0; i < len; i++) { searchText[numSearchBytes++] = str[i]; } searchText[numSearchBytes + 1] = '\0'; // Null-terminate the buffer numSearchLetters++; return 0; } // Determine the number of bytes in the last UTF-8 character int getLastCharBytes(const char *str, int len) { if (len == 0) return 0; int i = len - 1; while (i >= 0 && (str[i] & 0xC0) == 0x80) { i--; } return len - i; } // Remove the preceding character from the search text int removeFromSearchText() { if (numSearchLetters == 0) return 0; // Determine the number of bytes to remove for the last character int lastCharBytes = getLastCharBytes(searchText, numSearchBytes); if (lastCharBytes == 0) return 0; // Restore cursor position printf("\033[u"); // Move cursor back one step printf("\033[D"); // Overwrite the character with spaces for (int i = 0; i < lastCharBytes; i++) { printf(" "); } // Move cursor back again to the original position for (int i = 0; i < lastCharBytes; i++) { printf("\033[D"); } // Save cursor position printf("\033[s"); // Print a block character to represent the cursor printf("█"); // Clear the end of the line printf("\033[K"); fflush(stdout); // Remove the character from the buffer numSearchBytes -= lastCharBytes; searchText[numSearchBytes] = '\0'; numSearchLetters--; return 0; } int displaySearchResults(int maxListSize, int indent, int *chosenRow, int startSearchIter) { int term_w, term_h; getTermSize(&term_w, &term_h); int maxNameWidth = term_w - indent - 5; char name[maxNameWidth + 1]; sortResults(); if (*chosenRow >= (int)resultsCount - 1) { *chosenRow = resultsCount - 1; } if (startSearchIter < 0) startSearchIter = 0; if (*chosenRow > startSearchIter + round(maxListSize / 2)) { startSearchIter = *chosenRow - round(maxListSize / 2) + 1; } if (*chosenRow < startSearchIter) startSearchIter = *chosenRow; if (*chosenRow < 0) startSearchIter = *chosenRow = 0; printf("\n"); // Print the sorted results for (size_t i = startSearchIter; i < resultsCount; i++) { if (numSearchLetters < minSearchLetters) break; if ((int)i >= (maxListSize + startSearchIter)) break; setDefaultTextColor(); printBlankSpaces(indent); if (*chosenRow == (int)i) { currentSearchEntry = results[i].entry; if (results[i].entry->isEnqueued) { if (useProfileColors) setTextColor(enqueuedColor); else setColor(); printf("\x1b[7m * "); } else { printf(" \x1b[7m "); } } else { if (results[i].entry->isEnqueued) { if (useProfileColors) setTextColor(enqueuedColor); else setColor(); printf(" * "); } else printf(" "); } name[0] = '\0'; if (results[i].entry->isDirectory) { snprintf(name, maxNameWidth + 1, "[%s]", results[i].entry->name); } else { snprintf(name, maxNameWidth + 1, "%s", results[i].entry->name); } printf("%s\n", name); } return 0; } int displaySearch(int maxListSize, int indent, int *chosenRow, int startSearchIter) { displaySearchBox(indent); displaySearchResults(maxListSize, indent, chosenRow, startSearchIter); return 0; } kew-2.6.0/src/search_ui.h000066400000000000000000000006771464150306200152220ustar00rootroot00000000000000#include #include #include "directorytree.h" #include "term.h" #include "common_ui.h" extern bool newUndisplayedSearch; int displaySearch(int maxListSize, int indent, int *chosenRow, int startSearchIter); int addToSearchText(const char *str); int removeFromSearchText(); int getSearchResultsCount(); void fuzzySearch(FileSystemEntry *root, int threshold); void freeSearchResults(); FileSystemEntry *getCurrentSearchEntry(); kew-2.6.0/src/settings.c000066400000000000000000000555531464150306200151160ustar00rootroot00000000000000#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, "[17~", sizeof(settings.hardShowKeys)); strncpy(settings.hardShowKeysAlt, "[17~", 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.hardShowSearch, "[15~", sizeof(settings.hardShowSearch)); strncpy(settings.hardShowSearchAlt, "[[E", sizeof(settings.hardShowSearchAlt)); 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.titleColor, "6", sizeof(settings.titleColor)); strncpy(settings.enqueuedColor, "7", sizeof(settings.enqueuedColor)); strncpy(settings.quit, "q", sizeof(settings.quit)); strncpy(settings.hardQuit, "\x1B", sizeof(settings.hardQuit)); 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 music library directory tree 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.6.0/src/settings.h000066400000000000000000000006561464150306200151150ustar00rootroot00000000000000#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.6.0/src/songloader.c000066400000000000000000000227651464150306200154120ustar00rootroot00000000000000#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, -2 if no file found or if file has errors 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; return -2; } 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.6.0/src/songloader.h000066400000000000000000000021501464150306200154010ustar00rootroot00000000000000#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.6.0/src/sound.c000066400000000000000000000425341464150306200144010ustar00rootroot00000000000000#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"); } bool validFilePath(char *filePath) { if (filePath == NULL || filePath[0] == '\0' || filePath[0] == '\r' || existsFile(filePath) < 0) return false; else return true; } 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 (!validFilePath(filePath)) { 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); gint64 length = getLengthInSec(currentSongData->duration); // update mpris emitMetadataChanged( currentSongData->metadata->title, currentSongData->metadata->artist, currentSongData->metadata->album, currentSongData->coverArtPath, currentSongData->trackId != NULL ? currentSongData->trackId : "", currentSong, length); } } else { return -1; } return 0; } kew-2.6.0/src/sound.h000066400000000000000000000026451464150306200144050ustar00rootroot00000000000000#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.6.0/src/soundbuiltin.c000066400000000000000000000155031464150306200157640ustar00rootroot00000000000000#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())) { 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 (firstDecoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } result = ma_data_source_read_pcm_frames(firstDecoder, (ma_int32 *)pFramesOut + framesRead * audioData->channels, 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.6.0/src/soundbuiltin.h000066400000000000000000000006711464150306200157710ustar00rootroot00000000000000#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.6.0/src/soundcommon.c000066400000000000000000001335231464150306200156110ustar00rootroot00000000000000#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; void logTime(const char *message) { (void)message; // struct timespec ts; // clock_gettime(CLOCK_REALTIME, &ts); // printf("[%ld.%09ld] %s\n", ts.tv_sec, ts.tv_nsec, message); } 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 *removeBlacklistedChars(const char *input, const char *blacklist) { if (!input || !blacklist) { return NULL; } char *output = calloc(strlen(input) + 1, sizeof(char)); 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++; } return output; } gint64 getLengthInSec(double duration) { return floor(llround(duration * G_USEC_PER_SEC)); } int displaySongNotification(const char *artist, const char *title, const char *cover) { if (!allowNotifications) { return 0; } char sanitized_cover[MAXPATHLEN]; const char *blacklist = "&;`|*~<>^()[]{}$\\\""; char *sanitizedArtist = removeBlacklistedChars(artist, blacklist); char *sanitizedTitle = removeBlacklistedChars(title, blacklist); if (!sanitizedArtist || !sanitizedTitle) { free(sanitizedArtist); free(sanitizedTitle); return -1; } sanitize_filepath(cover, sanitized_cover, sizeof(sanitized_cover)); char message[MAXPATHLEN + 1024]; if (strlen(artist) > 0) { snprintf(message, sizeof(message), "%s - %s", sanitizedArtist, sanitizedTitle); } else { snprintf(message, sizeof(message), "%s", sanitizedTitle); } char *args[] = {"/usr/bin/notify-send", "-a", "kew", message, "--icon", sanitized_cover, NULL}; pid_t pid = vfork(); if (pid == -1) { perror("vfork"); free(sanitizedArtist); free(sanitizedTitle); return -1; } else if (pid == 0) { // Child process execv(args[0], args); // If execv fails perror("execv"); _Exit(EXIT_FAILURE); } // Parent process free(sanitizedArtist); free(sanitizedTitle); return 0; } 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.6.0/src/soundcommon.h000066400000000000000000000164761464150306200156250ustar00rootroot00000000000000#ifndef SOUND_COMMON_H #define SOUND_COMMON_H #include #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 hardQuit[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 hardShowSearch[6]; char hardShowSearchAlt[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, SEARCH_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); gint64 getLengthInSec(double duration); 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); void logTime(const char *message); #endif kew-2.6.0/src/term.c000066400000000000000000000127551464150306200142220ustar00rootroot00000000000000#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_old(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 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 a single-byte ASCII character, return it if ((c & 0x80) == 0) { seq[0] = c; seq[1] = '\0'; return 1; } // Determine the length of the UTF-8 sequence int additionalBytes; if ((c & 0xE0) == 0xC0) { additionalBytes = 1; // 2-byte sequence } else if ((c & 0xF0) == 0xE0) { additionalBytes = 2; // 3-byte sequence } else if ((c & 0xF8) == 0xF0) { additionalBytes = 3; // 4-byte sequence } else { // Invalid UTF-8 start byte return 0; } // Ensure there's enough space in the buffer if ((size_t)additionalBytes + 1 >= seqSize) { return 0; } // Read the remaining bytes of the UTF-8 sequence seq[0] = c; bytesRead = read(STDIN_FILENO, &seq[1], additionalBytes); if (bytesRead != additionalBytes) { return 0; } seq[additionalBytes + 1] = '\0'; return additionalBytes + 1; } 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; } // Function to check if the key is a function key int isFunctionKey(const char *seq) { if (seq[0] == '\033') { // ESC character return 1; } return 0; } kew-2.6.0/src/term.h000066400000000000000000000024061464150306200142170ustar00rootroot00000000000000#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); // Function to check if the key is a function key int isFunctionKey(const char *seq); #endif kew-2.6.0/src/utils.c000066400000000000000000000214141464150306200144030ustar00rootroot00000000000000#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 *getConfigPathOld() { 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; } char *getConfigPath() { char *configPath = malloc(MAXPATHLEN); if (!configPath) return NULL; const char *xdgConfig = getenv("XDG_CONFIG_HOME"); if (xdgConfig) { snprintf(configPath, MAXPATHLEN, "%s/kew", xdgConfig); } else { const char *home = getHomePath(); if (home) { snprintf(configPath, MAXPATHLEN, "%s/.config/kew", home); } else { struct passwd *pw = getpwuid(getuid()); if (pw) { snprintf(configPath, MAXPATHLEN, "%s/.config/kew", pw->pw_dir); } else { free(configPath); return NULL; } } } return configPath; } int moveConfigFiles() { char *oldPath = getConfigPathOld(); char *newPath = getConfigPath(); if (!oldPath || !newPath) { free(oldPath); free(newPath); return -1; } struct stat st = {0}; if (stat(newPath, &st) == -1) { mkdir(newPath, 0700); } char oldFileLibrary[MAXPATHLEN]; char newFileLibrary[MAXPATHLEN]; char oldFileRc[MAXPATHLEN]; char newFileRc[MAXPATHLEN]; snprintf(oldFileLibrary, MAXPATHLEN, "%s/kewlibrary", oldPath); snprintf(newFileLibrary, MAXPATHLEN, "%s/kewlibrary", newPath); snprintf(oldFileRc, MAXPATHLEN, "%s/kewrc", oldPath); snprintf(newFileRc, MAXPATHLEN, "%s/kewrc", newPath); if (access(oldFileLibrary, F_OK) == 0) { rename(oldFileLibrary, newFileLibrary); } if (access(oldFileRc, F_OK) == 0) { rename(oldFileRc, newFileRc); } free(oldPath); free(newPath); return 0; } 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++; } } void shortenString(char *str, size_t maxLength) { size_t length = strlen(str); if (length > maxLength) { str[maxLength] = '\0'; } } void printBlankSpaces(int numSpaces) { for (int i = 0; i < numSpaces; i++) { printf(" "); } } kew-2.6.0/src/utils.h000066400000000000000000000022171464150306200144100ustar00rootroot00000000000000#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 #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 *getConfigPathOld(); char *getConfigPath(); int moveConfigFiles(void); void removeUnneededChars(char *str); void shortenString(char *str, size_t maxLength); void printBlankSpaces(int numSpaces); #endif kew-2.6.0/src/visuals.c000066400000000000000000000303321464150306200147300ustar00rootroot00000000000000#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.2f; float lastMax = -1.0f; 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}; int terminalSupportsUnicode() { char *locale = setlocale(LC_ALL, ""); if (locale != NULL) return 1; return 0; } void initVisuals() { unicodeSupport = false; if (terminalSupportsUnicode() > 0) unicodeSupport = true; } #define MOVING_AVERAGE_WINDOW_SIZE 2 void updateMagnitudes(int height, int width, float maxMagnitude, float *magnitudes) { // Temporary array to store smoothed magnitudes float smoothedMagnitudes[width]; // Apply moving average smoothing to magnitudes for (int i = 0; i < width; i++) { float sum = magnitudes[i]; int count = 1; // Calculate moving average using a window centered at the current frequency bin for (int j = 1; j <= MOVING_AVERAGE_WINDOW_SIZE / 2; j++) { if (i - j >= 0) { sum += magnitudes[i - j]; count++; } if (i + j < width) { sum += magnitudes[i + j]; count++; } } // Compute the smoothed magnitude by averaging smoothedMagnitudes[i] = sum / count; } // Update magnitudes array with smoothed values for (int i = 0; i < width; i++) { magnitudes[i] = smoothedMagnitudes[i]; } // Apply decay factor to the smoothed magnitudes float exponent = 1.0; float decreaseFactor = 0.8; for (int i = 0; i < width; i++) { float normalizedMagnitude = magnitudes[i] / maxMagnitude; if (normalizedMagnitude > 1.0f) normalizedMagnitude = 1.0f; float scaledMagnitude = pow(normalizedMagnitude, exponent) * height; float decayedMagnitude = lastMagnitudes[i] * decreaseFactor; if (scaledMagnitude < decayedMagnitude) { magnitudes[i] = decayedMagnitude; } 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]; } } if (lastMax < 0.0f) return maxMagnitude; 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; i < numBars && i < j / 2; i++) { // Directly set magnitude for each bar from FFT output float magnitude = sqrtf(fftOutput[i][0] * fftOutput[i][0] + fftOutput[i][1] * fftOutput[i][1]); magnitudes[i] = magnitude; } // Normalize and update magnitudes for visualization 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) { lastMax = -1.0f; 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.6.0/src/visuals.h000066400000000000000000000011011464150306200147250ustar00rootroot00000000000000 #include #include #include #include #include #include #include #include "sound.h" #include "term.h" #include "utils.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);