kew-3.2.0/000077500000000000000000000000001500206121000122655ustar00rootroot00000000000000kew-3.2.0/.editorconfig000066400000000000000000000003441500206121000147430ustar00rootroot00000000000000# 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 = true insert_final_newline = truekew-3.2.0/.github/000077500000000000000000000000001500206121000136255ustar00rootroot00000000000000kew-3.2.0/.github/FUNDING.yml000066400000000000000000000000211500206121000154330ustar00rootroot00000000000000github: ravachol kew-3.2.0/.github/workflows/000077500000000000000000000000001500206121000156625ustar00rootroot00000000000000kew-3.2.0/.github/workflows/appimage_alpine.yml000066400000000000000000000051761500206121000215310ustar00rootroot00000000000000name: Alpine-appimage on: workflow_dispatch: jobs: build-and-create-appimage: runs-on: ubuntu-latest container: image: alpine:latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Install build essentials and dependencies run: | apk update apk add --no-cache \ build-base \ taglib-dev \ fftw-dev \ chafa-dev \ opus-dev \ opusfile-dev \ libvorbis-dev \ libogg-dev \ glib-dev \ wget git desktop-file-utils \ squashfs-tools \ patchelf \ musl \ musl-dev \ gcompat \ curl-dev - name: Build code with static linking run: | # export CC=musl-gcc # export LDFLAGS="-static -Wl,-z,relro,-lz" make - name: Prepare AppDir run: | mkdir -p appdir/usr/bin chmod +x ./kew mv ./kew appdir/usr/bin/ mkdir -p appdir/usr/lib - name: Download uploadtool run: | wget -q https://github.com/probonopd/uploadtool/raw/master/upload.sh chmod +x upload.sh mv upload.sh /usr/local/bin/uploadtool - name: Download and prepare appimagetool run: | wget -O appimagetool-x86_64.AppImage -c https://github.com/$(wget -q https://github.com/probonopd/go-appimage/releases/expanded_assets/continuous -O - | grep "appimagetool-.*-x86_64.AppImage" | head -n 1 | cut -d '"' -f 2) if [ ! -f appimagetool-*.AppImage ]; then echo "appimagetool download failed"; exit 1; fi chmod +x appimagetool-x86_64.AppImage - name: Use appimagetool with --appimage-extract-and-run run: | ./appimagetool-x86_64.AppImage --appimage-extract-and-run deploy appdir/usr/share/applications/kew.desktop - name: Create AppImage run: | mkdir -p output APPIMAGE_EXTRACT_AND_RUN=1 \ ARCH=$(uname -m) \ VERSION=$(./appdir/usr/bin/kew --version | awk -F": " 'FNR==6 {printf $NF}') \ ./appimagetool-*.AppImage ./appdir - name: Move and Rename kew AppImage run: | mv kew*.AppImage output/kew chmod +x output/kew - name: Release uses: marvinpinto/action-automatic-releases@latest with: title: kew appImage (musl systems) automatic_release_tag: stable-musl prerelease: false draft: true files: | output/kew repo_token: ${{ secrets.GITHUB_TOKEN }} kew-3.2.0/.github/workflows/arm_macos.yml000066400000000000000000000013761500206121000203550ustar00rootroot00000000000000name: Build Check macOS on: pull_request: push: paths: - 'Makefile' workflow_dispatch: jobs: macos-build-check: name: macOS Build Check runs-on: macos-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Check system architecture run: uname -m - name: Install Homebrew run: | if ! command -v brew &> /dev/null; then /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" fi - name: Update Homebrew and install dependencies run: | brew update brew install faad2 taglib chafa fftw opus opusfile libogg libvorbis make curl - name: Build code run: make kew-3.2.0/.github/workflows/ci.yml000066400000000000000000000011641500206121000170020ustar00rootroot00000000000000name: Build Check on: pull_request: push: branches: - main jobs: ubuntu-build-check: name: Ubuntu 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 -y libcurl4-openssl-dev libtag1-dev libfaad-dev libogg-dev libfftw3-dev libopus-dev libopusfile-dev libvorbis-dev libchafa-dev libavformat-dev libstb-dev libglib2.0-dev - name: Build code run: make kew-3.2.0/.gitignore000066400000000000000000000004671500206121000142640ustar00rootroot00000000000000# 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-3.2.0/AUTHORS.md000066400000000000000000000026031500206121000137350ustar00rootroot00000000000000# AUTHORS ## Maintainers * **Ravachol** @ravachol * Founder and Main Author ## Contributors (in alphabetical order) * Chromium-3-Oxide * Davis @kazemaksOG * DNEGEL3125 @DNEGEL3125 * Hans Petter Jansson @hpjansson * John Lakeman @j-lakeman * Matthias Geiger @werdahias * Ravachol @ravachol * Rowan Shi @rowanxshi * Rui Chen @chenrui333 * Ruoyu Zhong @ZhongRuoyu * Ryouji @soryu-ryouji * Samuel @Samueru-sama * Vafone @vafone * Xplshn @xplshn * Zane Godden @mechatour ## Testers * Vafone @vafone * Nicolas F * Ravachol @ravachol ## Special Thanks We would like to extend our gratitude to the following individuals who have contributed significantly to kew: * Xplshn @xplshn * David Reid @mackron (author of Miniaudio, used for playing music) * Hans Petter Jansson @hpjansson (author of Chafa, used for displaying images) * Matthias Geiger @werdahias * Yuri @yurivict yuri@freebsd.org * Joey Schaff @jaoheah * Agustin Ballesteros @agus-balles * Mateo @malteo * Hampa @hampa * Markmark1 @markmark1 * VDawg @vdawg-git * INIROBO @inirobo kew-3.2.0/CHANGELOG.md000066400000000000000000000703321500206121000141030ustar00rootroot00000000000000# CHANGELOG ### 3.2.0 Now with a mini-player mode, a braille visualizer mode, a favorites list for radio stations, scrolling names that don't fit, the visualizer running at 60 fps, and much more! - New mini-player mode. Make the window small and it goes into this mode. Suggested by @HAPPIOcrz007. - The visualizer now runs at 60 fps instead of 10, making it much smoother. The rest of the ui runs slower much like before to save on system resources. - The visualizer can now be shown in braille mode (visualizerBrailleMode option in kewrc config file). - Track progress can now be shown as a thin line instead of dots. - Now shows sample rate and if relevant, bitrate, in track view. - A favorites list for radio stations. It's visible when the radio search field is empty. - Audio normalization for flac and mp3 files using replay gain metatags. Suggested by @endless-stupidity. - Long song names now scroll in the library and playlist. Suggested by @HAPPIOcrz007. - Press o to sort the library, either showing latest first or alphabetically. Suggested by @j-lakeman. - Radio search now refreshes the list as radio stations are found, making it less "laggy". - Track view works with radio now. - Added a stop command (shift+s). Space bar is play as before. - Removed the playback of tiny left overs of the previous song, when pausing and then switching song. - Added bitrate field to radio station list. - Added support for fullwidth characters. - Added repeat playlist option. Suggested by @HAPPIOcrz007. - Added option to set the visualizer so that the brightness of it depends on the height of the bar. By @Chromium-3-Oxide. - Added config option to disable mouse (in kewrc file). By @Chromium-3-Oxide. - Previous on first track now resets the track instead of stopping. - Code cleanup. By @Chromium-3-Oxide. #### Bug Fixes - Fixed deadlock when quickly and repeatedly starting a radio station. - Fixed bug with previous track with shuffle enabled. Found by @GuyInAShack. - Fixed bug with moving songs around, there was a case where it wasn't rebuilding the chain and the wrong song would get played. - Fixed bug with alt + mouse commands not working. By @Chromium-3-Oxide. ### 3.1.2 - Fix radio search sometimes freezing because of an invalid radio station URL. Found by joel. by @ravachol. - Added ability to play a song directly from the library (instead of just adding it to the playlist) by pressing Alt+Enter. Suggested by @PrivacyFriendlyMuffins. By @ravachol. - Added ability to disable the glimmering (flashing) last row. By @Chromium-3-Oxide. ### 3.1.1 - Reverts the command `kew path` to its previous behavior (exit on completion), which enables some automated tests to function again. By @ravachol. ### 3.1.0 Now with internet radio, mouse support and ability to move songs around in the playlist. #### Dependencies: - New dependency on libcurl. #### Changes: - Added Internet radio support. MP3 streams only so far, but the vast majority of streams are MP3 streams in the database we are using, others are excluded. Press F6 for radio search or Shift+B on macOS. By @ravachol. - Added mouse support. Use the middle button for playing or enqueueing a song. Right button to pause. This is configurable with plenty of options. By @Chromium-3-Oxide. - Move songs up and down the playlist with t and g. By @ravachol. Suggested By @HAPPIOcrz007. - Added support for m4a files using ALAC decoder. By @ravachol. - When the program exits previous commands and outputs are restored. By @DNEGEL3125. - Clear the entire playlist by pressing backspace. By @mechatour. - Added support for wav file covers. By @DNEGEL3125. - Made the app do less work when idle. By @ravachol. - The currently playing track is now underlined as well as bolded, because bold weight wasn't working with some fonts. Found By @yurivict. By @ravachol. - Added logic that enables running raw AAC files (but not HE-AAC). By @ravachol. - Added debugging flag to the makefile. Now to run make with debug symbols, run: make DEBUG=1 -ij4. - It's now possible to remove or alter the delay when printing the song title, in settings. By @Chromium-3-Oxide. - Added the config option of quitting after playing the playlist, same as --quitonstop flag. By @Chromium-3-Oxide. - Improved error message system. By @ravachol. - Reenabled seeking in ogg files. By @ravachol. #### Bug Fixes: - Fixed cover sometimes not centered in wezterm terminal. By @ravachol. - Fixed setting path on some machines doesn't work, returns 'path not found'. Found by @illnesse. - Fixed crash when in shuffle mode and choosing previous song on empty playlist. Found by @DNEGEL3125. - Fixed crash sometimes when pressing enter in track view. By @ravachol. - Fixed ogg vorbis playback sometimes leading to crash because there was no reliable way to tell if the song had ended. By @ravachol. - Fixed opus playback sometimes leading to crash because of a mixup with decoders. By @ravachol. - Uses a different method for detecting if kew is already running since the previous method didn't work on macOS. By @DNEGEL3125. - Prevent the cover from scrolling up on tmux+konsole. Found by @acdcbyl. By @ravachol. #### Special Thanks To These Sponsors: - @SpaceCheeseWizard - @nikolasdmtr - *one private sponsor* ### 3.0.3 - Fixed buffer redraw issue with cover images on ghostty. - Last Row is shown in the same place across all views. - The library text no longer shifts one char to the left sometimes when starting songs. - Fixed minor bug related to scrolling in library. - Fixed bug related to covers in ascii, on narrow terminal sizes it wouldn't print correctly. Found by @Hostuu. - Minor UI improvements, style adjustments and cleaning up. - Added play and stop icon, and replaced some nerdfont characters with unicode equivalents. - Disabled desktop notifications on macOS. The macOS desktop notifications didn't really gel well with the app, and the method used was unsafe in the long run. A better way to do it is by using objective-c, which I want to avoid using. ### 3.0.2 - You can now enqueue and play all your music (shuffled) in library view, by pressing MUSIC LIBRARY at the top. - Removed dependency on Libnotify because its' blocking in nature, and some users were experiencing freezes. Instead dbus is used directly if available and used with timeouts. Reported by @sina-salahshour. - Fixed bug introduced in 3.0.1 where songs whose titles start with a number would be sorted wrong. - Fixed music library folders containing spaces weren't being accepted. Found by @PoutineSyropErable. - Fixed bug where after finishing playing a playlist and then choosing a song in it, the next song would play twice. - Fixed kew all not being randomized properly. Found by @j-lakeman. - Fixed useConfigColors setting not being remembered. Found by @j-lakeman. - Added AUTHORS.md, DEVELOPERS.md and CHANGELOG.md files. - Dependencies Removed: Libnotify. ### 3.0.1 - Uses safer string functions. - Fixed bug where scrolling in the library would overshoot its place when it was scrolling through directories with lots of files. - Fixed mpris/dbus bug where some widgets weren't able to pause/play. - Fixed crash when playing very short samples in sequence. Found by @hampa. - Fixed order of songs in library was wrong in some cases. Found by @vincentcomfy. - Fixed bug related to switching songs while paused. - Fixed bug with being unable to rewind tracks to the start. Found by @INIROBO. - Seek while paused is now disabled. Problems found by @INIROBO. ### 3.0.0 This release comes with bigger changes than usual. If you have installed kew manually, you need to now install taglib, ogglib and, if you want, faad2 (for aac/m4a support) for this version (see the readme for instructions for your OS). - kew now works on macOS. The default terminal doesn't support colors and sixels fully, so installing a more modern terminal like kitty or wezterm is recommended. - Removed dependencies: FFmpeg, FreeImage. - Added Dependencies: Faad2, TagLib, Libogg. - These changes make kew lighter weight and makes it faster to install on macOS through brew. - Faad2 (which provides AAC decoding) is optional. By default, the build system will automatically detect if faad2 is available and include it if found. - More optimized and faster binary. Thank you @zamazan4ik for ideas. - Better support of Unicode strings. - Case-insensitive search for unicode strings. Thank you @valesnikov. - Fixed makefile and other things for building on all arches in Debian. Thank you so much @werdahias. - More efficient handling of input. - Added support for .m3u8 files. Thank you @mendhak for the suggestion. - Fixed bug where switching songs quickly, the cover in the desktop notification would remain the same. - Fixed issue with searching for nothing being broken. Thank you @Markmark1! Thank you so much @xplshn, @Vafone and @Markmark1 for help with testing. ### 3.0.0-rc1 This release comes with bigger changes than usual. If you have installed kew manually, you need to now install taglib, ogglib and, if you want, faad2 (for aac/m4a support) for this version (see the readme for instructions for your OS). - kew now works on macOS. The default terminal doesn't support colors and sixels fully, so installing a more modern terminal like kitty or wezterm is recommended. - Removed dependencies: FFmpeg, FreeImage - Added Dependencies: Faad2, TagLib, Ogglib - These changes makes kew lighter weight and makes it faster to install on macOS through brew. - Faad2 (which provides AAC decoding) is optional! By default, the build system will automatically detect if faad2 is available and include it if found. Disable with make USE_FAAD=0. - More optimized and faster binary. Thank you @zamazan4ik for ideas. - Better support of Unicode strings. - Fixed makefile and other things for building on all arches in Debian. Thank you @werdahias. - More efficient handling of input. - Added support for .m3u8 files. Thank you @mendhak for the suggestion. - Fixed bug where switching songs quickly, the cover in the desktop notification would remain the same. Thank you @xplshn and @Markmark1 for help with testing. Big thanks to everyone who helps report bugs! ### 2.8.2 - Fixed issue with building when libnotify is not installed. - Fixed build issue on FreeBSD. ### 2.8.1 New in this version: - Much nicer way to set the music library path on first use. - Checks at startup if the music library's modified time has changed when using cached library. If it has, update the library. Thanks @yurivict for the suggestion. - Improved search: kew now also shows the album name (directory name) of search results, for clarity. - You can now use TAB to cycle through the different views. - There's now a standalone executable AppImage for musl x86_64 systems. Thank you to @xplshn and @Samueru-sama for help with this. Bugfixes and other: - Added missing include file. Thank you @yurivict. - Don't repeat the song notification when enqueuing songs. A small but annoying bug that slipped into the last release. - Fixed issue where kew sometimes couldn't find the cover image in the folder. - Better handling of songs that cannot be initialized. - Removed support for .mp4 files so as to not add a bunch of video folders to the music library. Thanks @yurivict for the suggestion. - Made the makefile compatible with Void Linux. Thank you @itsdeadguy. - Cursor was not reappearing in some cases on FreeBSD after program exited. Thank you @yurivict. - Fixed slow loading UI on some machines, because of blocking desktop notification. Thanks @vdawg-git for reporting this. Thank you to @vdawg-git for helping me test and debug! Thank you also to @ZhongRuoyu! ### 2.8 New Features: - Much nicer way to set the music library path on first use. - Checks at startup if the music library's modified time has changed when using cached library. If it has, update the library. Thanks @yurivict for the suggestion. - Improved search: kew now also shows the album name (directory name) of search results, for clarity. - You can now use TAB to cycle through the different views. - There's now a standalone executable AppImage for musl x86_64 systems. Thank you to @xplshn and @Samueru-sama for help with this. Bugfixes and other: - Don't repeat the song notification when enqueuing songs. A small but annoying bug that slipped into the last release. - Fixed issue where kew sometimes couldn't find the cover image in the folder. - Better handling of songs that cannot be initialized. - Removed support for .mp4 files so as to not add a bunch of video folders to the music library. Thanks @yurivict for the suggestion. - Made the makefile compatible with Void Linux. Thank you @itsdeadguy. - Cursor was not reappearing in some cases on FreeBSD after program exited. Thank you @yurivict. - Fixed slow loading UI on some machines, because of blocking desktop notification. Thanks @vdawg-git for reporting this. Thank you to @vdawg-git for helping me test and debug! ### 2.7.2 - You can now remove the currently playing song from the playlist. Thank you @yurivict for the suggestion. You can then press space bar to play the next song in the list. - Scrolling now stops immediately after the key is released. - Better reset of the terminal at program exit. - MPRIS widgets are now updated when switching songs while paused. - When pressing update library ("u"), it now remembers which files are enqueued. - No more ugly scroll back buffer in the terminal. Btw, there is a bug in the KDE Media Player Widget which locks up plasmashell when certain songs play (in any music player). If you are having that problem, I suggest not using that widget until you have plasmashell version 6.20 or later. Bug description: https://bugs.kde.org/show_bug.cgi?id=491946. ### 2.7.1 - Added missing #ifdef for libnotify. This fixes #157. ### 2.7 This release adds: - Complete and corrected MPRIS implementation and support of playerCtl, except for opening Uris through mpris. - Libnotify as a new optional dependency. - Fixes to many minor issues that have cropped up. - Proper MPRIS and PlayerCtl support. Set volume, stop, seek and others now work as expected. You can also switch tracks while stopped or paused now. Everything should work except openUri and repeat playlist which are not available for now. - New (optional) dependency: Libnotify. In practice, adding libnotify as a dependency means browsing through music will no longer make desktop notifications pile up, instead the one visible will update itself. Thank you, @kazemaksOG, this looks much better. kew uses libnotify only if you have it installed, so it should if possible be an optional thing during installation. - Allows binding of other keys for the different ui views that you get with F2-F6. - Removed the option to toggle covers on and off by pressing 'c'. This led to confusion. - Removed build warning on systems with ffmpeg 4.4 installed. - Only run one instance of kew at a time, thanks @werdahias for the suggestion. - If you exit the kew with 0% volume, when you open it next time, volume will be at 10%. To avoid confusion. - Handle SIGHUP not only SIGINT. - Prints error message instead of crashing on Fedora (thanks @spambroo) when playing unsupported .m4a files. This problem is related to ffmpeg free/non-free versions. You need the non-free version. - Fixed issue where special characters in the song title could cause mpris widgets to not work correctly. ### 2.6 - New command: "kew albums", similar to "kew all" but it queues one album randomly after the other. Thank you @the-boar for the suggestion. - Fixed bug where sometimes kew couldn't find a suitable name for a playlist file (created by pressing x). - Made it so that seeking jumps in more reasonable smaller increments when not in song view. Previously it could jump 30 seconds at a time. - Rolled back code related to symlinked directories, it didn't work with freebsd, possibly others. ### 2.5.1 - Fixed bug where desktop notifications could stall the app if notify-send wasn't installed. Thank you @Visual-Dawg for reporting this and all the help testing! - Search: Removed duplicate search result name variable. This means search results will now have a very low memory footprint. - Symlinked directories should work better now. Works best if the symlink and the destination directory has the same name. ### 2.5 - Fuzzy search! Press F5 to search your library. - You can now quit with Esc. Handy when you are in search view, because pressing 'q' will just add that letter to the search string. - Fixed issue where after completing a playthrough of a playlist and then starting over, only the first song would be played. - Fine tuning of the spectrum visualizer. Still not perfect but I think this one is better. I might be wrong though. - Fixed issue where debian package tracker wasn't detecting LDFLAGS in the makefile. - Made scrolling quicker. ### 2.4.4 - Fixed no sound playing when playing a flac or mp3 song twice, then enqueuing another. - Don't save every change to the playlist when running the special playlist with 'kew .', only add the songs added by pressing '.'. - Removed compiler warning and a few other minor fixes. ### 2.4.3 - Fixed covers not being perfectly square on some terminals. - Fixed playlist selector could get 'stuck' after playing a long list. - Code refactoring and minor improvements to playlist view. - Moved the files kewrc and kewlibrary config files from XDG_CONFIG_HOME into XDG_CONFIG_HOME/kew/, typically ~/.config/kew. ### 2.4.2 - Fixed a few issues related to reading and writing the library. ### 2.4.1 - Improved album cover color mode. Press 'i' to try this. - To accelerate startup times, there is now a library cache. This feature is optional and can be enabled in the settings file (kewrc). If the library loading process is slow, you'll be prompted to consider using the cache. - You can now press 'u' to update the library in case you've added or removed songs. - Faster "kew all". It now bases its playlist on the library instead of scanning everything a second time. - Fixed when running the special playlist with "kew .", the app sometimes became unresponsive when adding / deleting. - Code refactoring and cleanup. ### 2.4 - Much faster song loading/skipping. - New settings: configurable colors. These are configured in the kewrc file (in ~/.config/ or wherever your config files are) with instructions there. - New setting: hidehelp. Hides the help text on library view and playlist view. - New setting: hidelogo. Prints the artist name as well as the song title at the top instead of a logo. - Fixed an issue with shuffle that could lead to a crash. - Fixed an issue where it could crash at the end of the playlist. - Fixed an issue where in some types of music libraries you couldn't scroll all the way to the bottom. - Fixed notifications not notifying on songs with spaces in cover art url. - Fixed sometimes not being able to switch song. - Further adjustments to the visualizer. - .aac and .mp4 file support. - New option: -q. Quits right after playing the playlist (same as --quitonstop). - Improved help text. ### 2.3.1 - The visualizer now (finally!) works like it's supposed to for all formats. - Proper clean up and restore cursor when using CTRL-C to quit the app. - Don't refresh track view twice when skipping to the previous song. ### 2.3 - Notifications of currently playing song through notify-send. New setting: allowNotifications. Set to 0 to disable notifications. - Fixed an issue that could lead to a crash when switching songs. - Fixed an issue with switching opus songs that could lead to a crash. - Plus other bug fixes. ### 2.2.1 - Fixed issue related to enqueuing/dequeuing the next song. - Some adapting for FreeBSD. ### 2.2.1 - Fixed issue related to enqueuing/dequeuing the next song. - Some adapting for FreeBSD. ### 2.2 - This update mostly contains improvements to stability. - M4a file decoding is no longer done by calling ffmpeg externally, it's (finally) done like the other file formats. This should make kew more stable, responsive and it should consume less memory when playing m4a files. - kew now starts the first time with your system volume as the volume, after that it remembers the last volume it exited with and uses that. - kew now picks up and starts using the cover color without the user having to first go to track view. ### 2.1.1 - Fixed a few issues related to passing cover art url and length to mpris. Should now display cover and progress correctly in widgets on gnome/wayland. ### 2.0.4 - You can now add "-e" or "--exact" in your searches to return an exact (not case sensitive) match. This can be helpful when two albums have a similar name, and you want to specify you want one or the other. Example: kew -e basement popstar. - Fixed issue where pressing del on the playlist changed view to track view. ### 2.0.3 - Fixed issue where sometimes the last of enqueued songs where being played instead of the first, - F4 is bound to show track view, and shown on the last row, so that the track view isn't hidden from new users. ### 2.0.1 - New view: Library. Shows the contents of the music library. From there you can add songs to the playlist. - Delete items from the playlist by pressing Del. - You can flip through pages of the library or playlist by pressing Page Up and Page Down. - Starting kew with no arguments now takes you to the library. - After the playlist finishes playing, the library is shown, instead of the app exiting. - To run kew with all songs shuffled like you could before, just type "kew all" in the terminal. - Running kew with the argument --quitonstop, enables the old behavior of exiting when finished. - Removed the playlist duration counter. It caused problems when coupled with the new features of being able to remove and add songs while audio is playing. - New ascii logo! This one takes up much less space. - kew now shows which song is playing on top of the library and playlist views. - Volume is now set at 50% at the start. - Also many bug fixes. ### 1.11 - Now shows volume percentage. - Fixed bug where on a small window size, the nerdfonts for seeking, repeat and shuffle when all three enabled could mess up the visualizer. ### 1.10 - Improved config file, with more information on how to make key bindings with special keys for instance. - Changing the volume is now for just kew, not the master volume of your system. - Switching songs now unpauses the player. - Fixed issue of potential crash when uninitializing decoders. ### 1.9 - Fixed a potential dead-lock situation. - Fixed one instance of wrong metadata/cover being displayed for a song on rare occasions. - Fixed an issue that could lead to a crash when switching songs. - Fixed issue of potential crash when closing audio file. - Fixed playlist showing the previous track as the current one. - Much improved memory allocation handling in visualizer. - Playlist builder now ignores hidden directories. ### 1.8.1 - Fixed bugs relating to showing the playlist and seeking. - Fixed bug where trying to seek on ogg files led to strange behavior. Now seeking in ogg is entirely disabled. - Fixed bug where kew for no reason stopped playing audio but kept counting elapsed seconds. - More colorful visualizer bars when using album cover colors. ### 1.8 - Visualizer bars now grow and decrease smoothly (if your terminal supports unicode). ### 1.7.4 - Kew is now interactive when paused. - Fixed issue with crashing after a few plays with repeat enabled. - Deletes cover images from cache after playing the file. ### 1.7.3 - Fixed issue with crash after seeking to the end of songs a few times. A lot was changed in 1.6.0 and 1.7.0 which led to some instability. ### 1.7.2 - Introduced Nerd Font glyphs for things like pause, repeat, fast forward and so on. - More fixes. ### 1.7.1 - Fixes a few issues in 1.7.0 ### 1.7.0 - Added decoders for ogg vorbis and opus. Seeking on ogg files doesn't yet work however. ### 1.6.0 - Now uses miniaudio's built-in decoders for mp3, flac and wav. ### 1.5.2 - Fix for unplayable songs. ### 1.5.1 - Misc issues with input handling resolved. - Faster seeking. ### 1.5 - Name changed to kew to resolve a name conflict. - Fixed bug with elapsed seconds being counted wrong if pausing multiple times. - Fixed bug with segfault on exit. ### 1.4 - Seeking is now possible, with A and D. - Config file now resides in your XDG_CONFIG_HOME, possibly ~/.config/kewrc. You can delete the one in your home folder after starting this version of cue once. - Most key bindings are now configurable. - Singing more visible in the visualizer. - Better looking visualizer with smoother gradient. - Misc fixes. - You can no longer press A to add to kew.m3u. instead use period. ### 1.3 - Now skips drm'd files more gracefully. - Improvements to thread safety and background process handling. - Misc bug fixes. - Using album colors is now the default again. ### 1.2 - It's now possible to scroll through the songs in the playlist menu. - Unfortunately this means a few key binding changes. Adjusting volume has been changed to +, -, instead up and down arrow is used for scrolling in the playlist menu. - h,l is now prev and next track (previously j and k). Now j,k is used for scrolling in the playlist menu. - Added a better check that metadata is correct before printing it, hopefully this fixes an occasional but annoying bug where the wrong metadata was sometimes displayed. - Using profile/theme colors is now the default choice for colors. ### 1.1 - Everything is now centered around the cover and left-aligned within that space. - Better visibility for text on white backgrounds. If colors are still too bright you can always press "i" and use the terminal colors, for now. - Playlist is now F2 and key bindings is F3 to help users who are using the terminator terminal and other terminals who might have help on the F1 key. - Now looks better in cases where there is no metadata and/or when there is no cover. - The window refreshes faster after resize. ### 1.0.9 - More colorful. It should be rarer for the color derived from the album cover to be gray/white - Press I to toggle using colors from the album cover or using colors from your terminal color scheme. - Smoother color transition on visualizer. ### 1.0.8 #### Features: - New Setting: useProfileColors. If set to 1 will match cue with your terminal profile colors instead of the album cover which remains the default. - It is now possible to switch songs a little quicker. - It's now faster (instant) to switch to playlist and key bindings views. #### Bug Fixes: - Skip to numbered song wasn't clearing the number array correctly. - Rapid typing of a song number wasn't being read correctly.. ### 1.0.7 - Fixed a bug where mpris stuff wasn't being cleaned up correctly. - More efficient printing to screen. - Better (refactored) cleanup. ### 1.0.6 - Fixed a bug where mpris stuff wasn't being cleaned up correctly ### 1.0.5 - Added a slow decay to the bars and made some other changes that should make the visualizer appear better, but this is all still experimental. - Some more VIM key bindings: 100G or 100g or 100+ENTER -> go to song 100 gg -> go to first song G -> go to last song ### 1.0.4 - Added man pages. - Added a few VIM key bindings (h,j,k,l) to use instead of arrow keys. - Shuffle now behaves like in other players, and works with MPRIS. Previously the list would be reordered, instead of the song just jumping from place to place, in the same list. Starting cue with 'cue shuffle ' still works the old way. - Now prints a R or S on the last line when repeat or shuffle is enabled. ### 1.0.3 - cue should now cleanly skip files it cannot play. Previously this caused instability to the app and made it become unresponsive. - Fixed a bug where the app sometimes became unresponsive, in relation to pausing/unpausing and pressing buttons while paused. ### 1.0.2: - Added support for MPRIS, which is the protocol used on Linux systems for controlling media players. - Added --nocover option. - Added --noui option. When it's used cue doesn't print anything to screen. - Now you can press K to see key bindings. - Fixed issue with long files being cut off. - Hiding cover and changing other settings, now clears the screen first. - Fixed installscript for opensuse. - Feature or bug? You can no longer raise the volume above 100% - New dependency: glib2. this was already required because chafa requires it, but it is now a direct dependency of cue. kew-3.2.0/CONTRIBUTING.md000066400000000000000000000043741500206121000145260ustar00rootroot00000000000000# CONTRIBUTING ## Welcome to kew contributing guide Thank you for your interest in 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 ### Create a pull request After making any changes, open a pull request on Github. - 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're fixing a particular bug in the issue list, please explicitly say "Fixes #" in your description". Once your PR has been reviewed and merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/ravachol/kew/graphs/contributors)! ### Issue assignment We don't have a process for assigning issues to contributors. Please feel free to jump into any issues in this repo that you are able to help with. Our intention is to encourage anyone to help without feeling burdened by an assigned task. Life can sometimes get in the way, and we don't want to leave contributors feeling obligated to complete issues when they may have limited time or unexpected commitments. We also recognize that not having a process could lead to competing or duplicate PRs. There's no perfect solution here. We encourage you to communicate early and often on an Issue to indicate that you're actively working on it. If you see that an Issue already has a PR, try working with that author instead of drafting your own. kew-3.2.0/DEVELOPERS.md000066400000000000000000000132171500206121000142630ustar00rootroot00000000000000# DEVELOPERS ## Getting started This document will help you setup your development environment. ### Prerequisites Before contributing, ensure you have the following tools installed on your development machine: - [GCC](https://gcc.gnu.org/) (or another compatible C/C++ compiler) - [Make](https://www.gnu.org/software/make/) - [Git](https://git-scm.com/) - [Valgrind](http://valgrind.org/) (optional, for memory debugging and profiling) - [VSCode](https://code.visualstudio.com/) (or other debugger) ### Building the Project 1. Clone the repository: ``` git clone https://github.com/ravachol/kew.git cd kew ``` 2. To enable debugging symbols, run make with DEBUG=1 3. Build the project: ``` make DEBUG=1 -j$(nproc) # Use all available processor cores for faster builds ``` ### Debugging with Visual Studio Code To enable debugging in VSCode, you'll need to create a `launch.json` file that configures the debugger. Follow these steps: 1. Open your project's folder in VSCode. 2. Press `F5` or go to the "Run and Debug" sidebar (`Ctrl+Shift+D` on Windows/Linux, `Cmd+Shift+D` on macOS), then click on the gear icon to create a new launch configuration file. 3. Select "C++ (GDB/LLDB)" as the debugger type, and choose your platform (e.g., x64-linux, x86-win32, etc.). 4. Replace the contents of the generated `launch.json` file with the following, adjusting paths and arguments as needed: ```json { "version": "0.2.0", "configurations": [ { "name": "kew", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/kew", //"args": ["artist or song name"], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": true, "MIMode": "gdb", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ] } ] } ``` 5. Save the `launch.json` file. 6. Create a c_cpp_properties.json file in the same folder (.vscode) with the following contents adjusting paths and arguments as needed: ```json { "configurations": [ { "name": "linux-gcc-x64", "includePath": [ "${workspaceFolder}/**", "/usr/include", "/usr/include/opus", "/usr/include/vorbis", "/usr/include/chafa/", "/usr/include/glib-2.0/**", "/usr/lib/chafa/include/**", "/usr/lib/glib-2.0/include/**" ], "defines": [ "_POSIX_C_SOURCE=200809L" ], "compilerPath": "/usr/bin/gcc", "cStandard": "${default}", "cppStandard": "${default}", "intelliSenseMode": "linux-gcc-x64" } ], "version": 4 } ``` 7. Add the extensions C/C++, C/C++ Extension pack, C/C++ Themes (optional). 8. Now you can use VSCode's debugger to step through your code, inspect variables, and analyze any issues: * Set breakpoints in your source code by placing your cursor on the desired line number, then press `F9`. * Press `F5` or click on the "Start Debugging" button (or go to the "Run and Debug" sidebar) to start the debugger. * When the execution reaches a breakpoint, VSCode will pause, allowing you to use its built-in features for debugging. #### Finding where libs are located If the paths in c_cpp_properties.json are wrong for your OS, to find the folder where for instance Chafa library is installed, you can use one of the following methods: 1. **Using `pkg-config`**: The `pkg-config` tool is a helper tool used to determine compiler flags and linker flags for libraries. You can use it to find the location of Chafa's include directory. Open your terminal and run the following command: ``` pkg-config --cflags chafa ``` This should display the `-I` flags required to include Chafa's headers, which in turn will reveal the installation prefix (e.g., `/usr/include/chafa/`). The folder containing the library files itself is typically located under `lib` or `lib64`, so you can find it by looking for a folder named `chafa` within those directories. 2. **Using `brew` (for macOS)**: If you installed Chafa using Homebrew, you can find its installation prefix with the following command: ``` brew --prefix chafa ``` This will display the installation prefix for Chafa (e.g., `/usr/local/opt/chafa`). 3. **Manually searching**: Alternatively, you can search your file system manually for the `chafa` folder or library files. On Unix-based systems like Linux and macOS, libraries are typically installed under `/usr`, `/usr/local`, or within the user's home directory (e.g., `~/.local`). You can use the `find` command to search for the folder: ``` find /usr /usr/local ~/.local -name chafa ``` This should display the location of the Chafa installation, revealing both the include and library folders. ### Valgrind To use Valgrind for memory debugging and profiling: 1. Build your project with debug symbols (`-g`) and position-independent executables (`-no-pie`): 2. Run Valgrind on your binary: ``` valgrind --leak-check=full --track-origins=yes --show-leak-kinds=all --log-file=valgrind-out.txt ./kew ``` ### Editorconfig - If you can, use EditorConfig for VS Code Extension. There is a file with settings for it: .editorconfig. ### Contributing For further information on how to contribute, see the file CONTRIBUTING.md. kew-3.2.0/LICENSE000066400000000000000000000432541500206121000133020ustar00rootroot00000000000000 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-3.2.0/Makefile000066400000000000000000000137731500206121000137400ustar00rootroot00000000000000CC ?= gcc CXX ?= g++ PKG_CONFIG ?= pkg-config # To enable debugging, run: # make DEBUG=1 # To disable DBUS notifications, run: # make USE_DBUS=0 # To disable faad2, run: # make USE_FAAD=0 # Detect system and architecture UNAME_S := $(shell uname -s) ARCH := $(shell uname -m) # Default USE_DBUS to auto-detect if not set by user ifeq ($(origin USE_DBUS), undefined) ifeq ($(UNAME_S), Darwin) USE_DBUS = 0 else USE_DBUS = 1 endif endif # Adjust the PREFIX for macOS and Linux ifeq ($(UNAME_S), Darwin) ifeq ($(ARCH), arm64) PREFIX ?= /usr/local PKG_CONFIG_PATH := /opt/homebrew/lib/pkgconfig:/opt/homebrew/share/pkgconfig:$(PKG_CONFIG_PATH) else PREFIX ?= /usr/local PKG_CONFIG_PATH := /usr/local/lib/pkgconfig:/usr/local/share/pkgconfig:$(PKG_CONFIG_PATH) endif else PREFIX ?= /usr PKG_CONFIG_PATH := /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig:$(PKG_CONFIG_PATH) endif # Default USE_FAAD to auto-detect if not set by user ifeq ($(origin USE_FAAD), undefined) USE_FAAD = $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) $(PKG_CONFIG) --exists faad && echo 1 || echo 0) ifeq ($(USE_FAAD), 0) # If pkg-config fails, try to find libfaad dynamically in common paths USE_FAAD = $(shell [ -f /usr/lib/libfaad.so ] || [ -f /usr/local/lib/libfaad.so ] || \ [ -f /opt/local/lib/libfaad.so ] || [ -f /opt/homebrew/lib/libfaad.dylib ] || \ [ -f /opt/homebrew/opt/faad2/lib/libfaad.dylib ] || \ [ -f /usr/local/lib/libfaad.dylib ] || [ -f /lib/x86_64-linux-gnu/libfaad.so.2 ] && echo 1 || echo 0) endif endif # Compiler flags COMMONFLAGS = -I/usr/include -I/opt/homebrew/include -I/usr/local/include -I/usr/lib -Iinclude/minimp4 \ -I/usr/include/chafa -I/usr/lib/chafa/include -I/usr/include/ogg -I/usr/include/opus \ -I/usr/include/stb -Iinclude/stb_image -Iinclude/alac/codec -I/usr/include/glib-2.0 \ -I/usr/lib/glib-2.0/include -Iinclude/miniaudio -I/usr/include/gdk-pixbuf-2.0 ifeq ($(DEBUG), 1) COMMONFLAGS += -g else COMMONFLAGS += -O2 endif COMMONFLAGS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) $(PKG_CONFIG) --cflags gio-2.0 chafa fftw3f opus opusfile vorbis ogg glib-2.0 taglib) COMMONFLAGS += -fstack-protector-strong -Wformat -Werror=format-security -fPIE -D_FORTIFY_SOURCE=2 COMMONFLAGS += -Wall -Wextra -Wpointer-arith -flto CFLAGS = $(COMMONFLAGS) # Compiler flags for C++ code CXXFLAGS = $(COMMONFLAGS) -std=c++11 # Libraries LIBS = -L/usr/lib -lm -lopusfile -lcurl -lglib-2.0 -lpthread $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) $(PKG_CONFIG) --libs gio-2.0 chafa fftw3f opus opusfile ogg vorbis vorbisfile glib-2.0 taglib) LIBS += -lstdc++ LDFLAGS = -logg -lz -flto ifeq ($(UNAME_S), Linux) CFLAGS += -fPIE -fstack-clash-protection CXXFLAGS += -fPIE -fstack-clash-protection LDFLAGS += -pie -Wl,-z,relro ifneq (,$(filter $(ARCH), x86_64 i386)) CFLAGS += -fcf-protection CXXFLAGS += -fcf-protection endif ifneq ($(DEBUG), 1) LDFLAGS += -s endif endif # Conditionally add USE_DBUS is enabled ifeq ($(USE_DBUS), 1) DEFINES += -DUSE_DBUS endif # Conditionally add faad2 support if USE_FAAD is enabled ifeq ($(USE_FAAD), 1) ifeq ($(ARCH), arm64) CFLAGS += -I/opt/homebrew/opt/faad2/include LIBS += -L/opt/homebrew/opt/faad2/lib -lfaad else CFLAGS += -I/usr/local/include LIBS += -L/usr/local/lib -lfaad endif DEFINES += -DUSE_FAAD endif ifeq ($(origin CC),default) CC := gcc endif ifneq ($(findstring gcc,$(CC)),) ifeq ($(UNAME_S), Linux) LIBS += -latomic endif endif OBJDIR = src/obj SRCS = src/common_ui.c src/common.c src/sound.c src/directorytree.c src/notifications.c \ src/soundcommon.c src/m4a.c src/search_ui.c src/soundradio.c src/searchradio_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/imgfunc.c src/cache.c src/songloader.c \ src/playlist.c src/term.c src/settings.c src/visuals.c src/kew.c # TagLib wrapper WRAPPER_SRC = src/tagLibWrapper.cpp WRAPPER_OBJ = $(OBJDIR)/tagLibWrapper.o MAN_PAGE = kew.1 MAN_DIR ?= $(PREFIX)/share/man all: kew # ALAC codec sources ALAC_SRCS_CPP = include/alac/codec/ALACDecoder.cpp include/alac/codec/alac_wrapper.cpp ALAC_SRCS_C = include/alac/codec/ag_dec.c include/alac/codec/ALACBitUtilities.c \ include/alac/codec/dp_dec.c include/alac/codec/EndianPortable.c \ include/alac/codec/matrix_dec.c # Generate object lists OBJS_C = $(SRCS:src/%.c=$(OBJDIR)/%.o) $(ALAC_SRCS_C:include/alac/codec/%.c=$(OBJDIR)/alac/%.o) OBJS_CPP = $(ALAC_SRCS_CPP:include/alac/codec/%.cpp=$(OBJDIR)/alac/%.o) # All objects together OBJS = $(OBJS_C) $(OBJS_CPP) # Create object directories $(OBJDIR): mkdir -p $(OBJDIR) $(OBJDIR)/alac ## Compile C sources $(OBJDIR)/%.o: src/%.c Makefile | $(OBJDIR) @mkdir -p $(dir $@) $(CC) $(CFLAGS) $(DEFINES) -c -o $@ $< # Compile ALAC C files $(OBJDIR)/alac/%.o: include/alac/codec/%.c Makefile | $(OBJDIR) @mkdir -p $(dir $@) $(CC) $(CFLAGS) $(DEFINES) -c -o $@ $< # Compile ALAC C++ files $(OBJDIR)/alac/%.o: include/alac/codec/%.cpp Makefile | $(OBJDIR) @mkdir -p $(dir $@) $(CXX) $(CXXFLAGS) $(DEFINES) -c -o $@ $< # Compile explicit C++ sources in src/ $(OBJDIR)/%.o: src/%.cpp Makefile | $(OBJDIR) @mkdir -p $(dir $@) $(CXX) $(CXXFLAGS) $(DEFINES) -c -o $@ $< # Compile TagLib wrapper C++ source $(WRAPPER_OBJ): $(WRAPPER_SRC) Makefile | $(OBJDIR) @mkdir -p $(dir $@) $(CXX) $(CXXFLAGS) $(DEFINES) -c $< -o $@ # Link all objects safely together using C++ linker kew: $(OBJS) $(WRAPPER_OBJ) Makefile $(CXX) -o kew $(OBJS) $(WRAPPER_OBJ) $(LIBS) $(LDFLAGS) .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-3.2.0/README.md000066400000000000000000000247371500206121000135610ustar00rootroot00000000000000# 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](images/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 terminal music player. ## 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. * Gapless playback (between files of the same format and type). * Supports MP3, FLAC, MPEG-4/M4A (AAC, ALAC), OPUS, OGG and WAV audio. * Supports desktop events through MPRIS. * Internet Radio. * Private, no data is collected by kew. ## Installing Packaging status ### Installing with package managers kew is available from Ubuntu 24.04. ```bash sudo apt install kew (Debian, Ubuntu) sudo yay -S kew (Arch Linux, Manjaro) sudo yay -S kew-git (Arch Linux, Manjaro) sudo zypper install kew (OpenSUSE) sudo pkg install kew (FreeBSD) brew install kew (macOS, Linux) apk add kew (Alpine Linux) ``` ### Building the project manually kew dependencies are: * FFTW * Chafa * libopus * opusfile * libvorbis * TagLib * faad2 (optional) * libogg * pkg-config * glib2.0 * curl Install these dependencies using your distro's package manager. Below are some examples. #### For Debian/Ubuntu: ```bash sudo apt install -y pkg-config libfaad-dev libtag1-dev libfftw3-dev libopus-dev libopusfile-dev libvorbis-dev libogg-dev git gcc make libchafa-dev libglib2.0-dev libcurl4-openssl-dev ``` #### For Arch Linux: ```bash sudo pacman -Syu --noconfirm --needed pkg-config faad2 taglib fftw git gcc make chafa glib2 opus opusfile libvorbis libogg curl ``` #### For macOS: Install git: ```bash xcode-select --install ``` Install dependencies: ```bash brew install gettext faad2 taglib chafa fftw opus opusfile libvorbis libogg glib pkg-config make curl ``` Notes for mac users: 1) A sixel-capable terminal like kitty or WezTerm is recommended for macOS. 2) The visualizer and album colors are disabled by default on macOS, because the default terminal doesn't handle them too well. To enable press v and i respectively. #### For Fedora: ```bash dnf install -y pkg-config taglib-devel fftw-devel opus-devel opusfile-devel libvorbis-devel libogg-devel git gcc make chafa-devel libatomic gcc-c++ glib2-devel libcurl-devel ``` Option: add faad2-devel for AAC,M4A support (Requires RPM-fusion to be enabled). #### For OpenSUSE: ```bash sudo zypper install -y pkg-config taglib-devel fftw-devel opus-devel opusfile-devel libvorbis-devel libogg-devel git chafa-devel gcc make glib2-devel libcurl-devel ``` Option: add libfaad-devel for AAC,M4A support (Requires Packman to be enabled). #### For CentOS/RHEL: ```bash sudo yum install -y pkgconfig taglib-devel fftw-devel opus-devel opusfile-devel libvorbis-devel libogg-devel git gcc make chafa-devel glib2-devel libcurl-devel ``` Option: add libfaad2-devel for AAC,M4A support (Probably requires EPEL to be enabled). #### For Solus: ```bash sudo eopkg install -y pkg-config faad2-devel taglib-devel fftw-devel opus-devel opusfile-devel libvorbis-devel libogg-devel git gcc make chafa-devel glib2-devel libcurl-devel ``` #### For Guix: ```bash guix install pkg-config faad2 taglib fftw git gcc make chafa opus opusfile libvorbis libogg glib libcurl ``` #### For Void Linux: ```bash sudo xbps-install -y pkg-config faad2 taglib taglib-devel fftw-devel git gcc make chafa chafa-devel opus opusfile opusfile-devel libvorbis-devel libogg glib-devel libcurl-devel ``` #### For Alpine Linux: ```bash sudo apk add pkgconfig faad2 faad2-dev taglib-dev fftw-dev opus opusfile libvorbis-dev libogg-dev git build-base chafa-dev glib-dev libcurl-dev ``` #### For Gentoo Linux: ```bash sudo emerge --ask pkgconf faad2 taglib fftw opus opusfile libvorbis libogg chafa dev-libs/glib curl ``` 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 -j4 ``` ```bash sudo make install ``` #### For Windows (WSL): 1) Install Windows Subsystem for Linux (WSL). 2) Install kew using the instructions for Ubuntu. 3) If you are running Windows 11, Pulseaudio should work out of the box, but if you are running Windows 10, use the instructions below for installing PulseAudio: https://www.reddit.com/r/bashonubuntuonwindows/comments/hrn1lz/wsl_sound_through_pulseaudio_solved/ 4) To install Pulseaudio as a service on Windows 10, follow the instructions at the bottom in this guide: https://www.linuxuprising.com/2021/03/how-to-get-sound-pulseaudio-to-work-on.html ### Uninstalling If you installed kew manually, simply run: ```bash sudo make uninstall ``` #### Faad2 is optional By default, the build system will automatically detect if `faad2` is available and includes it if found. ### Terminals 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/). ## Usage Run kew. It will first help you set the path to your music folder, then show you that folder's contents. kew can also be told to play a certain music from the command line. It automatically creates a playlist based on 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 you 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, if names collide, 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. shuffle needs to come first.) 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 kew path "/home/joe/Musik/" (changes the path) ``` Put single-quotes inside quotes "guns n' roses". #### Views Add songs to the playlist in Library View F3. See the playlist and select songs in Playlist View F2. See the song info and cover in Track View F4. Search music in Search View F5. Search internet radio in Radio Search View F6. See help in Help View F7. You can select all music by pressing the - MUSIC LIBRARY - header at the top of Library View. #### Key Bindings * Enter to select or replay a song. * Use + (or =), - keys to adjust the volume. * Use , or h, l keys to switch tracks. * Space, p or right mouse to play or pause. * Alt+s to stop. * F2 or Shift+z to show/hide playlist view. * F3 or Shift+x to show/hide library view. * F4 or Shift+c to show/hide track view. * F5 or Shift+v to show/hide search view. * F6 or Shift+b to show/hide internet radio search view. * F7 or Shift+n to show/hide key bindings view. * u to update the library. * v to toggle the spectrum visualizer. * 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 after playing. * 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. * Tab to switch to next view. * Shift+Tab to switch to previous view. * Backspace to clear the playlist. * Delete to remove a single playlist entry. * t, g to move songs up or down the playlist. * number + G or Enter to go to specific song number in the playlist. * . 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 or ~/Library/Preferences/kew on macOS. There you can change some settings like key bindings and the default colors of the app. To edit this file please make sure you quit kew first. ## Nerd Fonts kew looks better with Nerd Fonts: https://www.nerdfonts.com/. ## 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: Alac by Apple - https://github.com/macosforge/alac Chafa by Hans Petter Jansson - https://hpjansson.org/chafa/ Curl by Curl Team - https://github.com/curl/curl TagLib by TagLib Team - https://taglib.org/ faad2 by fabian_deb, knik, menno - https://sourceforge.net/projects/faac/ 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 Minimp4 by Lieff - https://github.com/lieff/minimp4 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-3.2.0/SECURITY.md000066400000000000000000000013021500206121000140520ustar00rootroot00000000000000# SECURITY ## 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 fix for the latest release. This fix will be released as fast as possible. You may be asked to provide further information in pursuit of a fix. kew-3.2.0/appdir/000077500000000000000000000000001500206121000135445ustar00rootroot00000000000000kew-3.2.0/appdir/kew.desktop000066400000000000000000000002051500206121000157220ustar00rootroot00000000000000[Desktop Entry] Name=kew Exec=kew Icon=kew Comment=Listen to music in the terminal. Terminal=true Type=Application Categories=Audio; kew-3.2.0/appdir/usr/000077500000000000000000000000001500206121000143555ustar00rootroot00000000000000kew-3.2.0/appdir/usr/share/000077500000000000000000000000001500206121000154575ustar00rootroot00000000000000kew-3.2.0/appdir/usr/share/applications/000077500000000000000000000000001500206121000201455ustar00rootroot00000000000000kew-3.2.0/appdir/usr/share/applications/kew.desktop000066400000000000000000000002041500206121000223220ustar00rootroot00000000000000[Desktop Entry] Name=kew Exec=kew Icon=kew Comment=Listen to music in the terminal. Terminal=true Type=Application Categories=Audio;kew-3.2.0/appdir/usr/share/icons/000077500000000000000000000000001500206121000165725ustar00rootroot00000000000000kew-3.2.0/appdir/usr/share/icons/hicolor/000077500000000000000000000000001500206121000202315ustar00rootroot00000000000000kew-3.2.0/appdir/usr/share/icons/hicolor/128x128/000077500000000000000000000000001500206121000211665ustar00rootroot00000000000000kew-3.2.0/appdir/usr/share/icons/hicolor/128x128/apps/000077500000000000000000000000001500206121000221315ustar00rootroot00000000000000kew-3.2.0/appdir/usr/share/icons/hicolor/128x128/apps/kew.png000066400000000000000000000056401500206121000234320ustar00rootroot00000000000000PNG  IHDR>aiCCPICC profile(}=H@_STD :EEkP :\MGbYWWAqvpRtZxp܏ww^fT2RɮFŀL}.Ls|׻>Ur&|"qEA߷_w\>o%uRRV^cՎ8 P?K#"VMH佯O*LicߏR~Vuk@VVu:8R\ L&L1§LERrrrqz$]jW(qqqv}kı^ou=bYrM{߼2NZǧ>jdS5{6̝'^[7ou!CEx~]YvrrB{ıgBpbWG /4!2<>-~`&ukի`0n# 0@bԘ1vn){|R"f͞ms!ص[71M( jtIRbAzJz%&mhV#]jz㍧Y%)ߺy& :Nr^^^t( CբOP\r֮ 4NO'4rՒti2h=c&/Xg;Q ,sx]mvϪ^΢L&(pQUY) 63gfgee"W*x}T|!̙7O˻՘v#2* UKrRչ+/[;[IJ -|'б&LL\)Ӧ`HÅs8*1uti2 gDUUr`?i ())mB(1!Fc>[|a]T ,NeOqܾ}[(ɠP($_^^o9"##ݮ P!߿/iߖmg@mLNN뾍6`Yd$(/@YY>z?HŐC PYYbѣvs8~H-^fp0_A7K[M PB PB PB PB PB PB PB PB PB PB PB @( o"pIENDB`kew-3.2.0/appdir/usr/share/icons/hicolor/256x256/000077500000000000000000000000001500206121000211725ustar00rootroot00000000000000kew-3.2.0/appdir/usr/share/icons/hicolor/256x256/apps/000077500000000000000000000000001500206121000221355ustar00rootroot00000000000000kew-3.2.0/appdir/usr/share/icons/hicolor/256x256/apps/kew.png000066400000000000000000000134731500206121000234410ustar00rootroot00000000000000PNG  IHDR\rfiCCPICC profile(}=H@_STD :EEkP :\MGbYWWAqvpRtZxp܏ww^fT2RɮFŀL}.Ls|׻>Ur&|"qEA=~V!}G H0 lBIddl Lf|;6In$ @ @ @ @ @@ @ @ @ @@ @ Z6m:w颉OfbDDFٌt:?[Pg|k0qJLLl5vtR^^/n6Lo5rCyyzW].tR €K7@ |\@W`ڰ . o/IZEEv6  ;wV:u-_2tS.LlTiIOkZrNlED]Nm55ղcd~t-hYzz3ի***tBNvUphKK}ϖwݣtAOP6ohkȐ=zݫ>@[6on:En~~~֖_/j}ݧrzjtVPPPqJNN:wJ[niܞ,/+o@ )Sez~~-۶?> EY,Ku8+KlEWth[JIIU``EM!BL‚((ȴ(+QJ$|֑ɓnzαcż!5$)9ESf+++nZ֛  1ܣݻtQ?2UtWv"?_ٯQwppt|E..ERFݭ~زҜٳ}%iGgqq"?_ ϟUq/==z'yn_}utzjZ_hXbcߕz 09S&LԄWHݻWO=9˲a4~yt啽L_;v+=,Z]8y}l6|EDF68]; qffN#а0Ѯ];+<xh&n[۶mլ3xLFM~~Mj۵uY/A󣔨h58={XdVhw73nSp9C$oY;L2n[֯׬3(]ޱcM 555z˴߾ftPUU^<*,,4L 4 5}6Mx`XCMFEM#nu˥U+W'fʇ_C4,k -[[e">!^znp' 73ӫku UM G, d/*EMl7 jZ1'?;˾[ez>6oh:M 2[;RR>蛙iLtǎ^IIħGd ..N.Rff?_]|-"0prvtYWVee;w0/5~>z͵ ,DMѹs|/ԣgOse.ӯ^^b_~kG}~>}|oz]TTTߵ[nh@[s{Ni?3g_k]CjZ%C2C<?[,0L )oX>έedYrwZg'Lи |N,  Q 1)$[~fUWnΝ WRWX 왑a\YyF5 wie4rmZvni߾tunͦ߷s6CAS=Ν=Қ5S\!aZKJJ2LKJJTd0 'Mc3#6GHHH>6M С, AAAOv e:7( ՍÆY|/ ӊ BBBڶmwﮧf?Ӣdupn(8*,0۵k+2 Si8ltf%W -b4x=-Z&Vi W24h`i4St8Ju,ih}u`mZؔhYMF]z9a8J+)9[q 4sc9pj/u҄=>cOfIIv^uʹXRX{̨UCQt\Znsgy\ MLL0\rҡC4k 'Xo;n\eZ^~ӳ1_fruVUWWwZZzUhr9] Եtɒz`mڴѨ;vh-_ҵ/\z(4uc=fR49^ ^aYg0\߰aM.ֽtj֭@jup-I'gFFW^?_sꑩS~VmW%K_=ӬVlzOS_6GirL.U\TD`ӯ^^RlM{l"""=Gi^Zt{;~C?j #""uwhҥsѣ;mLLxflz_w\ڰ~I*M77^פ&[摚fi=ݻviɋ5LvNǘt#de/,TII.L:tPTtBCCM;pNYYaúaiM^osԨz@\4olX дk =ukԙ34%%%ހu|:]\߾>M[6o<E J3V39b[dq{5ͦܢӤo߶MӦUr&|"qEA/?)\292Ϝ@l6~0abn2S]]c_By5A1oձiV>MIJ7r BAU>=pSC*v젢; 9AȤ: Egڵ( 9xe˗e q)rss1LLo V+*(~3fd޽D6^Y%)4az:E,\DKK [JQVcc4Fr ǣj4y Ex 1c%(X~=/L&mt11:t(99466RQմi¹s( t: "r9ccۉ .nc$L8؟o=rñ$$&|0  Y0|p<<=QSè!A:]]]?78f8jQ*KMEVqssnP\h<5~<8$I_nJ;.#c Fe66ɩ}:G!{.`fX_w'OrjƢhhlj:RSr]3 ˙9kfs[7jh4MĘ1 FPT4<Ӕttt0{-Nv\b0=''jP$&%VZӘ2 ???JKJ(,,DѢj98}R+V@yY_}vWWagF&2wknٰIsw/X;. l6JSf@j@mm-rj!|}³srJ>Dzi4xn|j|ҫr@AUҥ $ 7w^$1Ã$BeT0CAێ6ZR1J4t}=0B:{9>Q⣨@ @ @OY00IENDB`kew-3.2.0/docs/000077500000000000000000000000001500206121000132155ustar00rootroot00000000000000kew-3.2.0/docs/kew-manpage.mdoc000066400000000000000000000102511500206121000162540ustar00rootroot00000000000000.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 terminal 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. .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 path Changes the path to the music library. .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 Shift+s Stop. .It F2 or Shift+z Show playlist view .It F3 or Shift+x Show library view .It F4 or Shift+c Show track view .It F5 or Shift+v Show search view .It F6 or Shift+b Show radio search view. .It F7 or Shift+n Show key bindings view .It u Update the libarary. .It i Toggle colors derived from album cover or from color theme. .It v Toggle spectrum visualizer. .It b Switch between ascii and image album cover. .It r Repeat current song after playing. .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 Tab Switch to next view. .It Shift+Tab Switch to previous view. .It Backspace Clear the playlist. .It Delete Remove a single playlist entry. .It t,g Move a song up or down the playlist. .It number + G or Enter Go to specific song number in the playlist. .It Esc or q 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-3.2.0/docs/kew.1000066400000000000000000000075641500206121000141010ustar00rootroot00000000000000.\" 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 terminal 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 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 path ,\fR Sets the path to the music library. .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 Play, Pause. .TP 9n Shift+s Stop. .TP 9n F2 or Shift+z Show playlist view. .TP 9n F3 or Shift+x Show library view. .TP 9n F4 or Shift+c Show track view. .TP 9n F5 or Shift+v Show search view. .TP 9n F6 or Shift+b Show radio search view. .TP 9n F7 or Shift+n 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 v Toggle spectrum visualizer. .TP 9n b Switch between ascii and image album cover. .TP 9n r Repeat current song after playing. .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 Tab Switch to next view. .TP 9n Shift+Tab Switch to previous view. .TP 9n Backspace Clear the playlist. .TP 9n Delete Remove a single playlist entry. .TP 9n t,g Move a song up or down the playlist. .TP 9n number + G or Enter Go to specific song number in the playlist. .TP 9n Esc or q 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-3.2.0/images/000077500000000000000000000000001500206121000135325ustar00rootroot00000000000000kew-3.2.0/images/kew-screenshot.png000066400000000000000000005543121500206121000172130ustar00rootroot00000000000000PNG  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-3.2.0/include/000077500000000000000000000000001500206121000137105ustar00rootroot00000000000000kew-3.2.0/include/alac/000077500000000000000000000000001500206121000146105ustar00rootroot00000000000000kew-3.2.0/include/alac/LICENSE000066400000000000000000000216301500206121000156170ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. kew-3.2.0/include/alac/codec/000077500000000000000000000000001500206121000156655ustar00rootroot00000000000000kew-3.2.0/include/alac/codec/ALACAudioTypes.h000066400000000000000000000127351500206121000205550ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ /* File changed from the original to supress some warnings */ /* File: ALACAudioTypes.h */ #ifndef ALACAUDIOTYPES_H #define ALACAUDIOTYPES_H #if PRAGMA_ONCE #pragma once #endif #ifdef __cplusplus extern "C" { #endif #if PRAGMA_STRUCT_ALIGN #pragma options align=mac68k #elif PRAGMA_STRUCT_PACKPUSH #pragma pack(push, 2) #elif PRAGMA_STRUCT_PACK #pragma pack(2) #endif #include #if defined(__ppc__) #define TARGET_RT_BIG_ENDIAN 1 #elif defined(__ppc64__) #define TARGET_RT_BIG_ENDIAN 1 #endif #define kChannelAtomSize 12 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmultichar" enum { kALAC_UnimplementedError = -4, kALAC_FileNotFoundError = -43, kALAC_ParamError = -50, kALAC_MemFullError = -108 }; enum { kALACFormatAppleLossless = 'alac', kALACFormatLinearPCM = 'lpcm' }; enum { kALACMaxChannels = 8, kALACMaxEscapeHeaderBytes = 8, kALACMaxSearches = 16, kALACMaxCoefs = 16, kALACDefaultFramesPerPacket = 4096 }; typedef uint32_t ALACChannelLayoutTag; enum { kALACFormatFlagIsFloat = (1 << 0), // 0x1 kALACFormatFlagIsBigEndian = (1 << 1), // 0x2 kALACFormatFlagIsSignedInteger = (1 << 2), // 0x4 kALACFormatFlagIsPacked = (1 << 3), // 0x8 kALACFormatFlagIsAlignedHigh = (1 << 4), // 0x10 }; enum { #if TARGET_RT_BIG_ENDIAN kALACFormatFlagsNativeEndian = kALACFormatFlagIsBigEndian #else kALACFormatFlagsNativeEndian = 0 #endif }; // this is required to be an IEEE 64bit float typedef double alac_float64_t; // These are the Channel Layout Tags used in the Channel Layout Info portion of the ALAC magic cookie enum { kALACChannelLayoutTag_Mono = (100<<16) | 1, // C kALACChannelLayoutTag_Stereo = (101<<16) | 2, // L R kALACChannelLayoutTag_MPEG_3_0_B = (113<<16) | 3, // C L R kALACChannelLayoutTag_MPEG_4_0_B = (116<<16) | 4, // C L R Cs kALACChannelLayoutTag_MPEG_5_0_D = (120<<16) | 5, // C L R Ls Rs kALACChannelLayoutTag_MPEG_5_1_D = (124<<16) | 6, // C L R Ls Rs LFE kALACChannelLayoutTag_AAC_6_1 = (142<<16) | 7, // C L R Ls Rs Cs LFE kALACChannelLayoutTag_MPEG_7_1_B = (127<<16) | 8 // C Lc Rc L R Ls Rs LFE (doc: IS-13818-7 MPEG2-AAC) }; // ALAC currently only utilizes these channels layouts. There is a one for one correspondance between a // given number of channels and one of these layout tags static const ALACChannelLayoutTag ALACChannelLayoutTags[kALACMaxChannels] = { kALACChannelLayoutTag_Mono, // C kALACChannelLayoutTag_Stereo, // L R kALACChannelLayoutTag_MPEG_3_0_B, // C L R kALACChannelLayoutTag_MPEG_4_0_B, // C L R Cs kALACChannelLayoutTag_MPEG_5_0_D, // C L R Ls Rs kALACChannelLayoutTag_MPEG_5_1_D, // C L R Ls Rs LFE kALACChannelLayoutTag_AAC_6_1, // C L R Ls Rs Cs LFE kALACChannelLayoutTag_MPEG_7_1_B // C Lc Rc L R Ls Rs LFE (doc: IS-13818-7 MPEG2-AAC) }; // AudioChannelLayout from CoreAudioTypes.h. We never need the AudioChannelDescription so we remove it struct ALACAudioChannelLayout { ALACChannelLayoutTag mChannelLayoutTag; uint32_t mChannelBitmap; uint32_t mNumberChannelDescriptions; }; typedef struct ALACAudioChannelLayout ALACAudioChannelLayout; struct AudioFormatDescription { alac_float64_t mSampleRate; uint32_t mFormatID; uint32_t mFormatFlags; uint32_t mBytesPerPacket; uint32_t mFramesPerPacket; uint32_t mBytesPerFrame; uint32_t mChannelsPerFrame; uint32_t mBitsPerChannel; uint32_t mReserved; }; typedef struct AudioFormatDescription AudioFormatDescription; /* Lossless Definitions */ enum { kALACCodecFormat = 'alac', kALACVersion = 0, kALACCompatibleVersion = kALACVersion, kALACDefaultFrameSize = 4096 }; // note: this struct is wrapped in an 'alac' atom in the sample description extension area // note: in QT movies, it will be further wrapped in a 'wave' atom surrounded by 'frma' and 'term' atoms typedef struct ALACSpecificConfig { uint32_t frameLength; uint8_t compatibleVersion; uint8_t bitDepth; // max 32 uint8_t pb; // 0 <= pb <= 255 uint8_t mb; uint8_t kb; uint8_t numChannels; uint16_t maxRun; uint32_t maxFrameBytes; uint32_t avgBitRate; uint32_t sampleRate; } ALACSpecificConfig; // The AudioChannelLayout atom type is not exposed yet so define it here enum { AudioChannelLayoutAID = 'chan' }; #pragma GCC diagnostic pop #if PRAGMA_STRUCT_ALIGN #pragma options align=reset #elif PRAGMA_STRUCT_PACKPUSH #pragma pack(pop) #elif PRAGMA_STRUCT_PACK #pragma pack() #endif #ifdef __cplusplus } #endif #endif /* ALACAUDIOTYPES_H */ kew-3.2.0/include/alac/codec/ALACBitUtilities.c000066400000000000000000000126521500206121000210720ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ /*============================================================================= File: ALACBitUtilities.c $NoKeywords: $ =============================================================================*/ #include #include "ALACBitUtilities.h" // BitBufferInit // void BitBufferInit( BitBuffer * bits, uint8_t * buffer, uint32_t byteSize ) { bits->cur = buffer; bits->end = bits->cur + byteSize; bits->bitIndex = 0; bits->byteSize = byteSize; } // BitBufferRead // uint32_t BitBufferRead( BitBuffer * bits, uint8_t numBits ) { uint32_t returnBits; //Assert( numBits <= 16 ); returnBits = ((uint32_t)bits->cur[0] << 16) | ((uint32_t)bits->cur[1] << 8) | ((uint32_t)bits->cur[2]); returnBits = returnBits << bits->bitIndex; returnBits &= 0x00FFFFFF; bits->bitIndex += numBits; returnBits = returnBits >> (24 - numBits); bits->cur += (bits->bitIndex >> 3); bits->bitIndex &= 7; //Assert( bits->cur <= bits->end ); return returnBits; } // BitBufferReadSmall // // Reads up to 8 bits uint8_t BitBufferReadSmall( BitBuffer * bits, uint8_t numBits ) { uint16_t returnBits; //Assert( numBits <= 8 ); returnBits = (bits->cur[0] << 8) | bits->cur[1]; returnBits = returnBits << bits->bitIndex; bits->bitIndex += numBits; returnBits = returnBits >> (16 - numBits); bits->cur += (bits->bitIndex >> 3); bits->bitIndex &= 7; //Assert( bits->cur <= bits->end ); return (uint8_t)returnBits; } // BitBufferReadOne // // Reads one byte uint8_t BitBufferReadOne( BitBuffer * bits ) { uint8_t returnBits; returnBits = (bits->cur[0] >> (7 - bits->bitIndex)) & 1; bits->bitIndex++; bits->cur += (bits->bitIndex >> 3); bits->bitIndex &= 7; //Assert( bits->cur <= bits->end ); return returnBits; } // BitBufferPeek // uint32_t BitBufferPeek( BitBuffer * bits, uint8_t numBits ) { return ((((((uint32_t) bits->cur[0] << 16) | ((uint32_t) bits->cur[1] << 8) | ((uint32_t) bits->cur[2])) << bits->bitIndex) & 0x00FFFFFF) >> (24 - numBits)); } // BitBufferPeekOne // uint32_t BitBufferPeekOne( BitBuffer * bits ) { return ((bits->cur[0] >> (7 - bits->bitIndex)) & 1); } // BitBufferUnpackBERSize // uint32_t BitBufferUnpackBERSize( BitBuffer * bits ) { uint32_t size; uint8_t tmp; for ( size = 0, tmp = 0x80u; tmp &= 0x80u; size = (size << 7u) | (tmp & 0x7fu) ) tmp = (uint8_t) BitBufferReadSmall( bits, 8 ); return size; } // BitBufferGetPosition // uint32_t BitBufferGetPosition( BitBuffer * bits ) { uint8_t * begin; begin = bits->end - bits->byteSize; return ((uint32_t)(bits->cur - begin) * 8) + bits->bitIndex; } // BitBufferByteAlign // void BitBufferByteAlign( BitBuffer * bits, int32_t addZeros ) { // align bit buffer to next byte boundary, writing zeros if requested if ( bits->bitIndex == 0 ) return; if ( addZeros ) BitBufferWrite( bits, 0, 8 - bits->bitIndex ); else BitBufferAdvance( bits, 8 - bits->bitIndex ); } // BitBufferAdvance // void BitBufferAdvance( BitBuffer * bits, uint32_t numBits ) { if ( numBits ) { bits->bitIndex += numBits; bits->cur += (bits->bitIndex >> 3); bits->bitIndex &= 7; } } // BitBufferRewind // void BitBufferRewind( BitBuffer * bits, uint32_t numBits ) { uint32_t numBytes; if ( numBits == 0 ) return; if ( bits->bitIndex >= numBits ) { bits->bitIndex -= numBits; return; } numBits -= bits->bitIndex; bits->bitIndex = 0; numBytes = numBits / 8; numBits = numBits % 8; bits->cur -= numBytes; if ( numBits > 0 ) { bits->bitIndex = 8 - numBits; bits->cur--; } if ( bits->cur < (bits->end - bits->byteSize) ) { //DebugCMsg("BitBufferRewind: Rewound too far."); bits->cur = (bits->end - bits->byteSize); bits->bitIndex = 0; } } // BitBufferWrite // void BitBufferWrite( BitBuffer * bits, uint32_t bitValues, uint32_t numBits ) { uint32_t invBitIndex; RequireAction( bits != nil, return; ); RequireActionSilent( numBits > 0, return; ); invBitIndex = 8 - bits->bitIndex; while ( numBits > 0 ) { uint32_t tmp; uint8_t shift; uint8_t mask; uint32_t curNum; curNum = MIN( invBitIndex, numBits ); tmp = bitValues >> (numBits - curNum); shift = (uint8_t)(invBitIndex - curNum); mask = 0xffu >> (8 - curNum); // must be done in two steps to avoid compiler sequencing ambiguity mask <<= shift; bits->cur[0] = (bits->cur[0] & ~mask) | (((uint8_t) tmp << shift) & mask); numBits -= curNum; // increment to next byte if need be invBitIndex -= curNum; if ( invBitIndex == 0 ) { invBitIndex = 8; bits->cur++; } } bits->bitIndex = 8 - invBitIndex; } void BitBufferReset( BitBuffer * bits ) //void BitBufferInit( BitBuffer * bits, uint8_t * buffer, uint32_t byteSize ) { bits->cur = bits->end - bits->byteSize; bits->bitIndex = 0; } #if PRAGMA_MARK #pragma mark - #endif kew-3.2.0/include/alac/codec/ALACBitUtilities.h000066400000000000000000000056641500206121000211040ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ /*============================================================================= File: ALACBitUtilities.h $NoKeywords: $ =============================================================================*/ #ifndef __ALACBITUTILITIES_H #define __ALACBITUTILITIES_H #include #ifndef MIN #define MIN(x, y) ( (x)<(y) ?(x) :(y) ) #endif //MIN #ifndef MAX #define MAX(x, y) ( (x)>(y) ?(x): (y) ) #endif //MAX #ifndef nil #define nil NULL #endif #define RequireAction(condition, action) if (!(condition)) { action } #define RequireActionSilent(condition, action) if (!(condition)) { action } #define RequireNoErr(condition, action) if ((condition)) { action } #ifdef __cplusplus extern "C" { #endif enum { ALAC_noErr = 0 }; typedef enum { ID_SCE = 0, /* Single Channel Element */ ID_CPE = 1, /* Channel Pair Element */ ID_CCE = 2, /* Coupling Channel Element */ ID_LFE = 3, /* LFE Channel Element */ ID_DSE = 4, /* not yet supported */ ID_PCE = 5, ID_FIL = 6, ID_END = 7 } ELEMENT_TYPE; // types typedef struct BitBuffer { uint8_t * cur; uint8_t * end; uint32_t bitIndex; uint32_t byteSize; } BitBuffer; /* BitBuffer routines - these routines take a fixed size buffer and read/write to it - bounds checking must be done by the client */ void BitBufferInit( BitBuffer * bits, uint8_t * buffer, uint32_t byteSize ); uint32_t BitBufferRead( BitBuffer * bits, uint8_t numBits ); // note: cannot read more than 16 bits at a time uint8_t BitBufferReadSmall( BitBuffer * bits, uint8_t numBits ); uint8_t BitBufferReadOne( BitBuffer * bits ); uint32_t BitBufferPeek( BitBuffer * bits, uint8_t numBits ); // note: cannot read more than 16 bits at a time uint32_t BitBufferPeekOne( BitBuffer * bits ); uint32_t BitBufferUnpackBERSize( BitBuffer * bits ); uint32_t BitBufferGetPosition( BitBuffer * bits ); void BitBufferByteAlign( BitBuffer * bits, int32_t addZeros ); void BitBufferAdvance( BitBuffer * bits, uint32_t numBits ); void BitBufferRewind( BitBuffer * bits, uint32_t numBits ); void BitBufferWrite( BitBuffer * bits, uint32_t value, uint32_t numBits ); void BitBufferReset( BitBuffer * bits); #ifdef __cplusplus } #endif #endif /* __BITUTILITIES_H */ kew-3.2.0/include/alac/codec/ALACDecoder.cpp000066400000000000000000000531561500206121000203710ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ /* File changed from the original to supress some warnings */ /* File: ALACDecoder.cpp */ #include #include #include "ALACDecoder.h" #include "dplib.h" #include "aglib.h" #include "matrixlib.h" #include "ALACBitUtilities.h" #include "EndianPortable.h" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-but-set-variable" // constants/data const uint32_t kMaxBitDepth = 32; // max allowed bit depth is 32 // prototypes static void Zero16( int16_t * buffer, uint32_t numItems, uint32_t stride ); static void Zero24( uint8_t * buffer, uint32_t numItems, uint32_t stride ); static void Zero32( int32_t * buffer, uint32_t numItems, uint32_t stride ); /* Constructor */ ALACDecoder::ALACDecoder() : mMixBufferU( nil ), mMixBufferV( nil ), mPredictor( nil ), mShiftBuffer( nil ) { memset( &mConfig, 0, sizeof(mConfig) ); } /* Destructor */ ALACDecoder::~ALACDecoder() { // delete the matrix mixing buffers if ( mMixBufferU ) { free(mMixBufferU); mMixBufferU = NULL; } if ( mMixBufferV ) { free(mMixBufferV); mMixBufferV = NULL; } // delete the dynamic predictor's "corrector" buffer // - note: mShiftBuffer shares memory with this buffer if ( mPredictor ) { free(mPredictor); mPredictor = NULL; } } /* Init() - initialize the decoder with the given configuration */ int32_t ALACDecoder::Init( void * inMagicCookie, uint32_t inMagicCookieSize ) { int32_t status = ALAC_noErr; ALACSpecificConfig theConfig; uint8_t * theActualCookie = (uint8_t *)inMagicCookie; uint32_t theCookieBytesRemaining = inMagicCookieSize; // For historical reasons the decoder needs to be resilient to magic cookies vended by older encoders. // As specified in the ALACMagicCookieDescription.txt document, there may be additional data encapsulating // the ALACSpecificConfig. This would consist of format ('frma') and 'alac' atoms which precede the // ALACSpecificConfig. // See ALACMagicCookieDescription.txt for additional documentation concerning the 'magic cookie' // skip format ('frma') atom if present if (theActualCookie[4] == 'f' && theActualCookie[5] == 'r' && theActualCookie[6] == 'm' && theActualCookie[7] == 'a') { theActualCookie += 12; theCookieBytesRemaining -= 12; } // skip 'alac' atom header if present if (theActualCookie[4] == 'a' && theActualCookie[5] == 'l' && theActualCookie[6] == 'a' && theActualCookie[7] == 'c') { theActualCookie += 12; theCookieBytesRemaining -= 12; } // read the ALACSpecificConfig if (theCookieBytesRemaining >= sizeof(ALACSpecificConfig)) { theConfig.frameLength = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->frameLength); theConfig.compatibleVersion = ((ALACSpecificConfig *)theActualCookie)->compatibleVersion; theConfig.bitDepth = ((ALACSpecificConfig *)theActualCookie)->bitDepth; theConfig.pb = ((ALACSpecificConfig *)theActualCookie)->pb; theConfig.mb = ((ALACSpecificConfig *)theActualCookie)->mb; theConfig.kb = ((ALACSpecificConfig *)theActualCookie)->kb; theConfig.numChannels = ((ALACSpecificConfig *)theActualCookie)->numChannels; theConfig.maxRun = Swap16BtoN(((ALACSpecificConfig *)theActualCookie)->maxRun); theConfig.maxFrameBytes = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->maxFrameBytes); theConfig.avgBitRate = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->avgBitRate); theConfig.sampleRate = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->sampleRate); mConfig = theConfig; RequireAction( mConfig.compatibleVersion <= kALACVersion, return kALAC_ParamError; ); // allocate mix buffers mMixBufferU = (int32_t *) calloc( mConfig.frameLength * sizeof(int32_t), 1 ); mMixBufferV = (int32_t *) calloc( mConfig.frameLength * sizeof(int32_t), 1 ); // allocate dynamic predictor buffer mPredictor = (int32_t *) calloc( mConfig.frameLength * sizeof(int32_t), 1 ); // "shift off" buffer shares memory with predictor buffer mShiftBuffer = (uint16_t *) mPredictor; RequireAction( (mMixBufferU != nil) && (mMixBufferV != nil) && (mPredictor != nil), status = kALAC_MemFullError; goto Exit; ); } else { status = kALAC_ParamError; } // skip to Channel Layout Info // theActualCookie += sizeof(ALACSpecificConfig); // Currently, the Channel Layout Info portion of the magic cookie (as defined in the // ALACMagicCookieDescription.txt document) is unused by the decoder. Exit: return status; } /* Decode() - the decoded samples are interleaved into the output buffer in the order they arrive in the bitstream */ int32_t ALACDecoder::Decode( BitBuffer * bits, uint8_t * sampleBuffer, uint32_t numSamples, uint32_t numChannels, uint32_t * outNumSamples ) { BitBuffer shiftBits; uint32_t bits1, bits2; uint8_t tag; uint8_t elementInstanceTag; AGParamRec agParams; uint32_t channelIndex; int16_t coefsU[32]; // max possible size is 32 although NUMCOEPAIRS is the current limit int16_t coefsV[32]; uint8_t numU, numV; uint8_t mixBits; int8_t mixRes; uint16_t unusedHeader; uint8_t escapeFlag; uint32_t chanBits; uint8_t bytesShifted; uint32_t shift; uint8_t modeU, modeV; uint32_t denShiftU, denShiftV; uint16_t pbFactorU, pbFactorV; uint16_t pb; int16_t * samples; int16_t * out16; uint8_t * out20; uint8_t * out24; int32_t * out32; uint8_t headerByte; uint8_t partialFrame; uint32_t extraBits; int32_t val; uint32_t i, j; int32_t status; RequireAction( (bits != nil) && (sampleBuffer != nil) && (outNumSamples != nil), return kALAC_ParamError; ); RequireAction( numChannels > 0, return kALAC_ParamError; ); mActiveElements = 0; channelIndex = 0; samples = (int16_t *) sampleBuffer; status = ALAC_noErr; *outNumSamples = numSamples; while ( status == ALAC_noErr ) { // bail if we ran off the end of the buffer RequireAction( bits->cur < bits->end, status = kALAC_ParamError; goto Exit; ); // copy global decode params for this element pb = mConfig.pb; // read element tag tag = BitBufferReadSmall( bits, 3 ); switch ( tag ) { case ID_SCE: case ID_LFE: { // mono/LFE channel elementInstanceTag = BitBufferReadSmall( bits, 4 ); mActiveElements |= (1u << elementInstanceTag); // read the 12 unused header bits unusedHeader = (uint16_t) BitBufferRead( bits, 12 ); RequireAction( unusedHeader == 0, status = kALAC_ParamError; goto Exit; ); // read the 1-bit "partial frame" flag, 2-bit "shift-off" flag & 1-bit "escape" flag headerByte = (uint8_t) BitBufferRead( bits, 4 ); partialFrame = headerByte >> 3; bytesShifted = (headerByte >> 1) & 0x3u; RequireAction( bytesShifted != 3, status = kALAC_ParamError; goto Exit; ); shift = bytesShifted * 8; escapeFlag = headerByte & 0x1; chanBits = mConfig.bitDepth - (bytesShifted * 8); // check for partial frame to override requested numSamples if ( partialFrame != 0 ) { numSamples = BitBufferRead( bits, 16 ) << 16; numSamples |= BitBufferRead( bits, 16 ); } if ( escapeFlag == 0 ) { // compressed frame, read rest of parameters mixBits = (uint8_t) BitBufferRead( bits, 8 ); mixRes = (int8_t) BitBufferRead( bits, 8 ); //Assert( (mixBits == 0) && (mixRes == 0) ); // no mixing for mono headerByte = (uint8_t) BitBufferRead( bits, 8 ); modeU = headerByte >> 4; denShiftU = headerByte & 0xfu; headerByte = (uint8_t) BitBufferRead( bits, 8 ); pbFactorU = headerByte >> 5; numU = headerByte & 0x1fu; for ( i = 0; i < numU; i++ ) coefsU[i] = (int16_t) BitBufferRead( bits, 16 ); // if shift active, skip the the shift buffer but remember where it starts if ( bytesShifted != 0 ) { shiftBits = *bits; BitBufferAdvance( bits, (bytesShifted * 8) * numSamples ); } // decompress set_ag_params( &agParams, mConfig.mb, (pb * pbFactorU) / 4, mConfig.kb, numSamples, numSamples, mConfig.maxRun ); status = dyn_decomp( &agParams, bits, mPredictor, numSamples, chanBits, &bits1 ); RequireNoErr( status, goto Exit; ); if ( modeU == 0 ) { unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU ); } else { // the special "numActive == 31" mode can be done in-place unpc_block( mPredictor, mPredictor, numSamples, nil, 31, chanBits, 0 ); unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU ); } } else { //Assert( bytesShifted == 0 ); // uncompressed frame, copy data into the mix buffer to use common output code shift = 32 - chanBits; if ( chanBits <= 16 ) { for ( i = 0; i < numSamples; i++ ) { val = (int32_t) BitBufferRead( bits, (uint8_t) chanBits ); val = (val << shift) >> shift; mMixBufferU[i] = val; } } else { // BitBufferRead() can't read more than 16 bits at a time so break up the reads extraBits = chanBits - 16; for ( i = 0; i < numSamples; i++ ) { val = (int32_t) BitBufferRead( bits, 16 ); val = (val << 16) >> shift; mMixBufferU[i] = val | BitBufferRead( bits, (uint8_t) extraBits ); } } mixBits = mixRes = 0; bits1 = chanBits * numSamples; bytesShifted = 0; } // now read the shifted values into the shift buffer if ( bytesShifted != 0 ) { shift = bytesShifted * 8; //Assert( shift <= 16 ); for ( i = 0; i < numSamples; i++ ) mShiftBuffer[i] = (uint16_t) BitBufferRead( &shiftBits, (uint8_t) shift ); } // convert 32-bit integers into output buffer switch ( mConfig.bitDepth ) { case 16: out16 = &((int16_t *)sampleBuffer)[channelIndex]; for ( i = 0, j = 0; i < numSamples; i++, j += numChannels ) out16[j] = (int16_t) mMixBufferU[i]; break; case 20: out20 = (uint8_t *)sampleBuffer + (channelIndex * 3); copyPredictorTo20( mMixBufferU, out20, numChannels, numSamples ); break; case 24: out24 = (uint8_t *)sampleBuffer + (channelIndex * 3); if ( bytesShifted != 0 ) copyPredictorTo24Shift( mMixBufferU, mShiftBuffer, out24, numChannels, numSamples, bytesShifted ); else copyPredictorTo24( mMixBufferU, out24, numChannels, numSamples ); break; case 32: out32 = &((int32_t *)sampleBuffer)[channelIndex]; if ( bytesShifted != 0 ) copyPredictorTo32Shift( mMixBufferU, mShiftBuffer, out32, numChannels, numSamples, bytesShifted ); else copyPredictorTo32( mMixBufferU, out32, numChannels, numSamples); break; } channelIndex += 1; *outNumSamples = numSamples; break; } case ID_CPE: { // if decoding this pair would take us over the max channels limit, bail if ( (channelIndex + 2) > numChannels ) goto NoMoreChannels; // stereo channel pair elementInstanceTag = BitBufferReadSmall( bits, 4 ); mActiveElements |= (1u << elementInstanceTag); // read the 12 unused header bits unusedHeader = (uint16_t) BitBufferRead( bits, 12 ); RequireAction( unusedHeader == 0, status = kALAC_ParamError; goto Exit; ); // read the 1-bit "partial frame" flag, 2-bit "shift-off" flag & 1-bit "escape" flag headerByte = (uint8_t) BitBufferRead( bits, 4 ); partialFrame = headerByte >> 3; bytesShifted = (headerByte >> 1) & 0x3u; RequireAction( bytesShifted != 3, status = kALAC_ParamError; goto Exit; ); shift = bytesShifted * 8; escapeFlag = headerByte & 0x1; chanBits = mConfig.bitDepth - (bytesShifted * 8) + 1; // check for partial frame length to override requested numSamples if ( partialFrame != 0 ) { numSamples = BitBufferRead( bits, 16 ) << 16; numSamples |= BitBufferRead( bits, 16 ); } if ( escapeFlag == 0 ) { // compressed frame, read rest of parameters mixBits = (uint8_t) BitBufferRead( bits, 8 ); mixRes = (int8_t) BitBufferRead( bits, 8 ); headerByte = (uint8_t) BitBufferRead( bits, 8 ); modeU = headerByte >> 4; denShiftU = headerByte & 0xfu; headerByte = (uint8_t) BitBufferRead( bits, 8 ); pbFactorU = headerByte >> 5; numU = headerByte & 0x1fu; for ( i = 0; i < numU; i++ ) coefsU[i] = (int16_t) BitBufferRead( bits, 16 ); headerByte = (uint8_t) BitBufferRead( bits, 8 ); modeV = headerByte >> 4; denShiftV = headerByte & 0xfu; headerByte = (uint8_t) BitBufferRead( bits, 8 ); pbFactorV = headerByte >> 5; numV = headerByte & 0x1fu; for ( i = 0; i < numV; i++ ) coefsV[i] = (int16_t) BitBufferRead( bits, 16 ); // if shift active, skip the interleaved shifted values but remember where they start if ( bytesShifted != 0 ) { shiftBits = *bits; BitBufferAdvance( bits, (bytesShifted * 8) * 2 * numSamples ); } // decompress and run predictor for "left" channel set_ag_params( &agParams, mConfig.mb, (pb * pbFactorU) / 4, mConfig.kb, numSamples, numSamples, mConfig.maxRun ); status = dyn_decomp( &agParams, bits, mPredictor, numSamples, chanBits, &bits1 ); RequireNoErr( status, goto Exit; ); if ( modeU == 0 ) { unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU ); } else { // the special "numActive == 31" mode can be done in-place unpc_block( mPredictor, mPredictor, numSamples, nil, 31, chanBits, 0 ); unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU ); } // decompress and run predictor for "right" channel set_ag_params( &agParams, mConfig.mb, (pb * pbFactorV) / 4, mConfig.kb, numSamples, numSamples, mConfig.maxRun ); status = dyn_decomp( &agParams, bits, mPredictor, numSamples, chanBits, &bits2 ); RequireNoErr( status, goto Exit; ); if ( modeV == 0 ) { unpc_block( mPredictor, mMixBufferV, numSamples, &coefsV[0], numV, chanBits, denShiftV ); } else { // the special "numActive == 31" mode can be done in-place unpc_block( mPredictor, mPredictor, numSamples, nil, 31, chanBits, 0 ); unpc_block( mPredictor, mMixBufferV, numSamples, &coefsV[0], numV, chanBits, denShiftV ); } } else { //Assert( bytesShifted == 0 ); // uncompressed frame, copy data into the mix buffers to use common output code chanBits = mConfig.bitDepth; shift = 32 - chanBits; if ( chanBits <= 16 ) { for ( i = 0; i < numSamples; i++ ) { val = (int32_t) BitBufferRead( bits, (uint8_t) chanBits ); val = (val << shift) >> shift; mMixBufferU[i] = val; val = (int32_t) BitBufferRead( bits, (uint8_t) chanBits ); val = (val << shift) >> shift; mMixBufferV[i] = val; } } else { // BitBufferRead() can't read more than 16 bits at a time so break up the reads extraBits = chanBits - 16; for ( i = 0; i < numSamples; i++ ) { val = (int32_t) BitBufferRead( bits, 16 ); val = (val << 16) >> shift; mMixBufferU[i] = val | BitBufferRead( bits, (uint8_t)extraBits ); val = (int32_t) BitBufferRead( bits, 16 ); val = (val << 16) >> shift; mMixBufferV[i] = val | BitBufferRead( bits, (uint8_t)extraBits ); } } bits1 = chanBits * numSamples; bits2 = chanBits * numSamples; mixBits = mixRes = 0; bytesShifted = 0; } // now read the shifted values into the shift buffer if ( bytesShifted != 0 ) { shift = bytesShifted * 8; //Assert( shift <= 16 ); for ( i = 0; i < (numSamples * 2); i += 2 ) { mShiftBuffer[i + 0] = (uint16_t) BitBufferRead( &shiftBits, (uint8_t) shift ); mShiftBuffer[i + 1] = (uint16_t) BitBufferRead( &shiftBits, (uint8_t) shift ); } } // un-mix the data and convert to output format // - note that mixRes = 0 means just interleave so we use that path for uncompressed frames switch ( mConfig.bitDepth ) { case 16: out16 = &((int16_t *)sampleBuffer)[channelIndex]; unmix16( mMixBufferU, mMixBufferV, out16, numChannels, numSamples, mixBits, mixRes ); break; case 20: out20 = (uint8_t *)sampleBuffer + (channelIndex * 3); unmix20( mMixBufferU, mMixBufferV, out20, numChannels, numSamples, mixBits, mixRes ); break; case 24: out24 = (uint8_t *)sampleBuffer + (channelIndex * 3); unmix24( mMixBufferU, mMixBufferV, out24, numChannels, numSamples, mixBits, mixRes, mShiftBuffer, bytesShifted ); break; case 32: out32 = &((int32_t *)sampleBuffer)[channelIndex]; unmix32( mMixBufferU, mMixBufferV, out32, numChannels, numSamples, mixBits, mixRes, mShiftBuffer, bytesShifted ); break; } channelIndex += 2; *outNumSamples = numSamples; break; } case ID_CCE: case ID_PCE: { // unsupported element, bail //AssertNoErr( tag ); status = kALAC_ParamError; break; } case ID_DSE: { // data stream element -- parse but ignore status = this->DataStreamElement( bits ); break; } case ID_FIL: { // fill element -- parse but ignore status = this->FillElement( bits ); break; } case ID_END: { // frame end, all done so byte align the frame and check for overruns BitBufferByteAlign( bits, false ); //Assert( bits->cur == bits->end ); goto Exit; } } #if ! DEBUG // if we've decoded all of our channels, bail (but not in debug b/c we want to know if we're seeing bad bits) // - this also protects us if the config does not match the bitstream or crap data bits follow the audio bits if ( channelIndex >= numChannels ) break; #endif } NoMoreChannels: // if we get here and haven't decoded all of the requested channels, fill the remaining channels with zeros for ( ; channelIndex < numChannels; channelIndex++ ) { switch ( mConfig.bitDepth ) { case 16: { int16_t * fill16 = &((int16_t *)sampleBuffer)[channelIndex]; Zero16( fill16, numSamples, numChannels ); break; } case 24: { uint8_t * fill24 = (uint8_t *)sampleBuffer + (channelIndex * 3); Zero24( fill24, numSamples, numChannels ); break; } case 32: { int32_t * fill32 = &((int32_t *)sampleBuffer)[channelIndex]; Zero32( fill32, numSamples, numChannels ); break; } } } Exit: return status; } #if PRAGMA_MARK #pragma mark - #endif /* FillElement() - they're just filler so we don't need 'em */ int32_t ALACDecoder::FillElement( BitBuffer * bits ) { int16_t count; // 4-bit count or (4-bit + 8-bit count) if 4-bit count == 15 // - plus this weird -1 thing I still don't fully understand count = BitBufferReadSmall( bits, 4 ); if ( count == 15 ) count += (int16_t) BitBufferReadSmall( bits, 8 ) - 1; BitBufferAdvance( bits, count * 8 ); RequireAction( bits->cur <= bits->end, return kALAC_ParamError; ); return ALAC_noErr; } /* DataStreamElement() - we don't care about data stream elements so just skip them */ int32_t ALACDecoder::DataStreamElement( BitBuffer * bits ) { uint8_t element_instance_tag; int32_t data_byte_align_flag; uint16_t count; // the tag associates this data stream element with a given audio element element_instance_tag = BitBufferReadSmall( bits, 4 ); data_byte_align_flag = BitBufferReadOne( bits ); // 8-bit count or (8-bit + 8-bit count) if 8-bit count == 255 count = BitBufferReadSmall( bits, 8 ); if ( count == 255 ) count += BitBufferReadSmall( bits, 8 ); // the align flag means the bitstream should be byte-aligned before reading the following data bytes if ( data_byte_align_flag ) BitBufferByteAlign( bits, false ); // skip the data bytes BitBufferAdvance( bits, count * 8 ); RequireAction( bits->cur <= bits->end, return kALAC_ParamError; ); return ALAC_noErr; } /* ZeroN() - helper routines to clear out output channel buffers when decoding fewer channels than requested */ static void Zero16( int16_t * buffer, uint32_t numItems, uint32_t stride ) { if ( stride == 1 ) { memset( buffer, 0, numItems * sizeof(int16_t) ); } else { for ( uint32_t index = 0; index < (numItems * stride); index += stride ) buffer[index] = 0; } } static void Zero24( uint8_t * buffer, uint32_t numItems, uint32_t stride ) { if ( stride == 1 ) { memset( buffer, 0, numItems * 3 ); } else { for ( uint32_t index = 0; index < (numItems * stride * 3); index += (stride * 3) ) { buffer[index + 0] = 0; buffer[index + 1] = 0; buffer[index + 2] = 0; } } } static void Zero32( int32_t * buffer, uint32_t numItems, uint32_t stride ) { if ( stride == 1 ) { memset( buffer, 0, numItems * sizeof(int32_t) ); } else { for ( uint32_t index = 0; index < (numItems * stride); index += stride ) buffer[index] = 0; } } #pragma GCC diagnostic pop kew-3.2.0/include/alac/codec/ALACDecoder.h000066400000000000000000000032361500206121000200300ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ /* File: ALACDecoder.h */ #ifndef _ALACDECODER_H #define _ALACDECODER_H #if PRAGMA_ONCE #pragma once #endif #include #include "ALACAudioTypes.h" struct BitBuffer; class ALACDecoder { public: ALACDecoder(); ~ALACDecoder(); int32_t Init( void * inMagicCookie, uint32_t inMagicCookieSize ); int32_t Decode( struct BitBuffer * bits, uint8_t * sampleBuffer, uint32_t numSamples, uint32_t numChannels, uint32_t * outNumSamples ); public: // decoding parameters (public for use in the analyzer) ALACSpecificConfig mConfig; protected: int32_t FillElement( struct BitBuffer * bits ); int32_t DataStreamElement( struct BitBuffer * bits ); uint16_t mActiveElements; // decoding buffers int32_t * mMixBufferU; int32_t * mMixBufferV; int32_t * mPredictor; uint16_t * mShiftBuffer; // note: this points to mPredictor's memory but different // variable for clarity and type difference }; #endif /* _ALACDECODER_H */ kew-3.2.0/include/alac/codec/APPLE_LICENSE.txt000066400000000000000000000472141500206121000204410ustar00rootroot00000000000000APPLE PUBLIC SOURCE LICENSE Version 2.0 - August 6, 2003 Please read this License carefully before downloading this software. By downloading or using this software, you are agreeing to be bound by the terms of this License. If you do not or cannot agree to the terms of this License, please do not download or use the software. Apple Note: In January 2007, Apple changed its corporate name from "Apple Computer, Inc." to "Apple Inc." This change has been reflected below and copyright years updated, but no other changes have been made to the APSL 2.0. 1. General; Definitions. This License applies to any program or other work which Apple Inc. ("Apple") makes publicly available and which contains a notice placed by Apple identifying such program or work as "Original Code" and stating that it is subject to the terms of this Apple Public Source License version 2.0 ("License"). As used in this License: 1.1 "Applicable Patent Rights" mean: (a) in the case where Apple is the grantor of rights, (i) claims of patents that are now or hereafter acquired, owned by or assigned to Apple and (ii) that cover subject matter contained in the Original Code, but only to the extent necessary to use, reproduce and/or distribute the Original Code without infringement; and (b) in the case where You are the grantor of rights, (i) claims of patents that are now or hereafter acquired, owned by or assigned to You and (ii) that cover subject matter in Your Modifications, taken alone or in combination with Original Code. 1.2 "Contributor" means any person or entity that creates or contributes to the creation of Modifications. 1.3 "Covered Code" means the Original Code, Modifications, the combination of Original Code and any Modifications, and/or any respective portions thereof. 1.4 "Externally Deploy" means: (a) to sublicense, distribute or otherwise make Covered Code available, directly or indirectly, to anyone other than You; and/or (b) to use Covered Code, alone or as part of a Larger Work, in any way to provide a service, including but not limited to delivery of content, through electronic communication with a client other than You. 1.5 "Larger Work" means a work which combines Covered Code or portions thereof with code not governed by the terms of this License. 1.6 "Modifications" mean any addition to, deletion from, and/or change to, the substance and/or structure of the Original Code, any previous Modifications, the combination of Original Code and any previous Modifications, and/or any respective portions thereof. When code is released as a series of files, a Modification is: (a) any addition to or deletion from the contents of a file containing Covered Code; and/or (b) any new file or other representation of computer program statements that contains any part of Covered Code. 1.7 "Original Code" means (a) the Source Code of a program or other work as originally made available by Apple under this License, including the Source Code of any updates or upgrades to such programs or works made available by Apple under this License, and that has been expressly identified by Apple as such in the header file(s) of such work; and (b) the object code compiled from such Source Code and originally made available by Apple under this License 1.8 "Source Code" means the human readable form of a program or other work that is suitable for making modifications to it, including all modules it contains, plus any associated interface definition files, scripts used to control compilation and installation of an executable (object code). 1.9 "You" or "Your" means an individual or a legal entity exercising rights under this License. For legal entities, "You" or "Your" includes any entity which controls, is controlled by, or is under common control with, You, where "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of fifty percent (50%) or more of the outstanding shares or beneficial ownership of such entity. 2. Permitted Uses; Conditions & Restrictions. Subject to the terms and conditions of this License, Apple hereby grants You, effective on the date You accept this License and download the Original Code, a world-wide, royalty-free, non-exclusive license, to the extent of Apple's Applicable Patent Rights and copyrights covering the Original Code, to do the following: 2.1 Unmodified Code. You may use, reproduce, display, perform, internally distribute within Your organization, and Externally Deploy verbatim, unmodified copies of the Original Code, for commercial or non-commercial purposes, provided that in each instance: (a) You must retain and reproduce in all copies of Original Code the copyright and other proprietary notices and disclaimers of Apple as they appear in the Original Code, and keep intact all notices in the Original Code that refer to this License; and (b) You must include a copy of this License with every copy of Source Code of Covered Code and documentation You distribute or Externally Deploy, and You may not offer or impose any terms on such Source Code that alter or restrict this License or the recipients' rights hereunder, except as permitted under Section 6. 2.2 Modified Code. You may modify Covered Code and use, reproduce, display, perform, internally distribute within Your organization, and Externally Deploy Your Modifications and Covered Code, for commercial or non-commercial purposes, provided that in each instance You also meet all of these conditions: (a) You must satisfy all the conditions of Section 2.1 with respect to the Source Code of the Covered Code; (b) You must duplicate, to the extent it does not already exist, the notice in Exhibit A in each file of the Source Code of all Your Modifications, and cause the modified files to carry prominent notices stating that You changed the files and the date of any change; and (c) If You Externally Deploy Your Modifications, You must make Source Code of all Your Externally Deployed Modifications either available to those to whom You have Externally Deployed Your Modifications, or publicly available. Source Code of Your Externally Deployed Modifications must be released under the terms set forth in this License, including the license grants set forth in Section 3 below, for as long as you Externally Deploy the Covered Code or twelve (12) months from the date of initial External Deployment, whichever is longer. You should preferably distribute the Source Code of Your Externally Deployed Modifications electronically (e.g. download from a web site). 2.3 Distribution of Executable Versions. In addition, if You Externally Deploy Covered Code (Original Code and/or Modifications) in object code, executable form only, You must include a prominent notice, in the code itself as well as in related documentation, stating that Source Code of the Covered Code is available under the terms of this License with information on how and where to obtain such Source Code. 2.4 Third Party Rights. You expressly acknowledge and agree that although Apple and each Contributor grants the licenses to their respective portions of the Covered Code set forth herein, no assurances are provided by Apple or any Contributor that the Covered Code does not infringe the patent or other intellectual property rights of any other entity. Apple and each Contributor disclaim any liability to You for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, You hereby assume sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow You to distribute the Covered Code, it is Your responsibility to acquire that license before distributing the Covered Code. 3. Your Grants. In consideration of, and as a condition to, the licenses granted to You under this License, You hereby grant to any person or entity receiving or distributing Covered Code under this License a non-exclusive, royalty-free, perpetual, irrevocable license, under Your Applicable Patent Rights and other intellectual property rights (other than patent) owned or controlled by You, to use, reproduce, display, perform, modify, sublicense, distribute and Externally Deploy Your Modifications of the same scope and extent as Apple's licenses under Sections 2.1 and 2.2 above. 4. Larger Works. You may create a Larger Work by combining Covered Code with other code not governed by the terms of this License and distribute the Larger Work as a single product. In each such instance, You must make sure the requirements of this License are fulfilled for the Covered Code or any portion thereof. 5. Limitations on Patent License. Except as expressly stated in Section 2, no other patent rights, express or implied, are granted by Apple herein. Modifications and/or Larger Works may require additional patent licenses from Apple which Apple may grant in its sole discretion. 6. Additional Terms. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations and/or other rights consistent with the scope of the license granted herein ("Additional Terms") to one or more recipients of Covered Code. However, You may do so only on Your own behalf and as Your sole responsibility, and not on behalf of Apple or any Contributor. You must obtain the recipient's agreement that any such Additional Terms are offered by You alone, and You hereby agree to indemnify, defend and hold Apple and every Contributor harmless for any liability incurred by or claims asserted against Apple or such Contributor by reason of any such Additional Terms. 7. Versions of the License. Apple may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Once Original Code has been published under a particular version of this License, You may continue to use it under the terms of that version. You may also choose to use such Original Code under the terms of any subsequent version of this License published by Apple. No one other than Apple has the right to modify the terms applicable to Covered Code created under this License. 8. NO WARRANTY OR SUPPORT. The Covered Code may contain in whole or in part pre-release, untested, or not fully tested works. The Covered Code may contain errors that could cause failures or loss of data, and may be incomplete or contain inaccuracies. You expressly acknowledge and agree that use of the Covered Code, or any portion thereof, is at Your sole and entire risk. THE COVERED CODE IS PROVIDED "AS IS" AND WITHOUT WARRANTY, UPGRADES OR SUPPORT OF ANY KIND AND APPLE AND APPLE'S LICENSOR(S) (COLLECTIVELY REFERRED TO AS "APPLE" FOR THE PURPOSES OF SECTIONS 8 AND 9) AND ALL CONTRIBUTORS EXPRESSLY DISCLAIM ALL WARRANTIES AND/OR CONDITIONS, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES AND/OR CONDITIONS OF MERCHANTABILITY, OF SATISFACTORY QUALITY, OF FITNESS FOR A PARTICULAR PURPOSE, OF ACCURACY, OF QUIET ENJOYMENT, AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. APPLE AND EACH CONTRIBUTOR DOES NOT WARRANT AGAINST INTERFERENCE WITH YOUR ENJOYMENT OF THE COVERED CODE, THAT THE FUNCTIONS CONTAINED IN THE COVERED CODE WILL MEET YOUR REQUIREMENTS, THAT THE OPERATION OF THE COVERED CODE WILL BE UNINTERRUPTED OR ERROR-FREE, OR THAT DEFECTS IN THE COVERED CODE WILL BE CORRECTED. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY APPLE, AN APPLE AUTHORIZED REPRESENTATIVE OR ANY CONTRIBUTOR SHALL CREATE A WARRANTY. You acknowledge that the Covered Code is not intended for use in the operation of nuclear facilities, aircraft navigation, communication systems, or air traffic control machines in which case the failure of the Covered Code could lead to death, personal injury, or severe physical or environmental damage. 9. LIMITATION OF LIABILITY. TO THE EXTENT NOT PROHIBITED BY LAW, IN NO EVENT SHALL APPLE OR ANY CONTRIBUTOR BE LIABLE FOR ANY INCIDENTAL, SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATING TO THIS LICENSE OR YOUR USE OR INABILITY TO USE THE COVERED CODE, OR ANY PORTION THEREOF, WHETHER UNDER A THEORY OF CONTRACT, WARRANTY, TORT (INCLUDING NEGLIGENCE), PRODUCTS LIABILITY OR OTHERWISE, EVEN IF APPLE OR SUCH CONTRIBUTOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES AND NOTWITHSTANDING THE FAILURE OF ESSENTIAL PURPOSE OF ANY REMEDY. SOME JURISDICTIONS DO NOT ALLOW THE LIMITATION OF LIABILITY OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS LIMITATION MAY NOT APPLY TO YOU. In no event shall Apple's total liability to You for all damages (other than as may be required by applicable law) under this License exceed the amount of fifty dollars ($50.00). 10. Trademarks. This License does not grant any rights to use the trademarks or trade names "Apple", "Mac", "Mac OS", "QuickTime", "QuickTime Streaming Server" or any other trademarks, service marks, logos or trade names belonging to Apple (collectively "Apple Marks") or to any trademark, service mark, logo or trade name belonging to any Contributor. You agree not to use any Apple Marks in or as part of the name of products derived from the Original Code or to endorse or promote products derived from the Original Code other than as expressly permitted by and in strict compliance at all times with Apple's third party trademark usage guidelines which are posted at http://www.apple.com/legal/guidelinesfor3rdparties.html. 11. Ownership. Subject to the licenses granted under this License, each Contributor retains all rights, title and interest in and to any Modifications made by such Contributor. Apple retains all rights, title and interest in and to the Original Code and any Modifications made by or on behalf of Apple ("Apple Modifications"), and such Apple Modifications will not be automatically subject to this License. Apple may, at its sole discretion, choose to license such Apple Modifications under this License, or on different terms from those contained in this License or may choose not to license them at all. 12. Termination. 12.1 Termination. This License and the rights granted hereunder will terminate: (a) automatically without notice from Apple if You fail to comply with any term(s) of this License and fail to cure such breach within 30 days of becoming aware of such breach; (b) immediately in the event of the circumstances described in Section 13.5(b); or (c) automatically without notice from Apple if You, at any time during the term of this License, commence an action for patent infringement against Apple; provided that Apple did not first commence an action for patent infringement against You in that instance. 12.2 Effect of Termination. Upon termination, You agree to immediately stop any further use, reproduction, modification, sublicensing and distribution of the Covered Code. All sublicenses to the Covered Code which have been properly granted prior to termination shall survive any termination of this License. Provisions which, by their nature, should remain in effect beyond the termination of this License shall survive, including but not limited to Sections 3, 5, 8, 9, 10, 11, 12.2 and 13. No party will be liable to any other for compensation, indemnity or damages of any sort solely as a result of terminating this License in accordance with its terms, and termination of this License will be without prejudice to any other right or remedy of any party. 13. Miscellaneous. 13.1 Government End Users. The Covered Code is a "commercial item" as defined in FAR 2.101. Government software and technical data rights in the Covered Code include only those rights customarily provided to the public as defined in this License. This customary commercial license in technical data and software is provided in accordance with FAR 12.211 (Technical Data) and 12.212 (Computer Software) and, for Department of Defense purchases, DFAR 252.227-7015 (Technical Data -- Commercial Items) and 227.7202-3 (Rights in Commercial Computer Software or Computer Software Documentation). Accordingly, all U.S. Government End Users acquire Covered Code with only those rights set forth herein. 13.2 Relationship of Parties. This License will not be construed as creating an agency, partnership, joint venture or any other form of legal association between or among You, Apple or any Contributor, and You will not represent to the contrary, whether expressly, by implication, appearance or otherwise. 13.3 Independent Development. Nothing in this License will impair Apple's right to acquire, license, develop, have others develop for it, market and/or distribute technology or products that perform the same or similar functions as, or otherwise compete with, Modifications, Larger Works, technology or products that You may develop, produce, market or distribute. 13.4 Waiver; Construction. Failure by Apple or any Contributor to enforce any provision of this License will not be deemed a waiver of future enforcement of that or any other provision. Any law or regulation which provides that the language of a contract shall be construed against the drafter will not apply to this License. 13.5 Severability. (a) If for any reason a court of competent jurisdiction finds any provision of this License, or portion thereof, to be unenforceable, that provision of the License will be enforced to the maximum extent permissible so as to effect the economic benefits and intent of the parties, and the remainder of this License will continue in full force and effect. (b) Notwithstanding the foregoing, if applicable law prohibits or restricts You from fully and/or specifically complying with Sections 2 and/or 3 or prevents the enforceability of either of those Sections, this License will immediately terminate and You must immediately discontinue any use of the Covered Code and destroy all copies of it that are in your possession or control. 13.6 Dispute Resolution. Any litigation or other dispute resolution between You and Apple relating to this License shall take place in the Northern District of California, and You and Apple hereby consent to the personal jurisdiction of, and venue in, the state and federal courts within that District with respect to this License. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. 13.7 Entire Agreement; Governing Law. This License constitutes the entire agreement between the parties with respect to the subject matter hereof. This License shall be governed by the laws of the United States and the State of California, except that body of California law concerning conflicts of law. Where You are located in the province of Quebec, Canada, the following clause applies: The parties hereby confirm that they have requested that this License and all related documents be drafted in English. Les parties ont exigé que le présent contrat et tous les documents connexes soient rédigés en anglais. EXHIBIT A. "Portions Copyright (c) 1999-2007 Apple Inc. All Rights Reserved. This file contains Original Code and/or Modifications of Original Code as defined in and that are subject to the Apple Public Source License Version 2.0 (the 'License'). You may not use this file except in compliance with the License. Please obtain a copy of the License at http://www.opensource.apple.com/apsl/ and read it before using this file. The Original Code and all software distributed under the License are distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see the License for the specific language governing rights and limitations under the License." kew-3.2.0/include/alac/codec/EndianPortable.c000066400000000000000000000065161500206121000207300ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ // // EndianPortable.c // // Copyright 2011 Apple Inc. All rights reserved. // #include #include "EndianPortable.h" #define BSWAP16(x) (((x << 8) | ((x >> 8) & 0x00ff))) #define BSWAP32(x) (((x << 24) | ((x << 8) & 0x00ff0000) | ((x >> 8) & 0x0000ff00) | ((x >> 24) & 0x000000ff))) #define BSWAP64(x) ((((int64_t)x << 56) | (((int64_t)x << 40) & 0x00ff000000000000LL) | \ (((int64_t)x << 24) & 0x0000ff0000000000LL) | (((int64_t)x << 8) & 0x000000ff00000000LL) | \ (((int64_t)x >> 8) & 0x00000000ff000000LL) | (((int64_t)x >> 24) & 0x0000000000ff0000LL) | \ (((int64_t)x >> 40) & 0x000000000000ff00LL) | (((int64_t)x >> 56) & 0x00000000000000ffLL))) #if defined(__i386__) #define TARGET_RT_LITTLE_ENDIAN 1 #elif defined(__x86_64__) #define TARGET_RT_LITTLE_ENDIAN 1 #elif defined (TARGET_OS_WIN32) #define TARGET_RT_LITTLE_ENDIAN 1 #endif uint16_t Swap16NtoB(uint16_t inUInt16) { #if TARGET_RT_LITTLE_ENDIAN return BSWAP16(inUInt16); #else return inUInt16; #endif } uint16_t Swap16BtoN(uint16_t inUInt16) { #if TARGET_RT_LITTLE_ENDIAN return BSWAP16(inUInt16); #else return inUInt16; #endif } uint32_t Swap32NtoB(uint32_t inUInt32) { #if TARGET_RT_LITTLE_ENDIAN return BSWAP32(inUInt32); #else return inUInt32; #endif } uint32_t Swap32BtoN(uint32_t inUInt32) { #if TARGET_RT_LITTLE_ENDIAN return BSWAP32(inUInt32); #else return inUInt32; #endif } uint64_t Swap64BtoN(uint64_t inUInt64) { #if TARGET_RT_LITTLE_ENDIAN return BSWAP64(inUInt64); #else return inUInt64; #endif } uint64_t Swap64NtoB(uint64_t inUInt64) { #if TARGET_RT_LITTLE_ENDIAN return BSWAP64(inUInt64); #else return inUInt64; #endif } float SwapFloat32BtoN(float in) { #if TARGET_RT_LITTLE_ENDIAN union { float f; int32_t i; } x; x.f = in; x.i = BSWAP32(x.i); return x.f; #else return in; #endif } float SwapFloat32NtoB(float in) { #if TARGET_RT_LITTLE_ENDIAN union { float f; int32_t i; } x; x.f = in; x.i = BSWAP32(x.i); return x.f; #else return in; #endif } double SwapFloat64BtoN(double in) { #if TARGET_RT_LITTLE_ENDIAN union { double f; int64_t i; } x; x.f = in; x.i = BSWAP64(x.i); return x.f; #else return in; #endif } double SwapFloat64NtoB(double in) { #if TARGET_RT_LITTLE_ENDIAN union { double f; int64_t i; } x; x.f = in; x.i = BSWAP64(x.i); return x.f; #else return in; #endif } void Swap16(uint16_t * inUInt16) { *inUInt16 = BSWAP16(*inUInt16); } void Swap24(uint8_t * inUInt24) { uint8_t tempVal = inUInt24[0]; inUInt24[0] = inUInt24[2]; inUInt24[2] = tempVal; } void Swap32(uint32_t * inUInt32) { *inUInt32 = BSWAP32(*inUInt32); } kew-3.2.0/include/alac/codec/EndianPortable.h000066400000000000000000000026151500206121000207310ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ // // EndianPortable.h // // Copyright 2011 Apple Inc. All rights reserved. // #ifndef _EndianPortable_h #define _EndianPortable_h #include #ifdef __cplusplus extern "C" { #endif uint16_t Swap16NtoB(uint16_t inUInt16); uint16_t Swap16BtoN(uint16_t inUInt16); uint32_t Swap32NtoB(uint32_t inUInt32); uint32_t Swap32BtoN(uint32_t inUInt32); uint64_t Swap64BtoN(uint64_t inUInt64); uint64_t Swap64NtoB(uint64_t inUInt64); float SwapFloat32BtoN(float in); float SwapFloat32NtoB(float in); double SwapFloat64BtoN(double in); double SwapFloat64NtoB(double in); void Swap16(uint16_t * inUInt16); void Swap24(uint8_t * inUInt24); void Swap32(uint32_t * inUInt32); #ifdef __cplusplus } #endif #endif kew-3.2.0/include/alac/codec/ag_dec.c000066400000000000000000000204451500206121000172400ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ /* File changed from the original to supress some warnings */ /* File: ag_dec.c Contains: Adaptive Golomb decode routines. Copyright: (c) 2001-2011 Apple, Inc. */ #include "aglib.h" #include "ALACBitUtilities.h" #include "ALACAudioTypes.h" #include #include #include #include #if __GNUC__ && TARGET_OS_MAC #if __POWERPC__ #include #else #include #endif #endif #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wsign-compare" #define CODE_TO_LONG_MAXBITS 32 #define N_MAX_MEAN_CLAMP 0xffff #define N_MEAN_CLAMP_VAL 0xffff #define REPORT_VAL 40 #if __GNUC__ #define ALWAYS_INLINE __attribute__((always_inline)) #else #define ALWAYS_INLINE #endif /* And on the subject of the CodeWarrior x86 compiler and inlining, I reworked a lot of this to help the compiler out. In many cases this required manual inlining or a macro. Sorry if it is ugly but the performance gains are well worth it. - WSK 5/19/04 */ void set_standard_ag_params(AGParamRecPtr params, uint32_t fullwidth, uint32_t sectorwidth) { /* Use fullwidth = sectorwidth = numOfSamples, for analog 1-dimensional type-short data, but use fullwidth = full image width, sectorwidth = sector (patch) width for such as image (2-dim.) data. */ set_ag_params( params, MB0, PB0, KB0, fullwidth, sectorwidth, MAX_RUN_DEFAULT ); } void set_ag_params(AGParamRecPtr params, uint32_t m, uint32_t p, uint32_t k, uint32_t f, uint32_t s, uint32_t maxrun) { params->mb = params->mb0 = m; params->pb = p; params->kb = k; params->wb = (1u<kb)-1; params->qb = QB-params->pb; params->fw = f; params->sw = s; params->maxrun = maxrun; } #if PRAGMA_MARK #pragma mark - #endif // note: implementing this with some kind of "count leading zeros" assembly is a big performance win static inline int32_t lead( int32_t m ) { long j; unsigned long c = (1ul << 31); for(j=0; j < 32; j++) { if((c & m) != 0) break; c >>= 1; } return (j); } #define arithmin(a, b) ((a) < (b) ? (a) : (b)) static inline int32_t ALWAYS_INLINE lg3a( int32_t x) { int32_t result; x += 3; result = lead(x); return 31 - result; } static inline uint32_t ALWAYS_INLINE read32bit( uint8_t * buffer ) { // embedded CPUs typically can't read unaligned 32-bit words so just read the bytes uint32_t value; value = ((uint32_t)buffer[0] << 24) | ((uint32_t)buffer[1] << 16) | ((uint32_t)buffer[2] << 8) | (uint32_t)buffer[3]; return value; } #if PRAGMA_MARK #pragma mark - #endif #define get_next_fromlong(inlong, suff) ((inlong) >> (32 - (suff))) static inline uint32_t ALWAYS_INLINE getstreambits( uint8_t *in, int32_t bitoffset, int32_t numbits ) { uint32_t load1, load2; uint32_t byteoffset = bitoffset / 8; uint32_t result; //Assert( numbits <= 32 ); load1 = read32bit( in + byteoffset ); if ( (numbits + (bitoffset & 0x7)) > 32) { int32_t load2shift; result = load1 << (bitoffset & 0x7); load2 = (uint32_t) in[byteoffset+4]; load2shift = (8-(numbits + (bitoffset & 0x7)-32)); load2 >>= load2shift; result >>= (32-numbits); result |= load2; } else { result = load1 >> (32-numbits-(bitoffset & 7)); } // a shift of >= "the number of bits in the type of the value being shifted" results in undefined // behavior so don't try to shift by 32 if ( numbits != (sizeof(result) * 8) ) result &= ~(0xfffffffful << numbits); return result; } static inline int32_t dyn_get(unsigned char *in, uint32_t *bitPos, uint32_t m, uint32_t k) { uint32_t tempbits = *bitPos; uint32_t result; uint32_t pre = 0, v; uint32_t streamlong; streamlong = read32bit( in + (tempbits >> 3) ); streamlong <<= (tempbits & 7); /* find the number of bits in the prefix */ { uint32_t notI = ~streamlong; pre = lead( notI); } if(pre >= MAX_PREFIX_16) { pre = MAX_PREFIX_16; tempbits += pre; streamlong <<= pre; result = get_next_fromlong(streamlong,MAX_DATATYPE_BITS_16); tempbits += MAX_DATATYPE_BITS_16; } else { // all of the bits must fit within the long we have loaded //Assert(pre+1+k <= 32); tempbits += pre; tempbits += 1; streamlong <<= pre+1; v = get_next_fromlong(streamlong, k); tempbits += k; result = pre*m + v-1; if(v<2) { result -= (v-1); tempbits -= 1; } } *bitPos = tempbits; return result; } static inline int32_t dyn_get_32bit( uint8_t * in, uint32_t * bitPos, int32_t m, int32_t k, int32_t maxbits ) { uint32_t tempbits = *bitPos; uint32_t v; uint32_t streamlong; uint32_t result; streamlong = read32bit( in + (tempbits >> 3) ); streamlong <<= (tempbits & 7); /* find the number of bits in the prefix */ { uint32_t notI = ~streamlong; result = lead( notI); } if(result >= MAX_PREFIX_32) { result = getstreambits(in, tempbits+MAX_PREFIX_32, maxbits); tempbits += MAX_PREFIX_32 + maxbits; } else { /* all of the bits must fit within the long we have loaded*/ //Assert(k<=14); //Assert(result=2) { result += (v-1); tempbits += 1; } } } *bitPos = tempbits; return result; } int32_t dyn_decomp( AGParamRecPtr params, BitBuffer * bitstream, int32_t * pc, int32_t numSamples, int32_t maxSize, uint32_t * outNumBits ) { uint8_t *in; int32_t *outPtr = pc; uint32_t bitPos, startPos, maxPos; uint32_t j, m, k, n, c, mz; int32_t del, zmode; uint32_t mb; uint32_t pb_local = params->pb; uint32_t kb_local = params->kb; uint32_t wb_local = params->wb; int32_t status; RequireAction( (bitstream != nil) && (pc != nil) && (outNumBits != nil), return kALAC_ParamError; ); *outNumBits = 0; in = bitstream->cur; startPos = bitstream->bitIndex; maxPos = bitstream->byteSize * 8; bitPos = startPos; mb = params->mb0; zmode = 0; c = 0; status = ALAC_noErr; while (c < numSamples) { // bail if we've run off the end of the buffer RequireAction( bitPos < maxPos, status = kALAC_ParamError; goto Exit; ); m = (mb)>>QBSHIFT; k = lg3a(m); k = arithmin(k, kb_local); m = (1<> 1) * (multiplier); } *outPtr++ = del; c++; mb = pb_local*(n+zmode) + mb - ((pb_local*mb)>>QBSHIFT); // update mean tracking if (n > N_MAX_MEAN_CLAMP) mb = N_MEAN_CLAMP_VAL; zmode = 0; if (((mb << MMULSHIFT) < QB) && (c < numSamples)) { zmode = 1; k = lead(mb) - BITOFF+((mb+MOFF)>>MDENSHIFT); mz = ((1<= 65535) zmode = 0; mb = 0; } } Exit: *outNumBits = (bitPos - startPos); BitBufferAdvance( bitstream, *outNumBits ); RequireAction( bitstream->cur <= bitstream->end, status = kALAC_ParamError; ); return status; } #pragma GCC diagnostic pop kew-3.2.0/include/alac/codec/aglib.h000066400000000000000000000037461500206121000171260ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ /* File: aglib.h Copyright: (C) 2001-2011 Apple, Inc. */ #ifndef AGLIB_H #define AGLIB_H #include #ifdef __cplusplus extern "C" { #endif #define QBSHIFT 9 #define QB (1< #include #include #include "ALACDecoder.h" #include "ALACBitUtilities.h" #include "EndianPortable.h" #include "ALACAudioTypes.h" #include // Opaque struct typedef struct { ALACDecoder decoder; uint32_t channels; uint32_t bit_depth; uint32_t frame_length; } alac_decoder_t; extern "C" alac_decoder_t *alac_decoder_init_from_config(const ALACSpecificConfig *parsedConfig) { alac_decoder_t *ctx = new alac_decoder_t(); if (!ctx) return NULL; ctx->channels = parsedConfig->numChannels; ctx->bit_depth = parsedConfig->bitDepth; ctx->frame_length = parsedConfig->frameLength; ALACSpecificConfig config_to_decoder = *parsedConfig; config_to_decoder.frameLength = Swap32NtoB(parsedConfig->frameLength); config_to_decoder.maxRun = Swap16NtoB(parsedConfig->maxRun); config_to_decoder.maxFrameBytes = Swap32NtoB(parsedConfig->maxFrameBytes); config_to_decoder.avgBitRate = Swap32NtoB(parsedConfig->avgBitRate); config_to_decoder.sampleRate = Swap32NtoB(parsedConfig->sampleRate); if (ctx->decoder.Init(&config_to_decoder, sizeof(config_to_decoder)) != 0) { delete ctx; return NULL; } return ctx; } extern "C" int alac_decoder_decode(alac_decoder_t *ctx, uint8_t *inbuffer, uint32_t inbuffer_size, int32_t *outbuffer, uint32_t *samples_decoded) { if (!ctx || !samples_decoded) return -1; BitBuffer bits; BitBufferInit(&bits, inbuffer, inbuffer_size); BitBufferByteAlign(&bits, true); int32_t ret = ctx->decoder.Decode(&bits, (uint8_t *)outbuffer, ctx->frame_length, ctx->channels, samples_decoded); return ret; } extern "C" void alac_decoder_free(alac_decoder_t *ctx) { if (ctx) delete ctx; } kew-3.2.0/include/alac/codec/alac_wrapper.h000066400000000000000000000017701500206121000205030ustar00rootroot00000000000000#ifndef ALAC_WRAPPER_H #define ALAC_WRAPPER_H #include #include #ifdef __cplusplus extern "C" { #endif typedef struct { uint32_t frameLength; uint8_t compatibleVersion; uint8_t bitDepth; uint8_t pb; uint8_t mb; uint8_t kb; uint8_t numChannels; uint16_t maxRun; uint32_t maxFrameBytes; uint32_t avgBitRate; uint32_t sampleRate; } ALACSpecificConfig; typedef struct alac_decoder_t alac_decoder_t; alac_decoder_t *alac_decoder_init_from_config(const ALACSpecificConfig *parsedConfig); int alac_decoder_decode(alac_decoder_t *ctx, uint8_t *inbuffer, uint32_t inbuffer_size, int32_t *outbuffer, uint32_t *samples_decoded); void alac_decoder_free(alac_decoder_t *decoder); #ifdef __cplusplus } #endif #endif // ALAC_WRAPPER_H kew-3.2.0/include/alac/codec/dp_dec.c000066400000000000000000000176031500206121000172560ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ /* File: dp_dec.c Contains: Dynamic Predictor decode routines Copyright: (c) 2001-2011 Apple, Inc. */ #include "dplib.h" #include #if __GNUC__ #define ALWAYS_INLINE __attribute__((always_inline)) #else #define ALWAYS_INLINE #endif #if TARGET_CPU_PPC && (__MWERKS__ >= 0x3200) // align loops to a 16 byte boundary to make the G5 happy #pragma function_align 16 #define LOOP_ALIGN asm { align 16 } #else #define LOOP_ALIGN #endif static inline int32_t ALWAYS_INLINE sign_of_int( int32_t i ) { int32_t negishift; negishift = ((uint32_t)-i) >> 31; return negishift | (i >> 31); } void unpc_block( int32_t * pc1, int32_t * out, int32_t num, int16_t * coefs, int32_t numactive, uint32_t chanbits, uint32_t denshift ) { register int16_t a0, a1, a2, a3; register int32_t b0, b1, b2, b3; int32_t j, k, lim; int32_t sum1, sg, sgn, top, dd; int32_t * pout; int32_t del, del0; uint32_t chanshift = 32 - chanbits; int32_t denhalf = 1<<(denshift-1); out[0] = pc1[0]; if ( numactive == 0 ) { // just copy if numactive == 0 (but don't bother if in/out pointers the same) if ( (num > 1) && (pc1 != out) ) memcpy( &out[1], &pc1[1], (num - 1) * sizeof(int32_t) ); return; } if ( numactive == 31 ) { // short-circuit if numactive == 31 int32_t prev; /* this code is written such that the in/out buffers can be the same to conserve buffer space on embedded devices like the iPod (original code) for ( j = 1; j < num; j++ ) del = pc1[j] + out[j-1]; out[j] = (del << chanshift) >> chanshift; */ prev = out[0]; for ( j = 1; j < num; j++ ) { del = pc1[j] + prev; prev = (del << chanshift) >> chanshift; out[j] = prev; } return; } for ( j = 1; j <= numactive; j++ ) { del = pc1[j] + out[j-1]; out[j] = (del << chanshift) >> chanshift; } lim = numactive + 1; if ( numactive == 4 ) { // optimization for numactive == 4 register int16_t a0, a1, a2, a3; register int32_t b0, b1, b2, b3; a0 = coefs[0]; a1 = coefs[1]; a2 = coefs[2]; a3 = coefs[3]; for ( j = lim; j < num; j++ ) { LOOP_ALIGN top = out[j - lim]; pout = out + j - 1; b0 = top - pout[0]; b1 = top - pout[-1]; b2 = top - pout[-2]; b3 = top - pout[-3]; sum1 = (denhalf - a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3) >> denshift; del = pc1[j]; del0 = del; sg = sign_of_int(del); del += top + sum1; out[j] = (del << chanshift) >> chanshift; if ( sg > 0 ) { sgn = sign_of_int( b3 ); a3 -= sgn; del0 -= (4 - 3) * ((sgn * b3) >> denshift); if ( del0 <= 0 ) continue; sgn = sign_of_int( b2 ); a2 -= sgn; del0 -= (4 - 2) * ((sgn * b2) >> denshift); if ( del0 <= 0 ) continue; sgn = sign_of_int( b1 ); a1 -= sgn; del0 -= (4 - 1) * ((sgn * b1) >> denshift); if ( del0 <= 0 ) continue; a0 -= sign_of_int( b0 ); } else if ( sg < 0 ) { // note: to avoid unnecessary negations, we flip the value of "sgn" sgn = -sign_of_int( b3 ); a3 -= sgn; del0 -= (4 - 3) * ((sgn * b3) >> denshift); if ( del0 >= 0 ) continue; sgn = -sign_of_int( b2 ); a2 -= sgn; del0 -= (4 - 2) * ((sgn * b2) >> denshift); if ( del0 >= 0 ) continue; sgn = -sign_of_int( b1 ); a1 -= sgn; del0 -= (4 - 1) * ((sgn * b1) >> denshift); if ( del0 >= 0 ) continue; a0 += sign_of_int( b0 ); } } coefs[0] = a0; coefs[1] = a1; coefs[2] = a2; coefs[3] = a3; } else if ( numactive == 8 ) { register int16_t a4, a5, a6, a7; register int32_t b4, b5, b6, b7; // optimization for numactive == 8 a0 = coefs[0]; a1 = coefs[1]; a2 = coefs[2]; a3 = coefs[3]; a4 = coefs[4]; a5 = coefs[5]; a6 = coefs[6]; a7 = coefs[7]; for ( j = lim; j < num; j++ ) { LOOP_ALIGN top = out[j - lim]; pout = out + j - 1; b0 = top - (*pout--); b1 = top - (*pout--); b2 = top - (*pout--); b3 = top - (*pout--); b4 = top - (*pout--); b5 = top - (*pout--); b6 = top - (*pout--); b7 = top - (*pout); pout += 8; sum1 = (denhalf - a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3 - a4 * b4 - a5 * b5 - a6 * b6 - a7 * b7) >> denshift; del = pc1[j]; del0 = del; sg = sign_of_int(del); del += top + sum1; out[j] = (del << chanshift) >> chanshift; if ( sg > 0 ) { sgn = sign_of_int( b7 ); a7 -= sgn; del0 -= 1 * ((sgn * b7) >> denshift); if ( del0 <= 0 ) continue; sgn = sign_of_int( b6 ); a6 -= sgn; del0 -= 2 * ((sgn * b6) >> denshift); if ( del0 <= 0 ) continue; sgn = sign_of_int( b5 ); a5 -= sgn; del0 -= 3 * ((sgn * b5) >> denshift); if ( del0 <= 0 ) continue; sgn = sign_of_int( b4 ); a4 -= sgn; del0 -= 4 * ((sgn * b4) >> denshift); if ( del0 <= 0 ) continue; sgn = sign_of_int( b3 ); a3 -= sgn; del0 -= 5 * ((sgn * b3) >> denshift); if ( del0 <= 0 ) continue; sgn = sign_of_int( b2 ); a2 -= sgn; del0 -= 6 * ((sgn * b2) >> denshift); if ( del0 <= 0 ) continue; sgn = sign_of_int( b1 ); a1 -= sgn; del0 -= 7 * ((sgn * b1) >> denshift); if ( del0 <= 0 ) continue; a0 -= sign_of_int( b0 ); } else if ( sg < 0 ) { // note: to avoid unnecessary negations, we flip the value of "sgn" sgn = -sign_of_int( b7 ); a7 -= sgn; del0 -= 1 * ((sgn * b7) >> denshift); if ( del0 >= 0 ) continue; sgn = -sign_of_int( b6 ); a6 -= sgn; del0 -= 2 * ((sgn * b6) >> denshift); if ( del0 >= 0 ) continue; sgn = -sign_of_int( b5 ); a5 -= sgn; del0 -= 3 * ((sgn * b5) >> denshift); if ( del0 >= 0 ) continue; sgn = -sign_of_int( b4 ); a4 -= sgn; del0 -= 4 * ((sgn * b4) >> denshift); if ( del0 >= 0 ) continue; sgn = -sign_of_int( b3 ); a3 -= sgn; del0 -= 5 * ((sgn * b3) >> denshift); if ( del0 >= 0 ) continue; sgn = -sign_of_int( b2 ); a2 -= sgn; del0 -= 6 * ((sgn * b2) >> denshift); if ( del0 >= 0 ) continue; sgn = -sign_of_int( b1 ); a1 -= sgn; del0 -= 7 * ((sgn * b1) >> denshift); if ( del0 >= 0 ) continue; a0 += sign_of_int( b0 ); } } coefs[0] = a0; coefs[1] = a1; coefs[2] = a2; coefs[3] = a3; coefs[4] = a4; coefs[5] = a5; coefs[6] = a6; coefs[7] = a7; } else { // general case for ( j = lim; j < num; j++ ) { LOOP_ALIGN sum1 = 0; pout = out + j - 1; top = out[j-lim]; for ( k = 0; k < numactive; k++ ) sum1 += coefs[k] * (pout[-k] - top); del = pc1[j]; del0 = del; sg = sign_of_int( del ); del += top + ((sum1 + denhalf) >> denshift); out[j] = (del << chanshift) >> chanshift; if ( sg > 0 ) { for ( k = (numactive - 1); k >= 0; k-- ) { dd = top - pout[-k]; sgn = sign_of_int( dd ); coefs[k] -= sgn; del0 -= (numactive - k) * ((sgn * dd) >> denshift); if ( del0 <= 0 ) break; } } else if ( sg < 0 ) { for ( k = (numactive - 1); k >= 0; k-- ) { dd = top - pout[-k]; sgn = sign_of_int( dd ); coefs[k] += sgn; del0 -= (numactive - k) * ((-sgn * dd) >> denshift); if ( del0 >= 0 ) break; } } } } } kew-3.2.0/include/alac/codec/dplib.h000066400000000000000000000031701500206121000171310ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ /* File: dplib.h Contains: Dynamic Predictor routines Copyright: Copyright (C) 2001-2011 Apple, Inc. */ #ifndef __DPLIB_H__ #define __DPLIB_H__ #include #ifdef __cplusplus extern "C" { #endif // defines #define DENSHIFT_MAX 15 #define DENSHIFT_DEFAULT 9 #define AINIT 38 #define BINIT (-29) #define CINIT (-2) #define NUMCOEPAIRS 16 // prototypes void init_coefs( int16_t * coefs, uint32_t denshift, int32_t numPairs ); void copy_coefs( int16_t * srcCoefs, int16_t * dstCoefs, int32_t numPairs ); // NOTE: these routines read at least "numactive" samples so the i/o buffers must be at least that big void pc_block( int32_t * in, int32_t * pc, int32_t num, int16_t * coefs, int32_t numactive, uint32_t chanbits, uint32_t denshift ); void unpc_block( int32_t * pc, int32_t * out, int32_t num, int16_t * coefs, int32_t numactive, uint32_t chanbits, uint32_t denshift ); #ifdef __cplusplus } #endif #endif /* __DPLIB_H__ */ kew-3.2.0/include/alac/codec/matrix_dec.c000066400000000000000000000224251500206121000201550ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ /* File: matrix_dec.c Contains: ALAC mixing/matrixing decode routines. Copyright: (c) 2004-2011 Apple, Inc. */ #include "matrixlib.h" #include "ALACAudioTypes.h" // up to 24-bit "offset" macros for the individual bytes of a 20/24-bit word #if TARGET_RT_BIG_ENDIAN #define LBYTE 2 #define MBYTE 1 #define HBYTE 0 #else #define LBYTE 0 #define MBYTE 1 #define HBYTE 2 #endif /* There is no plain middle-side option; instead there are various mixing modes including middle-side, each lossless, as embodied in the mix() and unmix() functions. These functions exploit a generalized middle-side transformation: u := [(rL + (m-r)R)/m]; v := L - R; where [ ] denotes integer floor. The (lossless) inverse is L = u + v - [rV/m]; R = L - v; */ // 16-bit routines void unmix16( int32_t * u, int32_t * v, int16_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres ) { int16_t * op = out; int32_t j; if ( mixres != 0 ) { /* matrixed stereo */ for ( j = 0; j < numSamples; j++ ) { int32_t l, r; l = u[j] + v[j] - ((mixres * v[j]) >> mixbits); r = l - v[j]; op[0] = (int16_t) l; op[1] = (int16_t) r; op += stride; } } else { /* Conventional separated stereo. */ for ( j = 0; j < numSamples; j++ ) { op[0] = (int16_t) u[j]; op[1] = (int16_t) v[j]; op += stride; } } } // 20-bit routines // - the 20 bits of data are left-justified in 3 bytes of storage but right-aligned for input/output predictor buffers void unmix20( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres ) { uint8_t * op = out; int32_t j; if ( mixres != 0 ) { /* matrixed stereo */ for ( j = 0; j < numSamples; j++ ) { int32_t l, r; l = u[j] + v[j] - ((mixres * v[j]) >> mixbits); r = l - v[j]; l <<= 4; r <<= 4; op[HBYTE] = (uint8_t)((l >> 16) & 0xffu); op[MBYTE] = (uint8_t)((l >> 8) & 0xffu); op[LBYTE] = (uint8_t)((l >> 0) & 0xffu); op += 3; op[HBYTE] = (uint8_t)((r >> 16) & 0xffu); op[MBYTE] = (uint8_t)((r >> 8) & 0xffu); op[LBYTE] = (uint8_t)((r >> 0) & 0xffu); op += (stride - 1) * 3; } } else { /* Conventional separated stereo. */ for ( j = 0; j < numSamples; j++ ) { int32_t val; val = u[j] << 4; op[HBYTE] = (uint8_t)((val >> 16) & 0xffu); op[MBYTE] = (uint8_t)((val >> 8) & 0xffu); op[LBYTE] = (uint8_t)((val >> 0) & 0xffu); op += 3; val = v[j] << 4; op[HBYTE] = (uint8_t)((val >> 16) & 0xffu); op[MBYTE] = (uint8_t)((val >> 8) & 0xffu); op[LBYTE] = (uint8_t)((val >> 0) & 0xffu); op += (stride - 1) * 3; } } } // 24-bit routines // - the 24 bits of data are right-justified in the input/output predictor buffers void unmix24( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted ) { uint8_t * op = out; int32_t shift = bytesShifted * 8; int32_t l, r; int32_t j, k; if ( mixres != 0 ) { /* matrixed stereo */ if ( bytesShifted != 0 ) { for ( j = 0, k = 0; j < numSamples; j++, k += 2 ) { l = u[j] + v[j] - ((mixres * v[j]) >> mixbits); r = l - v[j]; l = (l << shift) | (uint32_t) shiftUV[k + 0]; r = (r << shift) | (uint32_t) shiftUV[k + 1]; op[HBYTE] = (uint8_t)((l >> 16) & 0xffu); op[MBYTE] = (uint8_t)((l >> 8) & 0xffu); op[LBYTE] = (uint8_t)((l >> 0) & 0xffu); op += 3; op[HBYTE] = (uint8_t)((r >> 16) & 0xffu); op[MBYTE] = (uint8_t)((r >> 8) & 0xffu); op[LBYTE] = (uint8_t)((r >> 0) & 0xffu); op += (stride - 1) * 3; } } else { for ( j = 0; j < numSamples; j++ ) { l = u[j] + v[j] - ((mixres * v[j]) >> mixbits); r = l - v[j]; op[HBYTE] = (uint8_t)((l >> 16) & 0xffu); op[MBYTE] = (uint8_t)((l >> 8) & 0xffu); op[LBYTE] = (uint8_t)((l >> 0) & 0xffu); op += 3; op[HBYTE] = (uint8_t)((r >> 16) & 0xffu); op[MBYTE] = (uint8_t)((r >> 8) & 0xffu); op[LBYTE] = (uint8_t)((r >> 0) & 0xffu); op += (stride - 1) * 3; } } } else { /* Conventional separated stereo. */ if ( bytesShifted != 0 ) { for ( j = 0, k = 0; j < numSamples; j++, k += 2 ) { l = u[j]; r = v[j]; l = (l << shift) | (uint32_t) shiftUV[k + 0]; r = (r << shift) | (uint32_t) shiftUV[k + 1]; op[HBYTE] = (uint8_t)((l >> 16) & 0xffu); op[MBYTE] = (uint8_t)((l >> 8) & 0xffu); op[LBYTE] = (uint8_t)((l >> 0) & 0xffu); op += 3; op[HBYTE] = (uint8_t)((r >> 16) & 0xffu); op[MBYTE] = (uint8_t)((r >> 8) & 0xffu); op[LBYTE] = (uint8_t)((r >> 0) & 0xffu); op += (stride - 1) * 3; } } else { for ( j = 0; j < numSamples; j++ ) { int32_t val; val = u[j]; op[HBYTE] = (uint8_t)((val >> 16) & 0xffu); op[MBYTE] = (uint8_t)((val >> 8) & 0xffu); op[LBYTE] = (uint8_t)((val >> 0) & 0xffu); op += 3; val = v[j]; op[HBYTE] = (uint8_t)((val >> 16) & 0xffu); op[MBYTE] = (uint8_t)((val >> 8) & 0xffu); op[LBYTE] = (uint8_t)((val >> 0) & 0xffu); op += (stride - 1) * 3; } } } } // 32-bit routines // - note that these really expect the internal data width to be < 32 but the arrays are 32-bit // - otherwise, the calculations might overflow into the 33rd bit and be lost // - therefore, these routines deal with the specified "unused lower" bytes in the "shift" buffers void unmix32( int32_t * u, int32_t * v, int32_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted ) { int32_t * op = out; int32_t shift = bytesShifted * 8; int32_t l, r; int32_t j, k; if ( mixres != 0 ) { //Assert( bytesShifted != 0 ); /* matrixed stereo with shift */ for ( j = 0, k = 0; j < numSamples; j++, k += 2 ) { int32_t lt, rt; lt = u[j]; rt = v[j]; l = lt + rt - ((mixres * rt) >> mixbits); r = l - rt; op[0] = (l << shift) | (uint32_t) shiftUV[k + 0]; op[1] = (r << shift) | (uint32_t) shiftUV[k + 1]; op += stride; } } else { if ( bytesShifted == 0 ) { /* interleaving w/o shift */ for ( j = 0; j < numSamples; j++ ) { op[0] = u[j]; op[1] = v[j]; op += stride; } } else { /* interleaving with shift */ for ( j = 0, k = 0; j < numSamples; j++, k += 2 ) { op[0] = (u[j] << shift) | (uint32_t) shiftUV[k + 0]; op[1] = (v[j] << shift) | (uint32_t) shiftUV[k + 1]; op += stride; } } } } // 20/24-bit <-> 32-bit helper routines (not really matrixing but convenient to put here) void copyPredictorTo24( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples ) { uint8_t * op = out; int32_t j; for ( j = 0; j < numSamples; j++ ) { int32_t val = in[j]; op[HBYTE] = (uint8_t)((val >> 16) & 0xffu); op[MBYTE] = (uint8_t)((val >> 8) & 0xffu); op[LBYTE] = (uint8_t)((val >> 0) & 0xffu); op += (stride * 3); } } void copyPredictorTo24Shift( int32_t * in, uint16_t * shift, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted ) { uint8_t * op = out; int32_t shiftVal = bytesShifted * 8; int32_t j; //Assert( bytesShifted != 0 ); for ( j = 0; j < numSamples; j++ ) { int32_t val = in[j]; val = (val << shiftVal) | (uint32_t) shift[j]; op[HBYTE] = (uint8_t)((val >> 16) & 0xffu); op[MBYTE] = (uint8_t)((val >> 8) & 0xffu); op[LBYTE] = (uint8_t)((val >> 0) & 0xffu); op += (stride * 3); } } void copyPredictorTo20( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples ) { uint8_t * op = out; int32_t j; // 32-bit predictor values are right-aligned but 20-bit output values should be left-aligned // in the 24-bit output buffer for ( j = 0; j < numSamples; j++ ) { int32_t val = in[j]; op[HBYTE] = (uint8_t)((val >> 12) & 0xffu); op[MBYTE] = (uint8_t)((val >> 4) & 0xffu); op[LBYTE] = (uint8_t)((val << 4) & 0xffu); op += (stride * 3); } } void copyPredictorTo32( int32_t * in, int32_t * out, uint32_t stride, int32_t numSamples ) { int32_t i, j; // this is only a subroutine to abstract the "iPod can only output 16-bit data" problem for ( i = 0, j = 0; i < numSamples; i++, j += stride ) out[j] = in[i]; } void copyPredictorTo32Shift( int32_t * in, uint16_t * shift, int32_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted ) { int32_t * op = out; uint32_t shiftVal = bytesShifted * 8; int32_t j; //Assert( bytesShifted != 0 ); // this is only a subroutine to abstract the "iPod can only output 16-bit data" problem for ( j = 0; j < numSamples; j++ ) { op[0] = (in[j] << shiftVal) | (uint32_t) shift[j]; op += stride; } } kew-3.2.0/include/alac/codec/matrixlib.h000066400000000000000000000070171500206121000200360ustar00rootroot00000000000000/* * Copyright (c) 2011 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ /* File: matrixlib.h Contains: ALAC mixing/matrixing routines to/from 32-bit predictor buffers. Copyright: Copyright (C) 2004 to 2011 Apple, Inc. */ #ifndef __MATRIXLIB_H #define __MATRIXLIB_H #pragma once #include #ifdef __cplusplus extern "C" { #endif // 16-bit routines void mix16( int16_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres ); void unmix16( int32_t * u, int32_t * v, int16_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres ); // 20-bit routines void mix20( uint8_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres ); void unmix20( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres ); // 24-bit routines // - 24-bit data sometimes compresses better by shifting off the bottom byte so these routines deal with // the specified "unused lower bytes" in the combined "shift" buffer void mix24( uint8_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted ); void unmix24( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted ); // 32-bit routines // - note that these really expect the internal data width to be < 32-bit but the arrays are 32-bit // - otherwise, the calculations might overflow into the 33rd bit and be lost // - therefore, these routines deal with the specified "unused lower" bytes in the combined "shift" buffer void mix32( int32_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted ); void unmix32( int32_t * u, int32_t * v, int32_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted ); // 20/24/32-bit <-> 32-bit helper routines (not really matrixing but convenient to put here) void copy20ToPredictor( uint8_t * in, uint32_t stride, int32_t * out, int32_t numSamples ); void copy24ToPredictor( uint8_t * in, uint32_t stride, int32_t * out, int32_t numSamples ); void copyPredictorTo24( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples ); void copyPredictorTo24Shift( int32_t * in, uint16_t * shift, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted ); void copyPredictorTo20( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples ); void copyPredictorTo32( int32_t * in, int32_t * out, uint32_t stride, int32_t numSamples ); void copyPredictorTo32Shift( int32_t * in, uint16_t * shift, int32_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted ); #ifdef __cplusplus } #endif #endif /* __MATRIXLIB_H */ kew-3.2.0/include/minimp4/000077500000000000000000000000001500206121000152655ustar00rootroot00000000000000kew-3.2.0/include/minimp4/minimp4.h000066400000000000000000003635511500206121000170300ustar00rootroot00000000000000#ifndef MINIMP4_H #define MINIMP4_H /* https://github.com/aspt/mp4 https://github.com/lieff/minimp4 To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. See . */ #include #include #include #include #include #include #ifdef __cplusplus extern "C" { #endif #define MINIMP4_MIN(x, y) ((x) < (y) ? (x) : (y)) /************************************************************************/ /* Build configuration */ /************************************************************************/ #define FIX_BAD_ANDROID_META_BOX 1 #define MAX_CHUNKS_DEPTH 64 // Max chunks nesting level #define MINIMP4_MAX_SPS 32 #define MINIMP4_MAX_PPS 256 #define MINIMP4_TRANSCODE_SPS_ID 1 // Support indexing of MP4 files over 4 GB. // If disabled, files with 64-bit offset fields is still supported, // but error signaled if such field contains too big offset // This switch affect return type of MP4D_frame_offset() function #define MINIMP4_ALLOW_64BIT 1 #define MP4D_TRACE_SUPPORTED 0 // Debug trace #define MP4D_TRACE_TIMESTAMPS 1 // Support parsing of supplementary information, not necessary for decoding: // duration, language, bitrate, metadata tags, etc #define MP4D_INFO_SUPPORTED 1 // Enable code, which prints to stdout supplementary MP4 information: #define MP4D_PRINT_INFO_SUPPORTED 0 #define MP4D_AVC_SUPPORTED 1 #define MP4D_HEVC_SUPPORTED 1 #define MP4D_TIMESTAMPS_SUPPORTED 1 // Enable TrackFragmentBaseMediaDecodeTimeBox support #define MP4D_TFDT_SUPPORT 0 /************************************************************************/ /* Some values of MP4(E/D)_track_t->object_type_indication */ /************************************************************************/ // MPEG-4 AAC (all profiles) #define MP4_OBJECT_TYPE_AUDIO_ISO_IEC_14496_3 0x40 // MPEG-2 AAC, Main profile #define MP4_OBJECT_TYPE_AUDIO_ISO_IEC_13818_7_MAIN_PROFILE 0x66 // MPEG-2 AAC, LC profile #define MP4_OBJECT_TYPE_AUDIO_ISO_IEC_13818_7_LC_PROFILE 0x67 // MPEG-2 AAC, SSR profile #define MP4_OBJECT_TYPE_AUDIO_ISO_IEC_13818_7_SSR_PROFILE 0x68 // H.264 (AVC) video #define MP4_OBJECT_TYPE_AVC 0x21 // H.265 (HEVC) video #define MP4_OBJECT_TYPE_HEVC 0x23 // http://www.mp4ra.org/object.html 0xC0-E0 && 0xE2 - 0xFE are specified as "user private" #define MP4_OBJECT_TYPE_USER_PRIVATE 0xC0 /************************************************************************/ /* API error codes */ /************************************************************************/ #define MP4E_STATUS_OK 0 #define MP4E_STATUS_BAD_ARGUMENTS -1 #define MP4E_STATUS_NO_MEMORY -2 #define MP4E_STATUS_FILE_WRITE_ERROR -3 #define MP4E_STATUS_ONLY_ONE_DSI_ALLOWED -4 /************************************************************************/ /* Sample kind for MP4E_put_sample() */ /************************************************************************/ #define MP4E_SAMPLE_DEFAULT 0 // (beginning of) audio or video frame #define MP4E_SAMPLE_RANDOM_ACCESS 1 // mark sample as random access point (key frame) #define MP4E_SAMPLE_CONTINUATION 2 // Not a sample, but continuation of previous sample (new slice) /************************************************************************/ /* Portable 64-bit type definition */ /************************************************************************/ #if MINIMP4_ALLOW_64BIT typedef uint64_t boxsize_t; #else typedef unsigned int boxsize_t; #endif typedef boxsize_t MP4D_file_offset_t; /************************************************************************/ /* Some values of MP4D_track_t->handler_type */ /************************************************************************/ // Video track : 'vide' #define MP4D_HANDLER_TYPE_VIDE 0x76696465 // Audio track : 'soun' #define MP4D_HANDLER_TYPE_SOUN 0x736F756E // General MPEG-4 systems streams (without specific handler). // Used for private stream, as suggested in http://www.mp4ra.org/handler.html #define MP4E_HANDLER_TYPE_GESM 0x6765736D #define HEVC_NAL_VPS 32 #define HEVC_NAL_SPS 33 #define HEVC_NAL_PPS 34 #define HEVC_NAL_BLA_W_LP 16 #define HEVC_NAL_CRA_NUT 21 /************************************************************************/ /* Data structures */ /************************************************************************/ typedef struct MP4E_mux_tag MP4E_mux_t; typedef enum { e_audio, e_video, e_private } track_media_kind_t; typedef struct { // MP4 object type code, which defined codec class for the track. // See MP4E_OBJECT_TYPE_* values for some codecs unsigned object_type_indication; // Track language: 3-char ISO 639-2T code: "und", "eng", "rus", "jpn" etc... unsigned char language[4]; track_media_kind_t track_media_kind; // 90000 for video, sample rate for audio unsigned time_scale; unsigned default_duration; union { struct { // number of channels in the audio track. unsigned channelcount; } a; struct { int width; int height; } v; } u; } MP4E_track_t; typedef struct MP4D_sample_to_chunk_t_tag MP4D_sample_to_chunk_t; typedef struct { /************************************************************************/ /* mandatory public data */ /************************************************************************/ // How many 'samples' in the track // The 'sample' is MP4 term, denoting audio or video frame unsigned sample_count; // Decoder-specific info (DSI) data unsigned char *dsi; // DSI data size unsigned dsi_bytes; // MP4 object type code // case 0x00: return "Forbidden"; // case 0x01: return "Systems ISO/IEC 14496-1"; // case 0x02: return "Systems ISO/IEC 14496-1"; // case 0x20: return "Visual ISO/IEC 14496-2"; // case 0x40: return "Audio ISO/IEC 14496-3"; // case 0x60: return "Visual ISO/IEC 13818-2 Simple Profile"; // case 0x61: return "Visual ISO/IEC 13818-2 Main Profile"; // case 0x62: return "Visual ISO/IEC 13818-2 SNR Profile"; // case 0x63: return "Visual ISO/IEC 13818-2 Spatial Profile"; // case 0x64: return "Visual ISO/IEC 13818-2 High Profile"; // case 0x65: return "Visual ISO/IEC 13818-2 422 Profile"; // case 0x66: return "Audio ISO/IEC 13818-7 Main Profile"; // case 0x67: return "Audio ISO/IEC 13818-7 LC Profile"; // case 0x68: return "Audio ISO/IEC 13818-7 SSR Profile"; // case 0x69: return "Audio ISO/IEC 13818-3"; // case 0x6A: return "Visual ISO/IEC 11172-2"; // case 0x6B: return "Audio ISO/IEC 11172-3"; // case 0x6C: return "Visual ISO/IEC 10918-1"; unsigned object_type_indication; #if MP4D_INFO_SUPPORTED /************************************************************************/ /* informational public data */ /************************************************************************/ // handler_type when present in a media box, is an integer containing one of // the following values, or a value from a derived specification: // 'vide' Video track // 'soun' Audio track // 'hint' Hint track unsigned handler_type; // Track duration: 64-bit value split into 2 variables unsigned duration_hi; unsigned duration_lo; // duration scale: duration = timescale*seconds unsigned timescale; // Average bitrate, bits per second unsigned avg_bitrate_bps; // Track language: 3-char ISO 639-2T code: "und", "eng", "rus", "jpn" etc... unsigned char language[4]; // MP4 stream type // case 0x00: return "Forbidden"; // case 0x01: return "ObjectDescriptorStream"; // case 0x02: return "ClockReferenceStream"; // case 0x03: return "SceneDescriptionStream"; // case 0x04: return "VisualStream"; // case 0x05: return "AudioStream"; // case 0x06: return "MPEG7Stream"; // case 0x07: return "IPMPStream"; // case 0x08: return "ObjectContentInfoStream"; // case 0x09: return "MPEGJStream"; unsigned stream_type; union { // for handler_type == 'soun' tracks struct { unsigned channelcount; unsigned samplerate_hz; } audio; // for handler_type == 'vide' tracks struct { unsigned width; unsigned height; } video; } SampleDescription; #endif /************************************************************************/ /* private data: MP4 indexes */ /************************************************************************/ unsigned *entry_size; unsigned sample_to_chunk_count; struct MP4D_sample_to_chunk_t_tag *sample_to_chunk; unsigned chunk_count; MP4D_file_offset_t *chunk_offset; #if MP4D_TIMESTAMPS_SUPPORTED unsigned *timestamp; unsigned *duration; #endif } MP4D_track_t; typedef struct MP4D_demux_tag { /************************************************************************/ /* mandatory public data */ /************************************************************************/ int64_t read_pos; int64_t read_size; MP4D_track_t *track; int (*read_callback)(int64_t offset, void *buffer, size_t size, void *token); void *token; unsigned track_count; // number of tracks in the movie #if MP4D_INFO_SUPPORTED /************************************************************************/ /* informational public data */ /************************************************************************/ // Movie duration: 64-bit value split into 2 variables unsigned duration_hi; unsigned duration_lo; // duration scale: duration = timescale*seconds unsigned timescale; // Metadata tag (optional) // Tags provided 'as-is', without any re-encoding struct { unsigned char *title; unsigned char *artist; unsigned char *album; unsigned char *year; unsigned char *comment; unsigned char *genre; } tag; #endif } MP4D_demux_t; struct MP4D_sample_to_chunk_t_tag { unsigned first_chunk; unsigned samples_per_chunk; }; typedef struct { void *sps_cache[MINIMP4_MAX_SPS]; void *pps_cache[MINIMP4_MAX_PPS]; int sps_bytes[MINIMP4_MAX_SPS]; int pps_bytes[MINIMP4_MAX_PPS]; int map_sps[MINIMP4_MAX_SPS]; int map_pps[MINIMP4_MAX_PPS]; } h264_sps_id_patcher_t; typedef struct mp4_h26x_writer_tag { #if MINIMP4_TRANSCODE_SPS_ID h264_sps_id_patcher_t sps_patcher; #endif MP4E_mux_t *mux; int mux_track_id, is_hevc, need_vps, need_sps, need_pps, need_idr; } mp4_h26x_writer_t; int mp4_h26x_write_init(mp4_h26x_writer_t *h, MP4E_mux_t *mux, int width, int height, int is_hevc); void mp4_h26x_write_close(mp4_h26x_writer_t *h); int mp4_h26x_write_nal(mp4_h26x_writer_t *h, const unsigned char *nal, int length, unsigned timeStamp90kHz_next); /************************************************************************/ /* API */ /************************************************************************/ /** * Parse given input stream as MP4 file. Allocate and store data indexes. * return 1 on success, 0 on failure * The MP4 indexes may be stored at the end of stream, so this * function may parse all stream. * It is guaranteed that function will read/seek sequentially, * and will never jump back. */ int MP4D_open(MP4D_demux_t *mp4, int (*read_callback)(int64_t offset, void *buffer, size_t size, void *token), void *token, int64_t file_size); /** * Return position and size for given sample from given track. The 'sample' is a * MP4 term for 'frame' * * frame_bytes [OUT] - return coded frame size in bytes * timestamp [OUT] - return frame timestamp (in mp4->timescale units) * duration [OUT] - return frame duration (in mp4->timescale units) * * function return offset for the frame */ MP4D_file_offset_t MP4D_frame_offset(const MP4D_demux_t *mp4, unsigned int ntrack, unsigned int nsample, unsigned int *frame_bytes, unsigned *timestamp, unsigned *duration); /** * De-allocated memory */ void MP4D_close(MP4D_demux_t *mp4); /** * Helper functions to parse mp4.track[ntrack].dsi for H.264 SPS/PPS * Return pointer to internal mp4 memory, it must not be free()-ed * * Example: process all SPS in MP4 file: * while (sps = MP4D_read_sps(mp4, num_of_avc_track, sps_count, &sps_bytes)) * { * process(sps, sps_bytes); * sps_count++; * } */ const void *MP4D_read_sps(const MP4D_demux_t *mp4, unsigned int ntrack, int nsps, int *sps_bytes); const void *MP4D_read_pps(const MP4D_demux_t *mp4, unsigned int ntrack, int npps, int *pps_bytes); #if MP4D_PRINT_INFO_SUPPORTED /** * Print MP4 information to stdout. * Uses printf() as well as floating-point functions * Given as implementation example and for test purposes */ void MP4D_printf_info(const MP4D_demux_t *mp4); #endif /** * Allocates and initialize mp4 multiplexor * Given file handler is transparent to the MP4 library, and used only as * argument for given fwrite_callback() function. By appropriate definition * of callback function application may use any other file output API (for * example C++ streams, or Win32 file functions) * * return multiplexor handle on success; NULL on failure */ MP4E_mux_t *MP4E_open(int sequential_mode_flag, int enable_fragmentation, void *token, int (*write_callback)(int64_t offset, const void *buffer, size_t size, void *token)); /** * Add new track * The track_data parameter does not referred by the multiplexer after function * return, and may be allocated in short-time memory. The dsi member of * track_data parameter is mandatory. * * return ID of added track, or error code MP4E_STATUS_* */ int MP4E_add_track(MP4E_mux_t *mux, const MP4E_track_t *track_data); /** * Add new sample to specified track * The tracks numbered starting with 0, according to order of MP4E_add_track() calls * 'kind' is one of MP4E_SAMPLE_... defines * * return error code MP4E_STATUS_* * * Example: * MP4E_put_sample(mux, 0, data, data_bytes, duration, MP4E_SAMPLE_DEFAULT); */ int MP4E_put_sample(MP4E_mux_t *mux, int track_num, const void *data, int data_bytes, int duration, int kind); /** * Finalize MP4 file, de-allocated memory, and closes MP4 multiplexer. * The close operation takes a time and disk space, since it writes MP4 file * indexes. Please note that this function does not closes file handle, * which was passed to open function. * * return error code MP4E_STATUS_* */ int MP4E_close(MP4E_mux_t *mux); /** * Set Decoder Specific Info (DSI) * Can be used for audio and private tracks. * MUST be used for AAC track. * Only one DSI can be set. It is an error to set DSI again * * return error code MP4E_STATUS_* */ int MP4E_set_dsi(MP4E_mux_t *mux, int track_id, const void *dsi, int bytes); /** * Set VPS data. MUST be used for HEVC (H.265) track. * * return error code MP4E_STATUS_* */ int MP4E_set_vps(MP4E_mux_t *mux, int track_id, const void *vps, int bytes); /** * Set SPS data. MUST be used for AVC (H.264) track. Up to 32 different SPS can be used in one track. * * return error code MP4E_STATUS_* */ int MP4E_set_sps(MP4E_mux_t *mux, int track_id, const void *sps, int bytes); /** * Set PPS data. MUST be used for AVC (H.264) track. Up to 256 different PPS can be used in one track. * * return error code MP4E_STATUS_* */ int MP4E_set_pps(MP4E_mux_t *mux, int track_id, const void *pps, int bytes); /** * Set or replace ASCII test comment for the file. Set comment to NULL to remove comment. * * return error code MP4E_STATUS_* */ int MP4E_set_text_comment(MP4E_mux_t *mux, const char *comment); #ifdef __cplusplus } #endif #endif //MINIMP4_H #if defined(MINIMP4_IMPLEMENTATION) && !defined(MINIMP4_IMPLEMENTATION_GUARD) #define MINIMP4_IMPLEMENTATION_GUARD #define FOUR_CHAR_INT(a, b, c, d) (((uint32_t)(a) << 24) | ((b) << 16) | ((c) << 8) | (d)) enum { BOX_co64 = FOUR_CHAR_INT( 'c', 'o', '6', '4' ),//ChunkLargeOffsetAtomType BOX_stco = FOUR_CHAR_INT( 's', 't', 'c', 'o' ),//ChunkOffsetAtomType BOX_crhd = FOUR_CHAR_INT( 'c', 'r', 'h', 'd' ),//ClockReferenceMediaHeaderAtomType BOX_ctts = FOUR_CHAR_INT( 'c', 't', 't', 's' ),//CompositionOffsetAtomType BOX_cprt = FOUR_CHAR_INT( 'c', 'p', 'r', 't' ),//CopyrightAtomType BOX_url_ = FOUR_CHAR_INT( 'u', 'r', 'l', ' ' ),//DataEntryURLAtomType BOX_urn_ = FOUR_CHAR_INT( 'u', 'r', 'n', ' ' ),//DataEntryURNAtomType BOX_dinf = FOUR_CHAR_INT( 'd', 'i', 'n', 'f' ),//DataInformationAtomType BOX_dref = FOUR_CHAR_INT( 'd', 'r', 'e', 'f' ),//DataReferenceAtomType BOX_stdp = FOUR_CHAR_INT( 's', 't', 'd', 'p' ),//DegradationPriorityAtomType BOX_edts = FOUR_CHAR_INT( 'e', 'd', 't', 's' ),//EditAtomType BOX_elst = FOUR_CHAR_INT( 'e', 'l', 's', 't' ),//EditListAtomType BOX_uuid = FOUR_CHAR_INT( 'u', 'u', 'i', 'd' ),//ExtendedAtomType BOX_free = FOUR_CHAR_INT( 'f', 'r', 'e', 'e' ),//FreeSpaceAtomType BOX_hdlr = FOUR_CHAR_INT( 'h', 'd', 'l', 'r' ),//HandlerAtomType BOX_hmhd = FOUR_CHAR_INT( 'h', 'm', 'h', 'd' ),//HintMediaHeaderAtomType BOX_hint = FOUR_CHAR_INT( 'h', 'i', 'n', 't' ),//HintTrackReferenceAtomType BOX_mdia = FOUR_CHAR_INT( 'm', 'd', 'i', 'a' ),//MediaAtomType BOX_mdat = FOUR_CHAR_INT( 'm', 'd', 'a', 't' ),//MediaDataAtomType BOX_mdhd = FOUR_CHAR_INT( 'm', 'd', 'h', 'd' ),//MediaHeaderAtomType BOX_minf = FOUR_CHAR_INT( 'm', 'i', 'n', 'f' ),//MediaInformationAtomType BOX_moov = FOUR_CHAR_INT( 'm', 'o', 'o', 'v' ),//MovieAtomType BOX_mvhd = FOUR_CHAR_INT( 'm', 'v', 'h', 'd' ),//MovieHeaderAtomType BOX_stsd = FOUR_CHAR_INT( 's', 't', 's', 'd' ),//SampleDescriptionAtomType BOX_stsz = FOUR_CHAR_INT( 's', 't', 's', 'z' ),//SampleSizeAtomType BOX_stz2 = FOUR_CHAR_INT( 's', 't', 'z', '2' ),//CompactSampleSizeAtomType BOX_stbl = FOUR_CHAR_INT( 's', 't', 'b', 'l' ),//SampleTableAtomType BOX_stsc = FOUR_CHAR_INT( 's', 't', 's', 'c' ),//SampleToChunkAtomType BOX_stsh = FOUR_CHAR_INT( 's', 't', 's', 'h' ),//ShadowSyncAtomType BOX_skip = FOUR_CHAR_INT( 's', 'k', 'i', 'p' ),//SkipAtomType BOX_smhd = FOUR_CHAR_INT( 's', 'm', 'h', 'd' ),//SoundMediaHeaderAtomType BOX_stss = FOUR_CHAR_INT( 's', 't', 's', 's' ),//SyncSampleAtomType BOX_stts = FOUR_CHAR_INT( 's', 't', 't', 's' ),//TimeToSampleAtomType BOX_trak = FOUR_CHAR_INT( 't', 'r', 'a', 'k' ),//TrackAtomType BOX_tkhd = FOUR_CHAR_INT( 't', 'k', 'h', 'd' ),//TrackHeaderAtomType BOX_tref = FOUR_CHAR_INT( 't', 'r', 'e', 'f' ),//TrackReferenceAtomType BOX_udta = FOUR_CHAR_INT( 'u', 'd', 't', 'a' ),//UserDataAtomType BOX_vmhd = FOUR_CHAR_INT( 'v', 'm', 'h', 'd' ),//VideoMediaHeaderAtomType BOX_url = FOUR_CHAR_INT( 'u', 'r', 'l', ' ' ), BOX_urn = FOUR_CHAR_INT( 'u', 'r', 'n', ' ' ), BOX_gnrv = FOUR_CHAR_INT( 'g', 'n', 'r', 'v' ),//GenericVisualSampleEntryAtomType BOX_gnra = FOUR_CHAR_INT( 'g', 'n', 'r', 'a' ),//GenericAudioSampleEntryAtomType //V2 atoms BOX_ftyp = FOUR_CHAR_INT( 'f', 't', 'y', 'p' ),//FileTypeAtomType BOX_padb = FOUR_CHAR_INT( 'p', 'a', 'd', 'b' ),//PaddingBitsAtomType //MP4 Atoms BOX_sdhd = FOUR_CHAR_INT( 's', 'd', 'h', 'd' ),//SceneDescriptionMediaHeaderAtomType BOX_dpnd = FOUR_CHAR_INT( 'd', 'p', 'n', 'd' ),//StreamDependenceAtomType BOX_iods = FOUR_CHAR_INT( 'i', 'o', 'd', 's' ),//ObjectDescriptorAtomType BOX_odhd = FOUR_CHAR_INT( 'o', 'd', 'h', 'd' ),//ObjectDescriptorMediaHeaderAtomType BOX_mpod = FOUR_CHAR_INT( 'm', 'p', 'o', 'd' ),//ODTrackReferenceAtomType BOX_nmhd = FOUR_CHAR_INT( 'n', 'm', 'h', 'd' ),//MPEGMediaHeaderAtomType BOX_esds = FOUR_CHAR_INT( 'e', 's', 'd', 's' ),//ESDAtomType BOX_sync = FOUR_CHAR_INT( 's', 'y', 'n', 'c' ),//OCRReferenceAtomType BOX_ipir = FOUR_CHAR_INT( 'i', 'p', 'i', 'r' ),//IPIReferenceAtomType BOX_mp4s = FOUR_CHAR_INT( 'm', 'p', '4', 's' ),//MPEGSampleEntryAtomType BOX_mp4a = FOUR_CHAR_INT( 'm', 'p', '4', 'a' ),//MPEGAudioSampleEntryAtomType BOX_mp4v = FOUR_CHAR_INT( 'm', 'p', '4', 'v' ),//MPEGVisualSampleEntryAtomType // http://www.itscj.ipsj.or.jp/sc29/open/29view/29n7644t.doc BOX_avc1 = FOUR_CHAR_INT( 'a', 'v', 'c', '1' ), BOX_avc2 = FOUR_CHAR_INT( 'a', 'v', 'c', '2' ), BOX_svc1 = FOUR_CHAR_INT( 's', 'v', 'c', '1' ), BOX_avcC = FOUR_CHAR_INT( 'a', 'v', 'c', 'C' ), BOX_svcC = FOUR_CHAR_INT( 's', 'v', 'c', 'C' ), BOX_btrt = FOUR_CHAR_INT( 'b', 't', 'r', 't' ), BOX_m4ds = FOUR_CHAR_INT( 'm', '4', 'd', 's' ), BOX_seib = FOUR_CHAR_INT( 's', 'e', 'i', 'b' ), // H264/HEVC BOX_hev1 = FOUR_CHAR_INT( 'h', 'e', 'v', '1' ), BOX_hvc1 = FOUR_CHAR_INT( 'h', 'v', 'c', '1' ), BOX_hvcC = FOUR_CHAR_INT( 'h', 'v', 'c', 'C' ), //3GPP atoms BOX_samr = FOUR_CHAR_INT( 's', 'a', 'm', 'r' ),//AMRSampleEntryAtomType BOX_sawb = FOUR_CHAR_INT( 's', 'a', 'w', 'b' ),//WB_AMRSampleEntryAtomType BOX_damr = FOUR_CHAR_INT( 'd', 'a', 'm', 'r' ),//AMRConfigAtomType BOX_s263 = FOUR_CHAR_INT( 's', '2', '6', '3' ),//H263SampleEntryAtomType BOX_d263 = FOUR_CHAR_INT( 'd', '2', '6', '3' ),//H263ConfigAtomType //V2 atoms - Movie Fragments BOX_mvex = FOUR_CHAR_INT( 'm', 'v', 'e', 'x' ),//MovieExtendsAtomType BOX_trex = FOUR_CHAR_INT( 't', 'r', 'e', 'x' ),//TrackExtendsAtomType BOX_moof = FOUR_CHAR_INT( 'm', 'o', 'o', 'f' ),//MovieFragmentAtomType BOX_mfhd = FOUR_CHAR_INT( 'm', 'f', 'h', 'd' ),//MovieFragmentHeaderAtomType BOX_traf = FOUR_CHAR_INT( 't', 'r', 'a', 'f' ),//TrackFragmentAtomType BOX_tfhd = FOUR_CHAR_INT( 't', 'f', 'h', 'd' ),//TrackFragmentHeaderAtomType BOX_tfdt = FOUR_CHAR_INT( 't', 'f', 'd', 't' ),//TrackFragmentBaseMediaDecodeTimeBox BOX_trun = FOUR_CHAR_INT( 't', 'r', 'u', 'n' ),//TrackFragmentRunAtomType BOX_mehd = FOUR_CHAR_INT( 'm', 'e', 'h', 'd' ),//MovieExtendsHeaderBox // Object Descriptors (OD) data coding // These takes only 1 byte; this implementation translate to // + OD_BASE to keep API uniform and safe for string functions OD_BASE = FOUR_CHAR_INT( '$', '$', '$', '0' ),// OD_ESD = FOUR_CHAR_INT( '$', '$', '$', '3' ),//SDescriptor_Tag OD_DCD = FOUR_CHAR_INT( '$', '$', '$', '4' ),//DecoderConfigDescriptor_Tag OD_DSI = FOUR_CHAR_INT( '$', '$', '$', '5' ),//DecoderSpecificInfo_Tag OD_SLC = FOUR_CHAR_INT( '$', '$', '$', '6' ),//SLConfigDescriptor_Tag BOX_meta = FOUR_CHAR_INT( 'm', 'e', 't', 'a' ), BOX_ilst = FOUR_CHAR_INT( 'i', 'l', 's', 't' ), // Metagata tags, see http://atomicparsley.sourceforge.net/mpeg-4files.html BOX_calb = FOUR_CHAR_INT( '\xa9', 'a', 'l', 'b'), // album BOX_cart = FOUR_CHAR_INT( '\xa9', 'a', 'r', 't'), // artist BOX_aART = FOUR_CHAR_INT( 'a', 'A', 'R', 'T' ), // album artist BOX_ccmt = FOUR_CHAR_INT( '\xa9', 'c', 'm', 't'), // comment BOX_cday = FOUR_CHAR_INT( '\xa9', 'd', 'a', 'y'), // year (as string) BOX_cnam = FOUR_CHAR_INT( '\xa9', 'n', 'a', 'm'), // title BOX_cgen = FOUR_CHAR_INT( '\xa9', 'g', 'e', 'n'), // custom genre (as string or as byte!) BOX_trkn = FOUR_CHAR_INT( 't', 'r', 'k', 'n'), // track number (byte) BOX_disk = FOUR_CHAR_INT( 'd', 'i', 's', 'k'), // disk number (byte) BOX_cwrt = FOUR_CHAR_INT( '\xa9', 'w', 'r', 't'), // composer BOX_ctoo = FOUR_CHAR_INT( '\xa9', 't', 'o', 'o'), // encoder BOX_tmpo = FOUR_CHAR_INT( 't', 'm', 'p', 'o'), // bpm (byte) BOX_cpil = FOUR_CHAR_INT( 'c', 'p', 'i', 'l'), // compilation (byte) BOX_covr = FOUR_CHAR_INT( 'c', 'o', 'v', 'r'), // cover art (JPEG/PNG) BOX_rtng = FOUR_CHAR_INT( 'r', 't', 'n', 'g'), // rating/advisory (byte) BOX_cgrp = FOUR_CHAR_INT( '\xa9', 'g', 'r', 'p'), // grouping BOX_stik = FOUR_CHAR_INT( 's', 't', 'i', 'k'), // stik (byte) 0 = Movie 1 = Normal 2 = Audiobook 5 = Whacked Bookmark 6 = Music Video 9 = Short Film 10 = TV Show 11 = Booklet 14 = Ringtone BOX_pcst = FOUR_CHAR_INT( 'p', 'c', 's', 't'), // podcast (byte) BOX_catg = FOUR_CHAR_INT( 'c', 'a', 't', 'g'), // category BOX_keyw = FOUR_CHAR_INT( 'k', 'e', 'y', 'w'), // keyword BOX_purl = FOUR_CHAR_INT( 'p', 'u', 'r', 'l'), // podcast URL (byte) BOX_egid = FOUR_CHAR_INT( 'e', 'g', 'i', 'd'), // episode global unique ID (byte) BOX_desc = FOUR_CHAR_INT( 'd', 'e', 's', 'c'), // description BOX_clyr = FOUR_CHAR_INT( '\xa9', 'l', 'y', 'r'), // lyrics (may be > 255 bytes) BOX_tven = FOUR_CHAR_INT( 't', 'v', 'e', 'n'), // tv episode number BOX_tves = FOUR_CHAR_INT( 't', 'v', 'e', 's'), // tv episode (byte) BOX_tvnn = FOUR_CHAR_INT( 't', 'v', 'n', 'n'), // tv network name BOX_tvsh = FOUR_CHAR_INT( 't', 'v', 's', 'h'), // tv show name BOX_tvsn = FOUR_CHAR_INT( 't', 'v', 's', 'n'), // tv season (byte) BOX_purd = FOUR_CHAR_INT( 'p', 'u', 'r', 'd'), // purchase date BOX_pgap = FOUR_CHAR_INT( 'p', 'g', 'a', 'p'), // Gapless Playback (byte) //BOX_aart = FOUR_CHAR_INT( 'a', 'a', 'r', 't' ), // Album artist BOX_cART = FOUR_CHAR_INT( '\xa9', 'A', 'R', 'T'), // artist BOX_gnre = FOUR_CHAR_INT( 'g', 'n', 'r', 'e'), // 3GPP metatags (http://cpansearch.perl.org/src/JHAR/MP4-Info-1.12/Info.pm) BOX_auth = FOUR_CHAR_INT( 'a', 'u', 't', 'h'), // author BOX_titl = FOUR_CHAR_INT( 't', 'i', 't', 'l'), // title BOX_dscp = FOUR_CHAR_INT( 'd', 's', 'c', 'p'), // description BOX_perf = FOUR_CHAR_INT( 'p', 'e', 'r', 'f'), // performer BOX_mean = FOUR_CHAR_INT( 'm', 'e', 'a', 'n'), // BOX_name = FOUR_CHAR_INT( 'n', 'a', 'm', 'e'), // BOX_data = FOUR_CHAR_INT( 'd', 'a', 't', 'a'), // // these from http://lists.mplayerhq.hu/pipermail/ffmpeg-devel/2008-September/053151.html BOX_albm = FOUR_CHAR_INT( 'a', 'l', 'b', 'm'), // album BOX_yrrc = FOUR_CHAR_INT( 'y', 'r', 'r', 'c') // album }; // Video track : 'vide' #define MP4E_HANDLER_TYPE_VIDE 0x76696465 // Audio track : 'soun' #define MP4E_HANDLER_TYPE_SOUN 0x736F756E // General MPEG-4 systems streams (without specific handler). // Used for private stream, as suggested in http://www.mp4ra.org/handler.html #define MP4E_HANDLER_TYPE_GESM 0x6765736D typedef struct { boxsize_t size; boxsize_t offset; unsigned duration; unsigned flag_random_access; } sample_t; typedef struct { unsigned char *data; int bytes; int capacity; } minimp4_vector_t; typedef struct { MP4E_track_t info; minimp4_vector_t smpl; // sample descriptor minimp4_vector_t pending_sample; minimp4_vector_t vsps; // or dsi for audio minimp4_vector_t vpps; // not used for audio minimp4_vector_t vvps; // used for HEVC } track_t; typedef struct MP4E_mux_tag { minimp4_vector_t tracks; int64_t write_pos; int (*write_callback)(int64_t offset, const void *buffer, size_t size, void *token); void *token; char *text_comment; int sequential_mode_flag; int enable_fragmentation; // flag, indicating streaming-friendly 'fragmentation' mode int fragments_count; // # of fragments in 'fragmentation' mode } MP4E_mux_t; static const unsigned char box_ftyp[] = { #if 1 0,0,0,0x18,'f','t','y','p', 'm','p','4','2',0,0,0,0, 'm','p','4','2','i','s','o','m', #else // as in ffmpeg 0,0,0,0x20,'f','t','y','p', 'i','s','o','m',0,0,2,0, 'm','p','4','1','i','s','o','m', 'i','s','o','2','a','v','c','1', #endif }; /** * Endian-independent byte-write macros */ #define WR(x, n) *p++ = (unsigned char)((x) >> 8*n) #define WRITE_1(x) WR(x, 0); #define WRITE_2(x) WR(x, 1); WR(x, 0); #define WRITE_3(x) WR(x, 2); WR(x, 1); WR(x, 0); #define WRITE_4(x) WR(x, 3); WR(x, 2); WR(x, 1); WR(x, 0); #define WR4(p, x) (p)[0] = (char)((x) >> 8*3); (p)[1] = (char)((x) >> 8*2); (p)[2] = (char)((x) >> 8*1); (p)[3] = (char)((x)); // Finish atom: update atom size field #define END_ATOM --stack; WR4((unsigned char*)*stack, p - *stack); // Initiate atom: save position of size field on stack #define ATOM(x) *stack++ = p; p += 4; WRITE_4(x); // Atom with 'FullAtomVersionFlags' field #define ATOM_FULL(x, flag) ATOM(x); WRITE_4(flag); #define ERR(func) { int err = func; if (err) return err; } /** Allocate vector with given size, return 1 on success, 0 on fail */ static int minimp4_vector_init(minimp4_vector_t *h, int capacity) { h->bytes = 0; h->capacity = capacity; h->data = capacity ? (unsigned char *)malloc(capacity) : NULL; return !capacity || !!h->data; } /** Deallocates vector memory */ static void minimp4_vector_reset(minimp4_vector_t *h) { if (h->data) free(h->data); memset(h, 0, sizeof(minimp4_vector_t)); } /** Reallocate vector memory to the given size */ static int minimp4_vector_grow(minimp4_vector_t *h, int bytes) { void *p; int new_size = h->capacity*2 + 1024; if (new_size < h->capacity + bytes) new_size = h->capacity + bytes + 1024; p = realloc(h->data, new_size); if (!p) return 0; h->data = (unsigned char*)p; h->capacity = new_size; return 1; } /** Allocates given number of bytes at the end of vector data, increasing vector memory if necessary. Return allocated memory. */ static unsigned char *minimp4_vector_alloc_tail(minimp4_vector_t *h, int bytes) { unsigned char *p; if (!h->data && !minimp4_vector_init(h, 2*bytes + 1024)) return NULL; if ((h->capacity - h->bytes) < bytes && !minimp4_vector_grow(h, bytes)) return NULL; assert(h->data); assert((h->capacity - h->bytes) >= bytes); p = h->data + h->bytes; h->bytes += bytes; return p; } /** Append data to the end of the vector (accumulate ot enqueue) */ static unsigned char *minimp4_vector_put(minimp4_vector_t *h, const void *buf, int bytes) { unsigned char *tail = minimp4_vector_alloc_tail(h, bytes); if (tail) memcpy(tail, buf, bytes); return tail; } /** * Allocates and initialize mp4 multiplexer * return multiplexor handle on success; NULL on failure */ MP4E_mux_t *MP4E_open(int sequential_mode_flag, int enable_fragmentation, void *token, int (*write_callback)(int64_t offset, const void *buffer, size_t size, void *token)) { if (write_callback(0, box_ftyp, sizeof(box_ftyp), token)) // Write fixed header: 'ftyp' box return 0; MP4E_mux_t *mux = (MP4E_mux_t*)malloc(sizeof(MP4E_mux_t)); if (!mux) return mux; mux->sequential_mode_flag = sequential_mode_flag || enable_fragmentation; mux->enable_fragmentation = enable_fragmentation; mux->fragments_count = 0; mux->write_callback = write_callback; mux->token = token; mux->text_comment = NULL; mux->write_pos = sizeof(box_ftyp); if (!mux->sequential_mode_flag) { // Write filler, which would be updated later if (mux->write_callback(mux->write_pos, box_ftyp, 8, mux->token)) { free(mux); return 0; } mux->write_pos += 16; // box_ftyp + box_free for 32bit or 64bit size encoding } minimp4_vector_init(&mux->tracks, 2*sizeof(track_t)); return mux; } /** * Add new track */ int MP4E_add_track(MP4E_mux_t *mux, const MP4E_track_t *track_data) { track_t *tr; int ntr = mux->tracks.bytes / sizeof(track_t); if (!mux || !track_data) return MP4E_STATUS_BAD_ARGUMENTS; tr = (track_t*)minimp4_vector_alloc_tail(&mux->tracks, sizeof(track_t)); if (!tr) return MP4E_STATUS_NO_MEMORY; memset(tr, 0, sizeof(track_t)); memcpy(&tr->info, track_data, sizeof(*track_data)); if (!minimp4_vector_init(&tr->smpl, 256)) return MP4E_STATUS_NO_MEMORY; minimp4_vector_init(&tr->vsps, 0); minimp4_vector_init(&tr->vpps, 0); minimp4_vector_init(&tr->pending_sample, 0); return ntr; } // static const unsigned char *next_dsi(const unsigned char *p, const unsigned char *end, int *bytes) // { // if (p < end + 2) // { // *bytes = p[0]*256 + p[1]; // return p + 2; // } else // return NULL; // } static int append_mem(minimp4_vector_t *v, const void *mem, int bytes) { int i; unsigned char size[2]; const unsigned char *p = v->data; for (i = 0; i + 2 < v->bytes;) { int cb = p[i]*256 + p[i + 1]; if (cb == bytes && !memcmp(p + i + 2, mem, cb)) return 1; i += 2 + cb; } size[0] = bytes >> 8; size[1] = bytes; return minimp4_vector_put(v, size, 2) && minimp4_vector_put(v, mem, bytes); } static int items_count(minimp4_vector_t *v) { int i, count = 0; const unsigned char *p = v->data; for (i = 0; i + 2 < v->bytes;) { int cb = p[i]*256 + p[i + 1]; count++; i += 2 + cb; } return count; } int MP4E_set_dsi(MP4E_mux_t *mux, int track_id, const void *dsi, int bytes) { track_t* tr = ((track_t*)mux->tracks.data) + track_id; assert(tr->info.track_media_kind == e_audio || tr->info.track_media_kind == e_private); if (tr->vsps.bytes) return MP4E_STATUS_ONLY_ONE_DSI_ALLOWED; // only one DSI allowed return append_mem(&tr->vsps, dsi, bytes) ? MP4E_STATUS_OK : MP4E_STATUS_NO_MEMORY; } int MP4E_set_vps(MP4E_mux_t *mux, int track_id, const void *vps, int bytes) { track_t* tr = ((track_t*)mux->tracks.data) + track_id; assert(tr->info.track_media_kind == e_video); return append_mem(&tr->vvps, vps, bytes) ? MP4E_STATUS_OK : MP4E_STATUS_NO_MEMORY; } int MP4E_set_sps(MP4E_mux_t *mux, int track_id, const void *sps, int bytes) { track_t* tr = ((track_t*)mux->tracks.data) + track_id; assert(tr->info.track_media_kind == e_video); return append_mem(&tr->vsps, sps, bytes) ? MP4E_STATUS_OK : MP4E_STATUS_NO_MEMORY; } int MP4E_set_pps(MP4E_mux_t *mux, int track_id, const void *pps, int bytes) { track_t* tr = ((track_t*)mux->tracks.data) + track_id; assert(tr->info.track_media_kind == e_video); return append_mem(&tr->vpps, pps, bytes) ? MP4E_STATUS_OK : MP4E_STATUS_NO_MEMORY; } static unsigned get_duration(const track_t *tr) { unsigned i, sum_duration = 0; const sample_t *s = (const sample_t *)tr->smpl.data; for (i = 0; i < tr->smpl.bytes/sizeof(sample_t); i++) { sum_duration += s[i].duration; } return sum_duration; } static int write_pending_data(MP4E_mux_t *mux, track_t *tr) { // if have pending sample && have at least one sample in the index if (tr->pending_sample.bytes > 0 && tr->smpl.bytes >= (int)sizeof(sample_t)) { // Complete pending sample sample_t *smpl_desc; unsigned char base[8], *p = base; assert(mux->sequential_mode_flag); // Write each sample to a separate atom assert(mux->sequential_mode_flag); // Separate atom needed for sequential_mode only WRITE_4(tr->pending_sample.bytes + 8); WRITE_4(BOX_mdat); ERR(mux->write_callback(mux->write_pos, base, p - base, mux->token)); mux->write_pos += p - base; // Update sample descriptor with size and offset smpl_desc = ((sample_t*)minimp4_vector_alloc_tail(&tr->smpl, 0)) - 1; smpl_desc->size = tr->pending_sample.bytes; smpl_desc->offset = (boxsize_t)mux->write_pos; // Write data ERR(mux->write_callback(mux->write_pos, tr->pending_sample.data, tr->pending_sample.bytes, mux->token)); mux->write_pos += tr->pending_sample.bytes; // reset buffer tr->pending_sample.bytes = 0; } return MP4E_STATUS_OK; } static int add_sample_descriptor(MP4E_mux_t *mux, track_t *tr, int data_bytes, int duration, int kind) { sample_t smp; smp.size = data_bytes; smp.offset = (boxsize_t)mux->write_pos; smp.duration = (duration ? duration : (int)tr->info.default_duration); smp.flag_random_access = (kind == MP4E_SAMPLE_RANDOM_ACCESS); return NULL != minimp4_vector_put(&tr->smpl, &smp, sizeof(sample_t)); } static int mp4e_flush_index(MP4E_mux_t *mux); /** * Write Movie Fragment: 'moof' box */ static int mp4e_write_fragment_header(MP4E_mux_t *mux, int track_num, int data_bytes, int duration, int kind #if MP4D_TFDT_SUPPORT , uint64_t timestamp #endif ) { unsigned char base[888], *p = base; unsigned char *stack_base[20]; // atoms nesting stack unsigned char **stack = stack_base; unsigned char *pdata_offset; unsigned flags; enum { default_sample_duration_present = 0x000008, default_sample_flags_present = 0x000020, }; track_t *tr = ((track_t*)mux->tracks.data) + track_num; ATOM(BOX_moof) ATOM_FULL(BOX_mfhd, 0) WRITE_4(mux->fragments_count); // start from 1 END_ATOM ATOM(BOX_traf) flags = 0; if (tr->info.track_media_kind == e_video) flags |= 0x20; // default-sample-flags-present else flags |= 0x08; // default-sample-duration-present flags = (tr->info.track_media_kind == e_video) ? 0x20020 : 0x20008; ATOM_FULL(BOX_tfhd, flags) WRITE_4(track_num + 1); // track_ID if (tr->info.track_media_kind == e_video) { WRITE_4(0x1010000); // default_sample_flags } else { WRITE_4(duration); } END_ATOM #if MP4D_TFDT_SUPPORT ATOM_FULL(BOX_tfdt, 0x01000000) // version 1 WRITE_4(timestamp >> 32); // upper timestamp WRITE_4(timestamp & 0xffffffff); // lower timestamp END_ATOM #endif if (tr->info.track_media_kind == e_audio) { flags = 0; flags |= 0x001; // data-offset-present flags |= 0x200; // sample-size-present ATOM_FULL(BOX_trun, flags) WRITE_4(1); // sample_count pdata_offset = p; p += 4; // save ptr to data_offset WRITE_4(data_bytes);// sample_size END_ATOM } else if (kind == MP4E_SAMPLE_RANDOM_ACCESS) { flags = 0; flags |= 0x001; // data-offset-present flags |= 0x004; // first-sample-flags-present flags |= 0x100; // sample-duration-present flags |= 0x200; // sample-size-present ATOM_FULL(BOX_trun, flags) WRITE_4(1); // sample_count pdata_offset = p; p += 4; // save ptr to data_offset WRITE_4(0x2000000); // first_sample_flags WRITE_4(duration); // sample_duration WRITE_4(data_bytes);// sample_size END_ATOM } else { flags = 0; flags |= 0x001; // data-offset-present flags |= 0x100; // sample-duration-present flags |= 0x200; // sample-size-present ATOM_FULL(BOX_trun, flags) WRITE_4(1); // sample_count pdata_offset = p; p += 4; // save ptr to data_offset WRITE_4(duration); // sample_duration WRITE_4(data_bytes);// sample_size END_ATOM } END_ATOM END_ATOM WR4(pdata_offset, (p - base) + 8); ERR(mux->write_callback(mux->write_pos, base, p - base, mux->token)); mux->write_pos += p - base; return MP4E_STATUS_OK; } static int mp4e_write_mdat_box(MP4E_mux_t *mux, uint32_t size) { unsigned char base[8], *p = base; WRITE_4(size); WRITE_4(BOX_mdat); ERR(mux->write_callback(mux->write_pos, base, p - base, mux->token)); mux->write_pos += p - base; return MP4E_STATUS_OK; } /** * Add new sample to specified track */ int MP4E_put_sample(MP4E_mux_t *mux, int track_num, const void *data, int data_bytes, int duration, int kind) { track_t *tr; if (!mux || !data) return MP4E_STATUS_BAD_ARGUMENTS; tr = ((track_t*)mux->tracks.data) + track_num; if (mux->enable_fragmentation) { #if MP4D_TFDT_SUPPORT // NOTE: assume a constant `duration` to calculate current timestamp uint64_t timestamp = (uint64_t)mux->fragments_count * duration; #endif if (!mux->fragments_count++) ERR(mp4e_flush_index(mux)); // write file headers before 1st sample // write MOOF + MDAT + sample data #if MP4D_TFDT_SUPPORT ERR(mp4e_write_fragment_header(mux, track_num, data_bytes, duration, kind, timestamp)); #else ERR(mp4e_write_fragment_header(mux, track_num, data_bytes, duration, kind)); #endif // write MDAT box for each sample ERR(mp4e_write_mdat_box(mux, data_bytes + 8)); ERR(mux->write_callback(mux->write_pos, data, data_bytes, mux->token)); mux->write_pos += data_bytes; return MP4E_STATUS_OK; } if (kind != MP4E_SAMPLE_CONTINUATION) { if (mux->sequential_mode_flag) ERR(write_pending_data(mux, tr)); if (!add_sample_descriptor(mux, tr, data_bytes, duration, kind)) return MP4E_STATUS_NO_MEMORY; } else { if (!mux->sequential_mode_flag) { sample_t *smpl_desc; if ((size_t)tr->smpl.bytes < sizeof(sample_t)) return MP4E_STATUS_NO_MEMORY; // write continuation, but there are no samples in the index // Accumulate size of the continuation in the sample descriptor smpl_desc = (sample_t*)(tr->smpl.data + tr->smpl.bytes) - 1; smpl_desc->size += data_bytes; } } if (mux->sequential_mode_flag) { if (!minimp4_vector_put(&tr->pending_sample, data, data_bytes)) return MP4E_STATUS_NO_MEMORY; } else { ERR(mux->write_callback(mux->write_pos, data, data_bytes, mux->token)); mux->write_pos += data_bytes; } return MP4E_STATUS_OK; } /** * calculate size of length field of OD box */ static int od_size_of_size(int size) { int i, size_of_size = 1; for (i = size; i > 0x7F; i -= 0x7F) size_of_size++; return size_of_size; } /** * Add or remove MP4 file text comment according to Apple specs: * https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW1 * http://atomicparsley.sourceforge.net/mpeg-4files.html * note that ISO did not specify comment format. */ int MP4E_set_text_comment(MP4E_mux_t *mux, const char *comment) { if (!mux || !comment) return MP4E_STATUS_BAD_ARGUMENTS; if (mux->text_comment) free(mux->text_comment); mux->text_comment = strdup(comment); if (!mux->text_comment) return MP4E_STATUS_NO_MEMORY; return MP4E_STATUS_OK; } /** * Write file index 'moov' box with all its boxes and indexes */ static int mp4e_flush_index(MP4E_mux_t *mux) { unsigned char *stack_base[20]; // atoms nesting stack unsigned char **stack = stack_base; unsigned char *base, *p; unsigned int ntr, index_bytes, ntracks = mux->tracks.bytes / sizeof(track_t); int i, err; // How much memory needed for indexes // Experimental data: // file with 1 track = 560 bytes // file with 2 tracks = 972 bytes // track size = 412 bytes; // file header size = 148 bytes #define FILE_HEADER_BYTES 256 #define TRACK_HEADER_BYTES 512 index_bytes = FILE_HEADER_BYTES; if (mux->text_comment) index_bytes += 128 + strlen(mux->text_comment); for (ntr = 0; ntr < ntracks; ntr++) { track_t *tr = ((track_t*)mux->tracks.data) + ntr; index_bytes += TRACK_HEADER_BYTES; // fixed amount (implementation-dependent) // may need extra 4 bytes for duration field + 4 bytes for worst-case random access box index_bytes += tr->smpl.bytes * (sizeof(sample_t) + 4 + 4) / sizeof(sample_t); index_bytes += tr->vsps.bytes; index_bytes += tr->vpps.bytes; ERR(write_pending_data(mux, tr)); } base = (unsigned char*)malloc(index_bytes); if (!base) return MP4E_STATUS_NO_MEMORY; p = base; if (!mux->sequential_mode_flag) { // update size of mdat box. // One of 2 points, which requires random file access. // Second is optional duration update at beginning of file in fragmentation mode. // This can be avoided using "till eof" size code, but in this case indexes must be // written before the mdat.... int64_t size = mux->write_pos - sizeof(box_ftyp); const int64_t size_limit = (int64_t)(uint64_t)0xfffffffe; if (size > size_limit) { WRITE_4(1); WRITE_4(BOX_mdat); WRITE_4((size >> 32) & 0xffffffff); WRITE_4(size & 0xffffffff); } else { WRITE_4(8); WRITE_4(BOX_free); WRITE_4(size - 8); WRITE_4(BOX_mdat); } ERR(mux->write_callback(sizeof(box_ftyp), base, p - base, mux->token)); p = base; } // Write index atoms; order taken from Table 1 of [1] #define MOOV_TIMESCALE 1000 ATOM(BOX_moov); ATOM_FULL(BOX_mvhd, 0); WRITE_4(0); // creation_time WRITE_4(0); // modification_time if (ntracks) { track_t *tr = ((track_t*)mux->tracks.data) + 0; // take 1st track unsigned duration = get_duration(tr); duration = (unsigned)(duration * 1LL * MOOV_TIMESCALE / tr->info.time_scale); WRITE_4(MOOV_TIMESCALE); // duration WRITE_4(duration); // duration } WRITE_4(0x00010000); // rate WRITE_2(0x0100); // volume WRITE_2(0); // reserved WRITE_4(0); // reserved WRITE_4(0); // reserved // matrix[9] WRITE_4(0x00010000); WRITE_4(0); WRITE_4(0); WRITE_4(0); WRITE_4(0x00010000); WRITE_4(0); WRITE_4(0); WRITE_4(0); WRITE_4(0x40000000); // pre_defined[6] WRITE_4(0); WRITE_4(0); WRITE_4(0); WRITE_4(0); WRITE_4(0); WRITE_4(0); //next_track_ID is a non-zero integer that indicates a value to use for the track ID of the next track to be //added to this presentation. Zero is not a valid track ID value. The value of next_track_ID shall be //larger than the largest track-ID in use. WRITE_4(ntracks + 1); END_ATOM; for (ntr = 0; ntr < ntracks; ntr++) { track_t *tr = ((track_t*)mux->tracks.data) + ntr; unsigned duration = get_duration(tr); int samples_count = tr->smpl.bytes / sizeof(sample_t); const sample_t *sample = (const sample_t *)tr->smpl.data; unsigned handler_type; const char *handler_ascii = NULL; if (mux->enable_fragmentation) samples_count = 0; else if (samples_count <= 0) continue; // skip empty track switch (tr->info.track_media_kind) { case e_audio: handler_type = MP4E_HANDLER_TYPE_SOUN; handler_ascii = "SoundHandler"; break; case e_video: handler_type = MP4E_HANDLER_TYPE_VIDE; handler_ascii = "VideoHandler"; break; case e_private: handler_type = MP4E_HANDLER_TYPE_GESM; break; default: return MP4E_STATUS_BAD_ARGUMENTS; } ATOM(BOX_trak); ATOM_FULL(BOX_tkhd, 7); // flag: 1=trak enabled; 2=track in movie; 4=track in preview WRITE_4(0); // creation_time WRITE_4(0); // modification_time WRITE_4(ntr + 1); // track_ID WRITE_4(0); // reserved WRITE_4((unsigned)(duration * 1LL * MOOV_TIMESCALE / tr->info.time_scale)); WRITE_4(0); WRITE_4(0); // reserved[2] WRITE_2(0); // layer WRITE_2(0); // alternate_group WRITE_2(0x0100); // volume {if track_is_audio 0x0100 else 0}; WRITE_2(0); // reserved // matrix[9] WRITE_4(0x00010000); WRITE_4(0); WRITE_4(0); WRITE_4(0); WRITE_4(0x00010000); WRITE_4(0); WRITE_4(0); WRITE_4(0); WRITE_4(0x40000000); if (tr->info.track_media_kind == e_audio || tr->info.track_media_kind == e_private) { WRITE_4(0); // width WRITE_4(0); // height } else { WRITE_4(tr->info.u.v.width*0x10000); // width WRITE_4(tr->info.u.v.height*0x10000); // height } END_ATOM; ATOM(BOX_mdia); ATOM_FULL(BOX_mdhd, 0); WRITE_4(0); // creation_time WRITE_4(0); // modification_time WRITE_4(tr->info.time_scale); WRITE_4(duration); // duration { int lang_code = ((tr->info.language[0] & 31) << 10) | ((tr->info.language[1] & 31) << 5) | (tr->info.language[2] & 31); WRITE_2(lang_code); // language } WRITE_2(0); // pre_defined END_ATOM; ATOM_FULL(BOX_hdlr, 0); WRITE_4(0); // pre_defined WRITE_4(handler_type); // handler_type WRITE_4(0); WRITE_4(0); WRITE_4(0); // reserved[3] // name is a null-terminated string in UTF-8 characters which gives a human-readable name for the track type (for debugging and inspection purposes). // set mdia hdlr name field to what quicktime uses. // Sony smartphone may fail to decode short files w/o handler name if (handler_ascii) { for (i = 0; i < (int)strlen(handler_ascii) + 1; i++) { WRITE_1(handler_ascii[i]); } } else { WRITE_4(0); } END_ATOM; ATOM(BOX_minf); if (tr->info.track_media_kind == e_audio) { // Sound Media Header Box ATOM_FULL(BOX_smhd, 0); WRITE_2(0); // balance WRITE_2(0); // reserved END_ATOM; } if (tr->info.track_media_kind == e_video) { // mandatory Video Media Header Box ATOM_FULL(BOX_vmhd, 1); WRITE_2(0); // graphicsmode WRITE_2(0); WRITE_2(0); WRITE_2(0); // opcolor[3] END_ATOM; } ATOM(BOX_dinf); ATOM_FULL(BOX_dref, 0); WRITE_4(1); // entry_count // If the flag is set indicating that the data is in the same file as this box, then no string (not even an empty one) // shall be supplied in the entry field. // ASP the correct way to avoid supply the string, is to use flag 1 // otherwise ISO reference demux crashes ATOM_FULL(BOX_url, 1); END_ATOM; END_ATOM; END_ATOM; ATOM(BOX_stbl); ATOM_FULL(BOX_stsd, 0); WRITE_4(1); // entry_count; if (tr->info.track_media_kind == e_audio || tr->info.track_media_kind == e_private) { // AudioSampleEntry() assume MP4E_HANDLER_TYPE_SOUN if (tr->info.track_media_kind == e_audio) { ATOM(BOX_mp4a); } else { ATOM(BOX_mp4s); } // SampleEntry WRITE_4(0); WRITE_2(0); // reserved[6] WRITE_2(1); // data_reference_index; - this is a tag for descriptor below if (tr->info.track_media_kind == e_audio) { // AudioSampleEntry WRITE_4(0); WRITE_4(0); // reserved[2] WRITE_2(tr->info.u.a.channelcount); // channelcount WRITE_2(16); // samplesize WRITE_4(0); // pre_defined+reserved WRITE_4((tr->info.time_scale << 16)); // samplerate == = {timescale of media}<<16; } ATOM_FULL(BOX_esds, 0); if (tr->vsps.bytes > 0) { int dsi_bytes = tr->vsps.bytes - 2; // - two bytes size field int dsi_size_size = od_size_of_size(dsi_bytes); int dcd_bytes = dsi_bytes + dsi_size_size + 1 + (1 + 1 + 3 + 4 + 4); int dcd_size_size = od_size_of_size(dcd_bytes); int esd_bytes = dcd_bytes + dcd_size_size + 1 + 3; #define WRITE_OD_LEN(size) if (size > 0x7F) do { size -= 0x7F; WRITE_1(0x00ff); } while (size > 0x7F); WRITE_1(size) WRITE_1(3); // OD_ESD WRITE_OD_LEN(esd_bytes); WRITE_2(0); // ES_ID(2) // TODO - what is this? WRITE_1(0); // flags(1) WRITE_1(4); // OD_DCD WRITE_OD_LEN(dcd_bytes); if (tr->info.track_media_kind == e_audio) { WRITE_1(MP4_OBJECT_TYPE_AUDIO_ISO_IEC_14496_3); // OD_DCD WRITE_1(5 << 2); // stream_type == AudioStream } else { // http://xhelmboyx.tripod.com/formats/mp4-layout.txt WRITE_1(208); // 208 = private video WRITE_1(32 << 2); // stream_type == user private } WRITE_3(tr->info.u.a.channelcount * 6144/8); // bufferSizeDB in bytes, constant as in reference decoder WRITE_4(0); // maxBitrate TODO WRITE_4(0); // avg_bitrate_bps TODO WRITE_1(5); // OD_DSI WRITE_OD_LEN(dsi_bytes); for (i = 0; i < dsi_bytes; i++) { WRITE_1(tr->vsps.data[2 + i]); } } END_ATOM; END_ATOM; } if (tr->info.track_media_kind == e_video && (MP4_OBJECT_TYPE_AVC == tr->info.object_type_indication || MP4_OBJECT_TYPE_HEVC == tr->info.object_type_indication)) { int numOfSequenceParameterSets = items_count(&tr->vsps); int numOfPictureParameterSets = items_count(&tr->vpps); if (MP4_OBJECT_TYPE_AVC == tr->info.object_type_indication) { ATOM(BOX_avc1); } else { ATOM(BOX_hvc1); } // VisualSampleEntry 8.16.2 // extends SampleEntry WRITE_2(0); // reserved WRITE_2(0); // reserved WRITE_2(0); // reserved WRITE_2(1); // data_reference_index WRITE_2(0); // pre_defined WRITE_2(0); // reserved WRITE_4(0); // pre_defined WRITE_4(0); // pre_defined WRITE_4(0); // pre_defined WRITE_2(tr->info.u.v.width); WRITE_2(tr->info.u.v.height); WRITE_4(0x00480000); // horizresolution = 72 dpi WRITE_4(0x00480000); // vertresolution = 72 dpi WRITE_4(0); // reserved WRITE_2(1); // frame_count for (i = 0; i < 32; i++) { WRITE_1(0); // compressorname } WRITE_2(24); // depth WRITE_2(-1); // pre_defined if (MP4_OBJECT_TYPE_AVC == tr->info.object_type_indication) { ATOM(BOX_avcC); // AVCDecoderConfigurationRecord 5.2.4.1.1 WRITE_1(1); // configurationVersion WRITE_1(tr->vsps.data[2 + 1]); WRITE_1(tr->vsps.data[2 + 2]); WRITE_1(tr->vsps.data[2 + 3]); WRITE_1(255); // 0xfc + NALU_len - 1 WRITE_1(0xe0 | numOfSequenceParameterSets); for (i = 0; i < tr->vsps.bytes; i++) { WRITE_1(tr->vsps.data[i]); } WRITE_1(numOfPictureParameterSets); for (i = 0; i < tr->vpps.bytes; i++) { WRITE_1(tr->vpps.data[i]); } } else { int numOfVPS = items_count(&tr->vpps); ATOM(BOX_hvcC); // TODO: read actual params from stream WRITE_1(1); // configurationVersion WRITE_1(1); // Profile Space (2), Tier (1), Profile (5) WRITE_4(0x60000000); // Profile Compatibility WRITE_2(0); // progressive, interlaced, non packed constraint, frame only constraint flags WRITE_4(0); // constraint indicator flags WRITE_1(0); // level_idc WRITE_2(0xf000); // Min Spatial Segmentation WRITE_1(0xfc); // Parallelism Type WRITE_1(0xfc); // Chroma Format WRITE_1(0xf8); // Luma Depth WRITE_1(0xf8); // Chroma Depth WRITE_2(0); // Avg Frame Rate WRITE_1(3); // ConstantFrameRate (2), NumTemporalLayers (3), TemporalIdNested (1), LengthSizeMinusOne (2) WRITE_1(3); // Num Of Arrays WRITE_1((1 << 7) | (HEVC_NAL_VPS & 0x3f)); // Array Completeness + NAL Unit Type WRITE_2(numOfVPS); for (i = 0; i < tr->vvps.bytes; i++) { WRITE_1(tr->vvps.data[i]); } WRITE_1((1 << 7) | (HEVC_NAL_SPS & 0x3f)); WRITE_2(numOfSequenceParameterSets); for (i = 0; i < tr->vsps.bytes; i++) { WRITE_1(tr->vsps.data[i]); } WRITE_1((1 << 7) | (HEVC_NAL_PPS & 0x3f)); WRITE_2(numOfPictureParameterSets); for (i = 0; i < tr->vpps.bytes; i++) { WRITE_1(tr->vpps.data[i]); } } END_ATOM; END_ATOM; } END_ATOM; /************************************************************************/ /* indexes */ /************************************************************************/ // Time to Sample Box ATOM_FULL(BOX_stts, 0); { unsigned char *pentry_count = p; int cnt = 1, entry_count = 0; WRITE_4(0); for (i = 0; i < samples_count; i++, cnt++) { if (i == (samples_count - 1) || sample[i].duration != sample[i + 1].duration) { WRITE_4(cnt); WRITE_4(sample[i].duration); cnt = 0; entry_count++; } } WR4(pentry_count, entry_count); } END_ATOM; // Sample To Chunk Box ATOM_FULL(BOX_stsc, 0); if (mux->enable_fragmentation) { WRITE_4(0); // entry_count } else { WRITE_4(1); // entry_count WRITE_4(1); // first_chunk; WRITE_4(1); // samples_per_chunk; WRITE_4(1); // sample_description_index; } END_ATOM; // Sample Size Box ATOM_FULL(BOX_stsz, 0); WRITE_4(0); // sample_size If this field is set to 0, then the samples have different sizes, and those sizes // are stored in the sample size table. WRITE_4(samples_count); // sample_count; for (i = 0; i < samples_count; i++) { WRITE_4(sample[i].size); } END_ATOM; // Chunk Offset Box int is_64_bit = 0; if (samples_count && sample[samples_count - 1].offset > 0xffffffff) is_64_bit = 1; if (!is_64_bit) { ATOM_FULL(BOX_stco, 0); WRITE_4(samples_count); for (i = 0; i < samples_count; i++) { WRITE_4(sample[i].offset); } } else { ATOM_FULL(BOX_co64, 0); WRITE_4(samples_count); for (i = 0; i < samples_count; i++) { WRITE_4((sample[i].offset >> 32) & 0xffffffff); WRITE_4(sample[i].offset & 0xffffffff); } } END_ATOM; // Sync Sample Box { int ra_count = 0; for (i = 0; i < samples_count; i++) { ra_count += !!sample[i].flag_random_access; } if (ra_count != samples_count) { // If the sync sample box is not present, every sample is a random access point. ATOM_FULL(BOX_stss, 0); WRITE_4(ra_count); for (i = 0; i < samples_count; i++) { if (sample[i].flag_random_access) { WRITE_4(i + 1); } } END_ATOM; } } END_ATOM; END_ATOM; END_ATOM; END_ATOM; } // tracks loop if (mux->text_comment) { ATOM(BOX_udta); ATOM_FULL(BOX_meta, 0); ATOM_FULL(BOX_hdlr, 0); WRITE_4(0); // pre_defined #define MP4E_HANDLER_TYPE_MDIR 0x6d646972 WRITE_4(MP4E_HANDLER_TYPE_MDIR); // handler_type WRITE_4(0); WRITE_4(0); WRITE_4(0); // reserved[3] WRITE_4(0); // name is a null-terminated string in UTF-8 characters which gives a human-readable name for the track type (for debugging and inspection purposes). END_ATOM; ATOM(BOX_ilst); ATOM(BOX_ccmt); ATOM(BOX_data); WRITE_4(1); // type WRITE_4(0); // lang for (i = 0; i < (int)strlen(mux->text_comment) + 1; i++) { WRITE_1(mux->text_comment[i]); } END_ATOM; END_ATOM; END_ATOM; END_ATOM; END_ATOM; } if (mux->enable_fragmentation) { track_t *tr = ((track_t*)mux->tracks.data) + 0; uint32_t movie_duration = get_duration(tr); ATOM(BOX_mvex); ATOM_FULL(BOX_mehd, 0); WRITE_4(movie_duration); // duration END_ATOM; for (ntr = 0; ntr < ntracks; ntr++) { ATOM_FULL(BOX_trex, 0); WRITE_4(ntr + 1); // track_ID WRITE_4(1); // default_sample_description_index WRITE_4(0); // default_sample_duration WRITE_4(0); // default_sample_size WRITE_4(0); // default_sample_flags END_ATOM; } END_ATOM; } END_ATOM; // moov atom assert((unsigned)(p - base) <= index_bytes); err = mux->write_callback(mux->write_pos, base, p - base, mux->token); mux->write_pos += p - base; free(base); return err; } int MP4E_close(MP4E_mux_t *mux) { int err = MP4E_STATUS_OK; unsigned ntr, ntracks; if (!mux) return MP4E_STATUS_BAD_ARGUMENTS; if (!mux->enable_fragmentation) err = mp4e_flush_index(mux); if (mux->text_comment) free(mux->text_comment); ntracks = mux->tracks.bytes / sizeof(track_t); for (ntr = 0; ntr < ntracks; ntr++) { track_t *tr = ((track_t*)mux->tracks.data) + ntr; minimp4_vector_reset(&tr->vsps); minimp4_vector_reset(&tr->vpps); minimp4_vector_reset(&tr->smpl); minimp4_vector_reset(&tr->pending_sample); } minimp4_vector_reset(&mux->tracks); free(mux); return err; } typedef uint32_t bs_item_t; #define BS_BITS 32 typedef struct { // Look-ahead bit cache: MSB aligned, 17 bits guaranteed, zero stuffing unsigned int cache; // Bit counter = 16 - (number of bits in wCache) // cache refilled when cache_free_bits >= 0 int cache_free_bits; // Current read position const uint16_t *buf; // original data buffer const uint16_t *origin; // original data buffer length, bytes unsigned origin_bytes; } bit_reader_t; #define LOAD_SHORT(x) ((uint16_t)(x << 8) | (x >> 8)) static unsigned int show_bits(bit_reader_t *bs, int n) { unsigned int retval; assert(n > 0 && n <= 16); retval = (unsigned int)(bs->cache >> (32 - n)); return retval; } static void flush_bits(bit_reader_t *bs, int n) { assert(n >= 0 && n <= 16); bs->cache <<= n; bs->cache_free_bits += n; if (bs->cache_free_bits >= 0) { bs->cache |= ((uint32_t)LOAD_SHORT(*bs->buf)) << bs->cache_free_bits; bs->buf++; bs->cache_free_bits -= 16; } } static unsigned int get_bits(bit_reader_t *bs, int n) { unsigned int retval = show_bits(bs, n); flush_bits(bs, n); return retval; } static void set_pos_bits(bit_reader_t *bs, unsigned pos_bits) { assert((int)pos_bits >= 0); bs->buf = bs->origin + pos_bits/16; bs->cache = 0; bs->cache_free_bits = 16; flush_bits(bs, 0); flush_bits(bs, pos_bits & 15); } static unsigned get_pos_bits(const bit_reader_t *bs) { // Current bitbuffer position = // position of next wobits in the internal buffer // minus bs, available in bit cache wobits unsigned pos_bits = (unsigned)(bs->buf - bs->origin)*16; pos_bits -= 16 - bs->cache_free_bits; assert((int)pos_bits >= 0); return pos_bits; } static int remaining_bits(const bit_reader_t *bs) { return bs->origin_bytes * 8 - get_pos_bits(bs); } static void init_bits(bit_reader_t *bs, const void *data, unsigned data_bytes) { bs->origin = (const uint16_t *)data; bs->origin_bytes = data_bytes; set_pos_bits(bs, 0); } #define GetBits(n) get_bits(bs, n) /** * Unsigned Golomb code */ static int ue_bits(bit_reader_t *bs) { int clz; int val; for (clz = 0; !get_bits(bs, 1); clz++) {} //get_bits(bs, clz + 1); val = (1 << clz) - 1 + (clz ? get_bits(bs, clz) : 0); return val; } #if MINIMP4_TRANSCODE_SPS_ID /** * Output bitstream */ typedef struct { int shift; // bit position in the cache uint32_t cache; // bit cache bs_item_t *buf; // current position bs_item_t *origin; // initial position } bs_t; #define SWAP32(x) (uint32_t)((((x) >> 24) & 0xFF) | (((x) >> 8) & 0xFF00) | (((x) << 8) & 0xFF0000) | ((x & 0xFF) << 24)) static void h264e_bs_put_bits(bs_t *bs, unsigned n, unsigned val) { assert(!(val >> n)); bs->shift -= n; assert((unsigned)n <= 32); if (bs->shift < 0) { assert(-bs->shift < 32); bs->cache |= val >> -bs->shift; *bs->buf++ = SWAP32(bs->cache); bs->shift = 32 + bs->shift; bs->cache = 0; } bs->cache |= val << bs->shift; } static void h264e_bs_flush(bs_t *bs) { *bs->buf = SWAP32(bs->cache); } static unsigned h264e_bs_get_pos_bits(const bs_t *bs) { unsigned pos_bits = (unsigned)((bs->buf - bs->origin)*BS_BITS); pos_bits += BS_BITS - bs->shift; assert((int)pos_bits >= 0); return pos_bits; } static unsigned h264e_bs_byte_align(bs_t *bs) { int pos = h264e_bs_get_pos_bits(bs); h264e_bs_put_bits(bs, -pos & 7, 0); return pos + (-pos & 7); } /** * Golomb code * 0 => 1 * 1 => 01 0 * 2 => 01 1 * 3 => 001 00 * 4 => 001 01 * * [0] => 1 * [1..2] => 01x * [3..6] => 001xx * [7..14] => 0001xxx * */ static void h264e_bs_put_golomb(bs_t *bs, unsigned val) { int size = 0; unsigned t = val + 1; do { size++; } while (t >>= 1); h264e_bs_put_bits(bs, 2*size - 1, val + 1); } static void h264e_bs_init_bits(bs_t *bs, void *data) { bs->origin = (bs_item_t*)data; bs->buf = bs->origin; bs->shift = BS_BITS; bs->cache = 0; } static int find_mem_cache(void *cache[], int cache_bytes[], int cache_size, void *mem, int bytes) { int i; if (!bytes) return -1; for (i = 0; i < cache_size; i++) { if (cache_bytes[i] == bytes && !memcmp(mem, cache[i], bytes)) return i; // found } for (i = 0; i < cache_size; i++) { if (!cache_bytes[i]) { cache[i] = malloc(bytes); if (cache[i]) { memcpy(cache[i], mem, bytes); cache_bytes[i] = bytes; } return i; // put in } } return -1; // no room } /** * 7.4.1.1. "Encapsulation of an SODB within an RBSP" */ static int remove_nal_escapes(unsigned char *dst, const unsigned char *src, int h264_data_bytes) { int i = 0, j = 0, zero_cnt = 0; for (j = 0; j < h264_data_bytes; j++) { if (zero_cnt == 2 && src[j] <= 3) { if (src[j] == 3) { if (j == h264_data_bytes - 1) { // cabac_zero_word: no action } else if (src[j + 1] <= 3) { j++; zero_cnt = 0; } else { // TODO: assume end-of-nal //return 0; } } else return 0; } dst[i++] = src[j]; if (src[j]) zero_cnt = 0; else zero_cnt++; } //while (--j > i) src[j] = 0; return i; } /** * Put NAL escape codes to the output bitstream */ static int nal_put_esc(uint8_t *d, const uint8_t *s, int n) { int i, j = 4, cntz = 0; d[0] = d[1] = d[2] = 0; d[3] = 1; // start code for (i = 0; i < n; i++) { uint8_t byte = *s++; if (cntz == 2 && byte <= 3) { d[j++] = 3; cntz = 0; } if (byte) cntz = 0; else cntz++; d[j++] = byte; } return j; } static void copy_bits(bit_reader_t *bs, bs_t *bd) { unsigned cb, bits; int bit_count = remaining_bits(bs); while (bit_count > 7) { cb = MINIMP4_MIN(bit_count - 7, 8); bits = GetBits(cb); h264e_bs_put_bits(bd, cb, bits); bit_count -= cb; } // cut extra zeros after stop-bit bits = GetBits(bit_count); for (; bit_count && ~bits & 1; bit_count--) { bits >>= 1; } if (bit_count) { h264e_bs_put_bits(bd, bit_count, bits); } } static int change_sps_id(bit_reader_t *bs, bs_t *bd, int new_id, int *old_id) { unsigned bits, sps_id, i, bytes; for (i = 0; i < 3; i++) { bits = GetBits(8); h264e_bs_put_bits(bd, 8, bits); } sps_id = ue_bits(bs); // max = 31 *old_id = sps_id; sps_id = new_id; assert(sps_id <= 31); h264e_bs_put_golomb(bd, sps_id); copy_bits(bs, bd); bytes = h264e_bs_byte_align(bd) / 8; h264e_bs_flush(bd); return bytes; } static int patch_pps(h264_sps_id_patcher_t *h, bit_reader_t *bs, bs_t *bd, int new_pps_id, int *old_id) { int bytes; unsigned pps_id = ue_bits(bs); // max = 255 unsigned sps_id = ue_bits(bs); // max = 31 *old_id = pps_id; sps_id = h->map_sps[sps_id]; pps_id = new_pps_id; assert(sps_id <= 31); assert(pps_id <= 255); h264e_bs_put_golomb(bd, pps_id); h264e_bs_put_golomb(bd, sps_id); copy_bits(bs, bd); bytes = h264e_bs_byte_align(bd) / 8; h264e_bs_flush(bd); return bytes; } static void patch_slice_header(h264_sps_id_patcher_t *h, bit_reader_t *bs, bs_t *bd) { unsigned first_mb_in_slice = ue_bits(bs); unsigned slice_type = ue_bits(bs); unsigned pps_id = ue_bits(bs); pps_id = h->map_pps[pps_id]; assert(pps_id <= 255); h264e_bs_put_golomb(bd, first_mb_in_slice); h264e_bs_put_golomb(bd, slice_type); h264e_bs_put_golomb(bd, pps_id); copy_bits(bs, bd); } static int transcode_nalu(h264_sps_id_patcher_t *h, const unsigned char *src, int nalu_bytes, unsigned char *dst) { int old_id; bit_reader_t bst[1]; bs_t bdt[1]; bit_reader_t bs[1]; bs_t bd[1]; int payload_type = src[0] & 31; *dst = *src; h264e_bs_init_bits(bd, dst + 1); init_bits(bs, src + 1, nalu_bytes - 1); h264e_bs_init_bits(bdt, dst + 1); init_bits(bst, src + 1, nalu_bytes - 1); switch(payload_type) { case 7: { int cb = change_sps_id(bst, bdt, 0, &old_id); int id = find_mem_cache(h->sps_cache, h->sps_bytes, MINIMP4_MAX_SPS, dst + 1, cb); if (id == -1) return 0; h->map_sps[old_id] = id; change_sps_id(bs, bd, id, &old_id); } break; case 8: { int cb = patch_pps(h, bst, bdt, 0, &old_id); int id = find_mem_cache(h->pps_cache, h->pps_bytes, MINIMP4_MAX_PPS, dst + 1, cb); if (id == -1) return 0; h->map_pps[old_id] = id; patch_pps(h, bs, bd, id, &old_id); } break; case 1: case 2: case 5: patch_slice_header(h, bs, bd); break; default: memcpy(dst, src, nalu_bytes); return nalu_bytes; } nalu_bytes = 1 + h264e_bs_byte_align(bd) / 8; h264e_bs_flush(bd); return nalu_bytes; } #endif /** * Set pointer just after start code (00 .. 00 01), or to EOF if not found: * * NZ NZ ... NZ 00 00 00 00 01 xx xx ... xx (EOF) * ^ ^ * non-zero head.............. here ....... or here if no start code found * */ static const uint8_t *find_start_code(const uint8_t *h264_data, int h264_data_bytes, int *zcount) { const uint8_t *eof = h264_data + h264_data_bytes; const uint8_t *p = h264_data; do { int zero_cnt = 1; const uint8_t* found = (uint8_t*)memchr(p, 0, eof - p); p = found ? found : eof; while (p + zero_cnt < eof && !p[zero_cnt]) zero_cnt++; if (zero_cnt >= 2 && p[zero_cnt] == 1) { *zcount = zero_cnt + 1; return p + zero_cnt + 1; } p += zero_cnt; } while (p < eof); *zcount = 0; return eof; } /** * Locate NAL unit in given buffer, and calculate it's length */ static const uint8_t *find_nal_unit(const uint8_t *h264_data, int h264_data_bytes, int *pnal_unit_bytes) { const uint8_t *eof = h264_data + h264_data_bytes; int zcount; const uint8_t *start = find_start_code(h264_data, h264_data_bytes, &zcount); const uint8_t *stop = start; if (start) { stop = find_start_code(start, (int)(eof - start), &zcount); while (stop > start && !stop[-1]) { stop--; } } *pnal_unit_bytes = (int)(stop - start - zcount); return start; } int mp4_h26x_write_init(mp4_h26x_writer_t *h, MP4E_mux_t *mux, int width, int height, int is_hevc) { MP4E_track_t tr; tr.track_media_kind = e_video; tr.language[0] = 'u'; tr.language[1] = 'n'; tr.language[2] = 'd'; tr.language[3] = 0; tr.object_type_indication = is_hevc ? MP4_OBJECT_TYPE_HEVC : MP4_OBJECT_TYPE_AVC; tr.time_scale = 90000; tr.default_duration = 0; tr.u.v.width = width; tr.u.v.height = height; h->mux_track_id = MP4E_add_track(mux, &tr); h->mux = mux; h->is_hevc = is_hevc; h->need_vps = is_hevc; h->need_sps = 1; h->need_pps = 1; h->need_idr = 1; #if MINIMP4_TRANSCODE_SPS_ID memset(&h->sps_patcher, 0, sizeof(h264_sps_id_patcher_t)); #endif return MP4E_STATUS_OK; } void mp4_h26x_write_close(mp4_h26x_writer_t *h) { #if MINIMP4_TRANSCODE_SPS_ID h264_sps_id_patcher_t *p = &h->sps_patcher; int i; for (i = 0; i < MINIMP4_MAX_SPS; i++) { if (p->sps_cache[i]) free(p->sps_cache[i]); } for (i = 0; i < MINIMP4_MAX_PPS; i++) { if (p->pps_cache[i]) free(p->pps_cache[i]); } #endif memset(h, 0, sizeof(*h)); } static int mp4_h265_write_nal(mp4_h26x_writer_t *h, const unsigned char *nal, int sizeof_nal, unsigned timeStamp90kHz_next) { int payload_type = (nal[0] >> 1) & 0x3f; int is_intra = payload_type >= HEVC_NAL_BLA_W_LP && payload_type <= HEVC_NAL_CRA_NUT; int err = MP4E_STATUS_OK; //printf("payload_type=%d, intra=%d\n", payload_type, is_intra); if (is_intra && !h->need_sps && !h->need_pps && !h->need_vps) h->need_idr = 0; switch (payload_type) { case HEVC_NAL_VPS: MP4E_set_vps(h->mux, h->mux_track_id, nal, sizeof_nal); h->need_vps = 0; break; case HEVC_NAL_SPS: MP4E_set_sps(h->mux, h->mux_track_id, nal, sizeof_nal); h->need_sps = 0; break; case HEVC_NAL_PPS: MP4E_set_pps(h->mux, h->mux_track_id, nal, sizeof_nal); h->need_pps = 0; break; default: if (h->need_vps || h->need_sps || h->need_pps || h->need_idr) return MP4E_STATUS_BAD_ARGUMENTS; { unsigned char *tmp = (unsigned char *)malloc(4 + sizeof_nal); if (!tmp) return MP4E_STATUS_NO_MEMORY; int sample_kind = MP4E_SAMPLE_DEFAULT; tmp[0] = (unsigned char)(sizeof_nal >> 24); tmp[1] = (unsigned char)(sizeof_nal >> 16); tmp[2] = (unsigned char)(sizeof_nal >> 8); tmp[3] = (unsigned char)(sizeof_nal); memcpy(tmp + 4, nal, sizeof_nal); if (is_intra) sample_kind = MP4E_SAMPLE_RANDOM_ACCESS; err = MP4E_put_sample(h->mux, h->mux_track_id, tmp, 4 + sizeof_nal, timeStamp90kHz_next, sample_kind); free(tmp); } break; } return err; } int mp4_h26x_write_nal(mp4_h26x_writer_t *h, const unsigned char *nal, int length, unsigned timeStamp90kHz_next) { const unsigned char *eof = nal + length; int payload_type, sizeof_nal, err = MP4E_STATUS_OK; for (;;nal++) { #if MINIMP4_TRANSCODE_SPS_ID unsigned char *nal1, *nal2; #endif nal = find_nal_unit(nal, (int)(eof - nal), &sizeof_nal); if (!sizeof_nal) break; if (h->is_hevc) { ERR(mp4_h265_write_nal(h, nal, sizeof_nal, timeStamp90kHz_next)); continue; } payload_type = nal[0] & 31; if (9 == payload_type) continue; // access unit delimiter, nothing to be done #if MINIMP4_TRANSCODE_SPS_ID // Transcode SPS, PPS and slice headers, reassigning ID's for SPS and PPS: // - assign unique ID's to different SPS and PPS // - assign same ID's to equal (except ID) SPS and PPS // - save all different SPS and PPS nal1 = (unsigned char *)malloc(sizeof_nal*17/16 + 32); if (!nal1) return MP4E_STATUS_NO_MEMORY; nal2 = (unsigned char *)malloc(sizeof_nal*17/16 + 32); if (!nal2) { free(nal1); return MP4E_STATUS_NO_MEMORY; } sizeof_nal = remove_nal_escapes(nal2, nal, sizeof_nal); if (!sizeof_nal) { exit_with_free: free(nal1); free(nal2); return MP4E_STATUS_BAD_ARGUMENTS; } sizeof_nal = transcode_nalu(&h->sps_patcher, nal2, sizeof_nal, nal1); sizeof_nal = nal_put_esc(nal2, nal1, sizeof_nal); switch (payload_type) { case 7: MP4E_set_sps(h->mux, h->mux_track_id, nal2 + 4, sizeof_nal - 4); h->need_sps = 0; break; case 8: if (h->need_sps) goto exit_with_free; MP4E_set_pps(h->mux, h->mux_track_id, nal2 + 4, sizeof_nal - 4); h->need_pps = 0; break; case 5: if (h->need_sps) goto exit_with_free; h->need_idr = 0; // flow through /* FALLTHROUGH */ default: if (h->need_sps) goto exit_with_free; if (!h->need_pps && !h->need_idr) { bit_reader_t bs[1]; init_bits(bs, nal + 1, sizeof_nal - 4 - 1); unsigned first_mb_in_slice = ue_bits(bs); //unsigned slice_type = ue_bits(bs); int sample_kind = MP4E_SAMPLE_DEFAULT; nal2[0] = (unsigned char)((sizeof_nal - 4) >> 24); nal2[1] = (unsigned char)((sizeof_nal - 4) >> 16); nal2[2] = (unsigned char)((sizeof_nal - 4) >> 8); nal2[3] = (unsigned char)((sizeof_nal - 4)); if (first_mb_in_slice) sample_kind = MP4E_SAMPLE_CONTINUATION; else if (payload_type == 5) sample_kind = MP4E_SAMPLE_RANDOM_ACCESS; err = MP4E_put_sample(h->mux, h->mux_track_id, nal2, sizeof_nal, timeStamp90kHz_next, sample_kind); } break; } free(nal1); free(nal2); #else // No SPS/PPS transcoding // This branch assumes that encoder use correct SPS/PPS ID's switch (payload_type) { case 7: MP4E_set_sps(h->mux, h->mux_track_id, nal, sizeof_nal); h->need_sps = 0; break; case 8: MP4E_set_pps(h->mux, h->mux_track_id, nal, sizeof_nal); h->need_pps = 0; break; case 5: if (h->need_sps) return MP4E_STATUS_BAD_ARGUMENTS; h->need_idr = 0; // flow through default: if (h->need_sps) return MP4E_STATUS_BAD_ARGUMENTS; if (!h->need_pps && !h->need_idr) { bit_reader_t bs[1]; unsigned char *tmp = (unsigned char *)malloc(4 + sizeof_nal); if (!tmp) return MP4E_STATUS_NO_MEMORY; init_bits(bs, nal + 1, sizeof_nal - 1); unsigned first_mb_in_slice = ue_bits(bs); int sample_kind = MP4E_SAMPLE_DEFAULT; tmp[0] = (unsigned char)(sizeof_nal >> 24); tmp[1] = (unsigned char)(sizeof_nal >> 16); tmp[2] = (unsigned char)(sizeof_nal >> 8); tmp[3] = (unsigned char)(sizeof_nal); memcpy(tmp + 4, nal, sizeof_nal); if (first_mb_in_slice) sample_kind = MP4E_SAMPLE_CONTINUATION; else if (payload_type == 5) sample_kind = MP4E_SAMPLE_RANDOM_ACCESS; err = MP4E_put_sample(h->mux, h->mux_track_id, tmp, 4 + sizeof_nal, timeStamp90kHz_next, sample_kind); free(tmp); } break; } #endif if (err) break; } return err; } #if MP4D_TRACE_SUPPORTED # define TRACE(x) printf x #else # define TRACE(x) #endif #define NELEM(x) (sizeof(x) / sizeof((x)[0])) static int minimp4_fgets(MP4D_demux_t *mp4) { uint8_t c; if (mp4->read_callback(mp4->read_pos, &c, 1, mp4->token)) return -1; mp4->read_pos++; return c; } /** * Read given number of bytes from input stream * Used to read box headers */ static unsigned minimp4_read(MP4D_demux_t *mp4, int nb, int *eof_flag) { uint32_t v = 0; int last_byte; switch (nb) { case 4: v = (v << 8) | minimp4_fgets(mp4); /* FALLTHROUGH */ case 3: v = (v << 8) | minimp4_fgets(mp4); /* FALLTHROUGH */ case 2: v = (v << 8) | minimp4_fgets(mp4); /* FALLTHROUGH */ default: case 1: v = (v << 8) | (last_byte = minimp4_fgets(mp4)); } if (last_byte < 0) { *eof_flag = 1; } return v; } /** * Read given number of bytes, but no more than *payload_bytes specifies... * Used to read box payload */ static uint32_t read_payload(MP4D_demux_t *mp4, unsigned nb, boxsize_t *payload_bytes, int *eof_flag) { if (*payload_bytes < nb) { *eof_flag = 1; nb = (int)*payload_bytes; } *payload_bytes -= nb; return minimp4_read(mp4, nb, eof_flag); } /** * Skips given number of bytes. * Avoid math operations with fpos_t */ static void my_fseek(MP4D_demux_t *mp4, boxsize_t pos, int *eof_flag) { mp4->read_pos += pos; if (mp4->read_pos >= mp4->read_size) *eof_flag = 1; } #define READ(n) read_payload(mp4, n, &payload_bytes, &eof_flag) #define SKIP(n) { boxsize_t t = MINIMP4_MIN(payload_bytes, n); my_fseek(mp4, t, &eof_flag); payload_bytes -= t; } #define MALLOC(t, p, size) p = (t)malloc(size); if (!(p)) { ERROR("out of memory"); } /* * On error: release resources. */ #define RETURN_ERROR(mess) { \ TRACE(("\nMP4 ERROR: " mess)); \ MP4D_close(mp4); \ return 0; \ } /* * Any errors, occurred on top-level hierarchy is passed to exit check: 'if (!mp4->track_count) ... ' */ #define ERROR(mess) \ if (!depth) \ break; \ else \ RETURN_ERROR(mess); typedef enum { BOX_ATOM, BOX_OD } boxtype_t; int MP4D_open(MP4D_demux_t *mp4, int (*read_callback)(int64_t offset, void *buffer, size_t size, void *token), void *token, int64_t file_size) { // box stack size int depth = 0; struct { // remaining bytes for box in the stack boxsize_t bytes; // kind of box children's: OD chunks handled in the same manner as name chunks boxtype_t format; } stack[MAX_CHUNKS_DEPTH]; #if MP4D_TRACE_SUPPORTED // path of current element: List0/List1/... etc uint32_t box_path[MAX_CHUNKS_DEPTH]; #endif int eof_flag = 0; unsigned i; MP4D_track_t *tr = NULL; if (!mp4 || !read_callback) { TRACE(("\nERROR: invlaid arguments!")); return 0; } memset(mp4, 0, sizeof(MP4D_demux_t)); mp4->read_callback = read_callback; mp4->token = token; mp4->read_size = file_size; stack[0].format = BOX_ATOM; // start with atom box stack[0].bytes = 0; // never accessed do { // List of boxes, derived from 'FullBox' // ~~~~~~~~~~~~~~~~~~~~~ // need read version field and check version for these boxes static const struct { uint32_t name; unsigned max_version; unsigned use_track_flag; } g_fullbox[] = { #if MP4D_INFO_SUPPORTED {BOX_mdhd, 1, 1}, {BOX_mvhd, 1, 0}, {BOX_hdlr, 0, 0}, {BOX_meta, 0, 0}, // Android can produce meta box without 'FullBox' field, comment this line to simulate the bug #endif #if MP4D_TRACE_TIMESTAMPS {BOX_stts, 0, 0}, {BOX_ctts, 0, 0}, #endif {BOX_stz2, 0, 1}, {BOX_stsz, 0, 1}, {BOX_stsc, 0, 1}, {BOX_stco, 0, 1}, {BOX_co64, 0, 1}, {BOX_stsd, 0, 0}, {BOX_esds, 0, 1} // esds does not use track, but switches to OD mode. Check here, to avoid OD check }; // List of boxes, which contains other boxes ('envelopes') // Parser will descend down for boxes in this list, otherwise parsing will proceed to // the next sibling box // OD boxes handled in the same way as atom boxes... static const struct { uint32_t name; boxtype_t type; } g_envelope_box[] = { {BOX_esds, BOX_OD}, // TODO: BOX_esds can be used for both audio and video, but this code supports audio only! {OD_ESD, BOX_OD}, {OD_DCD, BOX_OD}, {OD_DSI, BOX_OD}, {BOX_trak, BOX_ATOM}, {BOX_moov, BOX_ATOM}, //{BOX_moof, BOX_ATOM}, {BOX_mdia, BOX_ATOM}, {BOX_tref, BOX_ATOM}, {BOX_minf, BOX_ATOM}, {BOX_dinf, BOX_ATOM}, {BOX_stbl, BOX_ATOM}, {BOX_stsd, BOX_ATOM}, {BOX_mp4a, BOX_ATOM}, {BOX_mp4s, BOX_ATOM}, #if MP4D_AVC_SUPPORTED {BOX_mp4v, BOX_ATOM}, {BOX_avc1, BOX_ATOM}, //{BOX_avc2, BOX_ATOM}, //{BOX_svc1, BOX_ATOM}, #endif #if MP4D_HEVC_SUPPORTED {BOX_hvc1, BOX_ATOM}, #endif {BOX_udta, BOX_ATOM}, {BOX_meta, BOX_ATOM}, {BOX_ilst, BOX_ATOM} }; uint32_t FullAtomVersionAndFlags = 0; boxsize_t payload_bytes; boxsize_t box_bytes; uint32_t box_name; #if MP4D_INFO_SUPPORTED unsigned char **ptag = NULL; #endif int read_bytes = 0; // Read header box type and it's length if (stack[depth].format == BOX_ATOM) { box_bytes = minimp4_read(mp4, 4, &eof_flag); #if FIX_BAD_ANDROID_META_BOX broken_android_meta_hack: #endif if (eof_flag) break; // normal exit if (box_bytes >= 2 && box_bytes < 8) { ERROR("invalid box size (broken file?)"); } box_name = minimp4_read(mp4, 4, &eof_flag); read_bytes = 8; // Decode box size if (box_bytes == 0 || // standard indication of 'till eof' size box_bytes == (boxsize_t)0xFFFFFFFFU // some files uses non-standard 'till eof' signaling ) { box_bytes = ~(boxsize_t)0; } payload_bytes = box_bytes - 8; if (box_bytes == 1) // 64-bit sizes { TRACE(("\n64-bit chunk encountered")); box_bytes = minimp4_read(mp4, 4, &eof_flag); #if MP4D_64BIT_SUPPORTED box_bytes <<= 32; box_bytes |= minimp4_read(mp4, 4, &eof_flag); #else if (box_bytes) { ERROR("UNSUPPORTED FEATURE: MP4BoxHeader(): 64-bit boxes not supported!"); } box_bytes = minimp4_read(mp4, 4, &eof_flag); #endif if (box_bytes < 16) { ERROR("invalid box size (broken file?)"); } payload_bytes = box_bytes - 16; } // Read and check box version for some boxes for (i = 0; i < NELEM(g_fullbox); i++) { if (box_name == g_fullbox[i].name) { FullAtomVersionAndFlags = READ(4); read_bytes += 4; #if FIX_BAD_ANDROID_META_BOX // Fix invalid BOX_meta, found in some Android-produced MP4 // This branch is optional: bad box would be skipped if (box_name == BOX_meta) { if (FullAtomVersionAndFlags >= 8 && FullAtomVersionAndFlags < payload_bytes) { if (box_bytes > stack[depth].bytes) { ERROR("broken file structure!"); } stack[depth].bytes -= box_bytes;; depth++; stack[depth].bytes = payload_bytes + 4; // +4 need for missing header stack[depth].format = BOX_ATOM; box_bytes = FullAtomVersionAndFlags; TRACE(("Bad metadata box detected (Android bug?)!\n")); goto broken_android_meta_hack; } } #endif // FIX_BAD_ANDROID_META_BOX if ((FullAtomVersionAndFlags >> 24) > g_fullbox[i].max_version) { ERROR("unsupported box version!"); } if (g_fullbox[i].use_track_flag && !tr) { ERROR("broken file structure!"); } } } } else // stack[depth].format == BOX_OD { int val; box_name = OD_BASE + minimp4_read(mp4, 1, &eof_flag); // 1-byte box type read_bytes += 1; if (eof_flag) break; payload_bytes = 0; box_bytes = 1; do { val = minimp4_read(mp4, 1, &eof_flag); read_bytes += 1; if (eof_flag) { ERROR("premature EOF!"); } payload_bytes = (payload_bytes << 7) | (val & 0x7F); box_bytes++; } while (val & 0x80); box_bytes += payload_bytes; } #if MP4D_TRACE_SUPPORTED box_path[depth] = (box_name >> 24) | (box_name << 24) | ((box_name >> 8) & 0x0000FF00) | ((box_name << 8) & 0x00FF0000); TRACE(("%2d %8d %.*s (%d bytes remains for sibilings) \n", depth, (int)box_bytes, depth*4, (char*)box_path, (int)stack[depth].bytes)); #endif // Check that box size <= parent size if (depth) { // Skip box with bad size assert(box_bytes > 0); if (box_bytes > stack[depth].bytes) { TRACE(("Wrong %c%c%c%c box size: broken file?\n", (box_name >> 24)&255, (box_name >> 16)&255, (box_name >> 8)&255, box_name&255)); box_bytes = stack[depth].bytes; box_name = 0; payload_bytes = box_bytes - read_bytes; } stack[depth].bytes -= box_bytes; } // Read box header switch(box_name) { case BOX_stz2: //ISO/IEC 14496-1 Page 38. Section 8.17.2 - Sample Size Box. case BOX_stsz: { int size = 0; uint32_t sample_size = READ(4); tr->sample_count = READ(4); MALLOC(unsigned int*, tr->entry_size, tr->sample_count*4); for (i = 0; i < tr->sample_count; i++) { if (box_name == BOX_stsz) { tr->entry_size[i] = (sample_size?sample_size:READ(4)); } else { switch (sample_size & 0xFF) { case 16: tr->entry_size[i] = READ(2); break; case 8: tr->entry_size[i] = READ(1); break; case 4: if (i & 1) { tr->entry_size[i] = size & 15; } else { size = READ(1); tr->entry_size[i] = (size >> 4); } break; } } } } break; case BOX_stsc: //ISO/IEC 14496-12 Page 38. Section 8.18 - Sample To Chunk Box. tr->sample_to_chunk_count = READ(4); MALLOC(MP4D_sample_to_chunk_t*, tr->sample_to_chunk, tr->sample_to_chunk_count*sizeof(tr->sample_to_chunk[0])); for (i = 0; i < tr->sample_to_chunk_count; i++) { tr->sample_to_chunk[i].first_chunk = READ(4); tr->sample_to_chunk[i].samples_per_chunk = READ(4); SKIP(4); // sample_description_index } break; #if MP4D_TRACE_TIMESTAMPS || MP4D_TIMESTAMPS_SUPPORTED case BOX_stts: { unsigned count = READ(4); unsigned j, k = 0, ts = 0, ts_count = count; #if MP4D_TIMESTAMPS_SUPPORTED MALLOC(unsigned int*, tr->timestamp, ts_count*4); MALLOC(unsigned int*, tr->duration, ts_count*4); #endif for (i = 0; i < count; i++) { unsigned sc = READ(4); int d = READ(4); TRACE(("sample %8d count %8d duration %8d\n", i, sc, d)); #if MP4D_TIMESTAMPS_SUPPORTED if (k + sc > ts_count) { ts_count = k + sc; tr->timestamp = (unsigned int*)realloc(tr->timestamp, ts_count * sizeof(unsigned)); tr->duration = (unsigned int*)realloc(tr->duration, ts_count * sizeof(unsigned)); } for (j = 0; j < sc; j++) { tr->duration[k] = d; tr->timestamp[k++] = ts; ts += d; } #endif } } break; case BOX_ctts: { unsigned count = READ(4); for (i = 0; i < count; i++) { int sc = READ(4); int d = READ(4); (void)sc; (void)d; TRACE(("sample %8d count %8d decoding to composition offset %8d\n", i, sc, d)); } } break; #endif case BOX_stco: //ISO/IEC 14496-12 Page 39. Section 8.19 - Chunk Offset Box. case BOX_co64: tr->chunk_count = READ(4); MALLOC(MP4D_file_offset_t*, tr->chunk_offset, tr->chunk_count*sizeof(MP4D_file_offset_t)); for (i = 0; i < tr->chunk_count; i++) { tr->chunk_offset[i] = READ(4); if (box_name == BOX_co64) { #if !MP4D_64BIT_SUPPORTED if (tr->chunk_offset[i]) { ERROR("UNSUPPORTED FEATURE: 64-bit chunk_offset not supported!"); } #endif tr->chunk_offset[i] <<= 32; tr->chunk_offset[i] |= READ(4); } } break; #if MP4D_INFO_SUPPORTED case BOX_mvhd: SKIP(((FullAtomVersionAndFlags >> 24) == 1) ? 8 + 8 : 4 + 4); mp4->timescale = READ(4); mp4->duration_hi = ((FullAtomVersionAndFlags >> 24) == 1) ? READ(4) : 0; mp4->duration_lo = READ(4); SKIP(4 + 2 + 2 + 4*2 + 4*9 + 4*6 + 4); break; case BOX_mdhd: SKIP(((FullAtomVersionAndFlags >> 24) == 1) ? 8 + 8 : 4 + 4); tr->timescale = READ(4); tr->duration_hi = ((FullAtomVersionAndFlags >> 24) == 1) ? READ(4) : 0; tr->duration_lo = READ(4); { int ISO_639_2_T = READ(2); tr->language[2] = (ISO_639_2_T & 31) + 0x60; ISO_639_2_T >>= 5; tr->language[1] = (ISO_639_2_T & 31) + 0x60; ISO_639_2_T >>= 5; tr->language[0] = (ISO_639_2_T & 31) + 0x60; } // the rest of this box is skipped by default ... break; case BOX_hdlr: if (tr) // When this box is within 'meta' box, the track may not be avaialable { SKIP(4); // pre_defined tr->handler_type = READ(4); } // typically hdlr box does not contain any useful info. // the rest of this box is skipped by default ... break; case BOX_btrt: if (!tr) { ERROR("broken file structure!"); } SKIP(4 + 4); tr->avg_bitrate_bps = READ(4); break; // Set pointer to tag to be read... case BOX_calb: ptag = &mp4->tag.album; break; case BOX_cART: ptag = &mp4->tag.artist; break; case BOX_cnam: ptag = &mp4->tag.title; break; case BOX_cday: ptag = &mp4->tag.year; break; case BOX_ccmt: ptag = &mp4->tag.comment; break; case BOX_cgen: ptag = &mp4->tag.genre; break; #endif case BOX_stsd: SKIP(4); // entry_count, BOX_mp4a & BOX_mp4v boxes follows immediately break; case BOX_mp4s: // private stream if (!tr) { ERROR("broken file structure!"); } SKIP(6*1 + 2/*Base SampleEntry*/); break; case BOX_mp4a: if (!tr) { ERROR("broken file structure!"); } #if MP4D_INFO_SUPPORTED SKIP(6*1+2/*Base SampleEntry*/ + 4*2); tr->SampleDescription.audio.channelcount = READ(2); SKIP(2/*samplesize*/ + 2 + 2); tr->SampleDescription.audio.samplerate_hz = READ(4) >> 16; #else SKIP(28); #endif break; #if MP4D_AVC_SUPPORTED case BOX_avc1: // AVCSampleEntry extends VisualSampleEntry // case BOX_avc2: - no test // case BOX_svc1: - no test case BOX_mp4v: if (!tr) { ERROR("broken file structure!"); } #if MP4D_INFO_SUPPORTED SKIP(6*1 + 2/*Base SampleEntry*/ + 2 + 2 + 4*3); tr->SampleDescription.video.width = READ(2); tr->SampleDescription.video.height = READ(2); // frame_count is always 1 // compressorname is rarely set.. SKIP(4 + 4 + 4 + 2/*frame_count*/ + 32/*compressorname*/ + 2 + 2); #else SKIP(78); #endif // ^^^ end of VisualSampleEntry // now follows for BOX_avc1: // BOX_avcC // BOX_btrt (optional) // BOX_m4ds (optional) // for BOX_mp4v: // BOX_esds break; case BOX_avcC: // AVCDecoderConfigurationRecord() // hack: AAC-specific DSI field reused (for it have same purpoose as sps/pps) // TODO: check this hack if BOX_esds co-exist with BOX_avcC tr->object_type_indication = MP4_OBJECT_TYPE_AVC; tr->dsi = (unsigned char*)malloc((size_t)box_bytes); tr->dsi_bytes = (unsigned)box_bytes; { int spspps; unsigned char *p = tr->dsi; unsigned int configurationVersion = READ(1); unsigned int AVCProfileIndication = READ(1); unsigned int profile_compatibility = READ(1); unsigned int AVCLevelIndication = READ(1); //bit(6) reserved = unsigned int lengthSizeMinusOne = READ(1) & 3; (void)configurationVersion; (void)AVCProfileIndication; (void)profile_compatibility; (void)AVCLevelIndication; (void)lengthSizeMinusOne; for (spspps = 0; spspps < 2; spspps++) { unsigned int numOfSequenceParameterSets= READ(1); if (!spspps) { numOfSequenceParameterSets &= 31; // clears 3 msb for SPS } *p++ = numOfSequenceParameterSets; for (i = 0; i < numOfSequenceParameterSets; i++) { unsigned k, sequenceParameterSetLength = READ(2); *p++ = sequenceParameterSetLength >> 8; *p++ = sequenceParameterSetLength ; for (k = 0; k < sequenceParameterSetLength; k++) { *p++ = READ(1); } } } } break; #endif // MP4D_AVC_SUPPORTED case OD_ESD: { unsigned flags = READ(3); // ES_ID(2) + flags(1) if (flags & 0x80) // steamdependflag { SKIP(2); // dependsOnESID } if (flags & 0x40) // urlflag { unsigned bytecount = READ(1); SKIP(bytecount); // skip URL } if (flags & 0x20) // ocrflag (was reserved in MPEG-4 v.1) { SKIP(2); // OCRESID } break; } case OD_DCD: //ISO/IEC 14496-1 Page 28. Section 8.6.5 - DecoderConfigDescriptor. assert(tr); // ensured by g_fullbox[] check tr->object_type_indication = READ(1); #if MP4D_INFO_SUPPORTED tr->stream_type = READ(1) >> 2; SKIP(3/*bufferSizeDB*/ + 4/*maxBitrate*/); tr->avg_bitrate_bps = READ(4); #else SKIP(1+3+4+4); #endif break; case OD_DSI: //ISO/IEC 14496-1 Page 28. Section 8.6.5 - DecoderConfigDescriptor. assert(tr); // ensured by g_fullbox[] check if (!tr->dsi && payload_bytes) { MALLOC(unsigned char*, tr->dsi, (int)payload_bytes); for (i = 0; i < payload_bytes; i++) { tr->dsi[i] = minimp4_read(mp4, 1, &eof_flag); // These bytes available due to check above } tr->dsi_bytes = i; payload_bytes -= i; break; } default: TRACE(("[%c%c%c%c] %d\n", box_name >> 24, box_name >> 16, box_name >> 8, box_name, (int)payload_bytes)); } #if MP4D_INFO_SUPPORTED // Read tag is tag pointer is set if (ptag && !*ptag && payload_bytes > 16) { #if 0 uint32_t size = READ(4); uint32_t data = READ(4); uint32_t class = READ(4); uint32_t x1 = READ(4); TRACE(("%2d %2d %2d ", size, class, x1)); #else SKIP(4 + 4 + 4 + 4); #endif MALLOC(unsigned char*, *ptag, (unsigned)payload_bytes + 1); for (i = 0; payload_bytes != 0; i++) { (*ptag)[i] = READ(1); } (*ptag)[i] = 0; // zero-terminated string } #endif if (box_name == BOX_trak) { // New track found: allocate memory using realloc() // Typically there are 1 audio track for AAC audio file, // 4 tracks for movie file, // 3-5 tracks for scalable audio (CELP+AAC) // and up to 50 tracks for BSAC scalable audio void *mem = realloc(mp4->track, (mp4->track_count + 1)*sizeof(MP4D_track_t)); if (!mem) { // if realloc fails, it does not deallocate old pointer! ERROR("out of memory"); } mp4->track = (MP4D_track_t*)mem; tr = mp4->track + mp4->track_count++; memset(tr, 0, sizeof(MP4D_track_t)); } else if (box_name == BOX_meta) { tr = NULL; // Avoid update of 'hdlr' box, which may contains in the 'meta' box } // If this box is envelope, save it's size in box stack for (i = 0; i < NELEM(g_envelope_box); i++) { if (box_name == g_envelope_box[i].name) { if (++depth >= MAX_CHUNKS_DEPTH) { ERROR("too deep atoms nesting!"); } stack[depth].bytes = payload_bytes; stack[depth].format = g_envelope_box[i].type; break; } } // if box is not envelope, just skip it if (i == NELEM(g_envelope_box)) { if (payload_bytes > (boxsize_t)file_size) { eof_flag = 1; } else { SKIP(payload_bytes); } } // remove empty boxes from stack // don't touch box with index 0 (which indicates whole file) while (depth > 0 && !stack[depth].bytes) { depth--; } } while(!eof_flag); if (!mp4->track_count) { RETURN_ERROR("no tracks found"); } return 1; } /** * Find chunk, containing given sample. * Returns chunk number, and first sample in this chunk. */ static int sample_to_chunk(MP4D_track_t *tr, unsigned nsample, unsigned *nfirst_sample_in_chunk) { unsigned chunk_group = 0, nc; unsigned sum = 0; *nfirst_sample_in_chunk = 0; if (tr->chunk_count <= 1) { return 0; } for (nc = 0; nc < tr->chunk_count; nc++) { if (chunk_group + 1 < tr->sample_to_chunk_count // stuck at last entry till EOF && nc + 1 == // Chunks counted starting with '1' tr->sample_to_chunk[chunk_group + 1].first_chunk) // next group? { chunk_group++; } sum += tr->sample_to_chunk[chunk_group].samples_per_chunk; if (nsample < sum) return nc; // TODO: this can be calculated once per file *nfirst_sample_in_chunk = sum; } return -1; } // Exported API function MP4D_file_offset_t MP4D_frame_offset(const MP4D_demux_t *mp4, unsigned ntrack, unsigned nsample, unsigned *frame_bytes, unsigned *timestamp, unsigned *duration) { MP4D_track_t *tr = mp4->track + ntrack; unsigned ns; int nchunk = sample_to_chunk(tr, nsample, &ns); MP4D_file_offset_t offset; if (nchunk < 0) { *frame_bytes = 0; return 0; } offset = tr->chunk_offset[nchunk]; for (; ns < nsample; ns++) { offset += tr->entry_size[ns]; } *frame_bytes = tr->entry_size[ns]; if (timestamp) { #if MP4D_TIMESTAMPS_SUPPORTED *timestamp = tr->timestamp[ns]; #else *timestamp = 0; #endif } if (duration) { #if MP4D_TIMESTAMPS_SUPPORTED *duration = tr->duration[ns]; #else *duration = 0; #endif } return offset; } #define FREE(x) if (x) {free(x); x = NULL;} // Exported API function void MP4D_close(MP4D_demux_t *mp4) { while (mp4->track_count) { MP4D_track_t *tr = mp4->track + --mp4->track_count; FREE(tr->entry_size); #if MP4D_TIMESTAMPS_SUPPORTED FREE(tr->timestamp); FREE(tr->duration); #endif FREE(tr->sample_to_chunk); FREE(tr->chunk_offset); FREE(tr->dsi); } FREE(mp4->track); #if MP4D_INFO_SUPPORTED FREE(mp4->tag.title); FREE(mp4->tag.artist); FREE(mp4->tag.album); FREE(mp4->tag.year); FREE(mp4->tag.comment); FREE(mp4->tag.genre); #endif } static int skip_spspps(const unsigned char *p, int nbytes, int nskip) { int i, k = 0; for (i = 0; i < nskip; i++) { unsigned segmbytes; if (k > nbytes - 2) return -1; segmbytes = p[k]*256 + p[k+1]; k += 2 + segmbytes; } return k; } static const void *MP4D_read_spspps(const MP4D_demux_t *mp4, unsigned int ntrack, int pps_flag, int nsps, int *sps_bytes) { int sps_count, skip_bytes; int bytepos = 0; unsigned char *p = mp4->track[ntrack].dsi; if (ntrack >= mp4->track_count) return NULL; if (mp4->track[ntrack].object_type_indication != MP4_OBJECT_TYPE_AVC) return NULL; // SPS/PPS are specific for AVC format only if (pps_flag) { // Skip all SPS sps_count = p[bytepos++]; skip_bytes = skip_spspps(p+bytepos, mp4->track[ntrack].dsi_bytes - bytepos, sps_count); if (skip_bytes < 0) return NULL; bytepos += skip_bytes; } // Skip sps/pps before the given target sps_count = p[bytepos++]; if (nsps >= sps_count) return NULL; skip_bytes = skip_spspps(p+bytepos, mp4->track[ntrack].dsi_bytes - bytepos, nsps); if (skip_bytes < 0) return NULL; bytepos += skip_bytes; *sps_bytes = p[bytepos]*256 + p[bytepos+1]; return p + bytepos + 2; } const void *MP4D_read_sps(const MP4D_demux_t *mp4, unsigned int ntrack, int nsps, int *sps_bytes) { return MP4D_read_spspps(mp4, ntrack, 0, nsps, sps_bytes); } const void *MP4D_read_pps(const MP4D_demux_t *mp4, unsigned int ntrack, int npps, int *pps_bytes) { return MP4D_read_spspps(mp4, ntrack, 1, npps, pps_bytes); } #if MP4D_PRINT_INFO_SUPPORTED /************************************************************************/ /* Purely informational part, may be removed for embedded applications */ /************************************************************************/ // // Decodes ISO/IEC 14496 MP4 stream type to ASCII string // static const char *GetMP4StreamTypeName(int streamType) { switch (streamType) { case 0x00: return "Forbidden"; case 0x01: return "ObjectDescriptorStream"; case 0x02: return "ClockReferenceStream"; case 0x03: return "SceneDescriptionStream"; case 0x04: return "VisualStream"; case 0x05: return "AudioStream"; case 0x06: return "MPEG7Stream"; case 0x07: return "IPMPStream"; case 0x08: return "ObjectContentInfoStream"; case 0x09: return "MPEGJStream"; default: if (streamType >= 0x20 && streamType <= 0x3F) { return "User private"; } else { return "Reserved for ISO use"; } } } // // Decodes ISO/IEC 14496 MP4 object type to ASCII string // static const char *GetMP4ObjectTypeName(int objectTypeIndication) { switch (objectTypeIndication) { case 0x00: return "Forbidden"; case 0x01: return "Systems ISO/IEC 14496-1"; case 0x02: return "Systems ISO/IEC 14496-1"; case 0x20: return "Visual ISO/IEC 14496-2"; case 0x40: return "Audio ISO/IEC 14496-3"; case 0x60: return "Visual ISO/IEC 13818-2 Simple Profile"; case 0x61: return "Visual ISO/IEC 13818-2 Main Profile"; case 0x62: return "Visual ISO/IEC 13818-2 SNR Profile"; case 0x63: return "Visual ISO/IEC 13818-2 Spatial Profile"; case 0x64: return "Visual ISO/IEC 13818-2 High Profile"; case 0x65: return "Visual ISO/IEC 13818-2 422 Profile"; case 0x66: return "Audio ISO/IEC 13818-7 Main Profile"; case 0x67: return "Audio ISO/IEC 13818-7 LC Profile"; case 0x68: return "Audio ISO/IEC 13818-7 SSR Profile"; case 0x69: return "Audio ISO/IEC 13818-3"; case 0x6A: return "Visual ISO/IEC 11172-2"; case 0x6B: return "Audio ISO/IEC 11172-3"; case 0x6C: return "Visual ISO/IEC 10918-1"; case 0xFF: return "no object type specified"; default: if (objectTypeIndication >= 0xC0 && objectTypeIndication <= 0xFE) return "User private"; else return "Reserved for ISO use"; } } /** * Print MP4 information to stdout. * Subject for customization to particular application Output Example #1: movie file MP4 FILE: 7 tracks found. Movie time 104.12 sec No|type|lng| duration | bitrate| Stream type | Object type 0|odsm|fre| 0.00 s 1 frm| 0| Forbidden | Forbidden 1|sdsm|fre| 0.00 s 1 frm| 0| Forbidden | Forbidden 2|vide|```| 104.12 s 2603 frm| 1960559| VisualStream | Visual ISO/IEC 14496-2 - 720x304 3|soun|ger| 104.06 s 2439 frm| 191242| AudioStream | Audio ISO/IEC 14496-3 - 6 ch 24000 hz 4|soun|eng| 104.06 s 2439 frm| 194171| AudioStream | Audio ISO/IEC 14496-3 - 6 ch 24000 hz 5|subp|ger| 71.08 s 25 frm| 0| Forbidden | Forbidden 6|subp|eng| 71.08 s 25 frm| 0| Forbidden | Forbidden Output Example #2: audio file with tags MP4 FILE: 1 tracks found. Movie time 92.42 sec title = 86-Second Blowout artist = Yo La Tengo album = May I Sing With Me year = 1992 No|type|lng| duration | bitrate| Stream type | Object type 0|mdir|und| 92.42 s 3980 frm| 128000| AudioStream | Audio ISO/IEC 14496-3MP4 FILE: 1 tracks found. Movie time 92.42 sec */ void MP4D_printf_info(const MP4D_demux_t *mp4) { unsigned i; printf("\nMP4 FILE: %d tracks found. Movie time %.2f sec\n", mp4->track_count, (4294967296.0*mp4->duration_hi + mp4->duration_lo) / mp4->timescale); #define STR_TAG(name) if (mp4->tag.name) printf("%10s = %s\n", #name, mp4->tag.name) STR_TAG(title); STR_TAG(artist); STR_TAG(album); STR_TAG(year); STR_TAG(comment); STR_TAG(genre); printf("\nNo|type|lng| duration | bitrate| %-23s| Object type", "Stream type"); for (i = 0; i < mp4->track_count; i++) { MP4D_track_t *tr = mp4->track + i; printf("\n%2d|%c%c%c%c|%c%c%c|%7.2f s %6d frm| %7d|", i, (tr->handler_type >> 24), (tr->handler_type >> 16), (tr->handler_type >> 8), (tr->handler_type >> 0), tr->language[0], tr->language[1], tr->language[2], (65536.0*65536.0*tr->duration_hi + tr->duration_lo) / tr->timescale, tr->sample_count, tr->avg_bitrate_bps); printf(" %-23s|", GetMP4StreamTypeName(tr->stream_type)); printf(" %-23s", GetMP4ObjectTypeName(tr->object_type_indication)); if (tr->handler_type == MP4D_HANDLER_TYPE_SOUN) { printf(" - %d ch %d hz", tr->SampleDescription.audio.channelcount, tr->SampleDescription.audio.samplerate_hz); } else if (tr->handler_type == MP4D_HANDLER_TYPE_VIDE) { printf(" - %dx%d", tr->SampleDescription.video.width, tr->SampleDescription.video.height); } } printf("\n"); } #endif // MP4D_PRINT_INFO_SUPPORTED #endif kew-3.2.0/src/000077500000000000000000000000001500206121000130545ustar00rootroot00000000000000kew-3.2.0/src/appstate.h000066400000000000000000000211271500206121000150510ustar00rootroot00000000000000#ifndef APPSTATE_H #define APPSTATE_H #include "cache.h" #include #include #include #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef PIXELDATA_STRUCT #define PIXELDATA_STRUCT typedef struct { unsigned char r; unsigned char g; unsigned char b; } PixelData; #endif typedef enum { TRACK_VIEW, KEYBINDINGS_VIEW, PLAYLIST_VIEW, LIBRARY_VIEW, SEARCH_VIEW, RADIOSEARCH_VIEW } ViewState; typedef struct { int mainColor; // Main terminal color, when using config colors int titleColor; // Color of the title, when using config colors int artistColor; // Artist color, when using config colors int enqueuedColor; // Color of enqueued files, when using config colors bool mouseEnabled; // Accept mouse input or not int mouseLeftClickAction; // Left mouse action int mouseMiddleClickAction; // Middle mouse action int mouseRightClickAction; // Right mouse action int mouseScrollUpAction; // Mouse scroll up action int mouseScrollDownAction; // Mouse scroll down action int mouseAltScrollUpAction; // Mouse scroll up + alt action int mouseAltScrollDownAction; // Mouse scroll down + alt action PixelData color; // The current color, when using album derived colors bool useConfigColors; // Use colors stored in config file or use an album derived color bool coverEnabled; // Show covers or not bool uiEnabled; // Show ui or not bool coverAnsi; // Show chafa cover (picture perfect in the right terminal), or ascii/ansi typ cover bool visualizerEnabled; // Show spectrum visualizer bool hideLogo; // No kew text at top bool hideHelp; // No help text at top bool allowNotifications; // Send desktop notifications or not int visualizerHeight; // Height in characters of the spectrum visualizer int visualizerColorType; // How colors are laid out in the spectrum visualizer bool visualizerBrailleMode; // Display the visualizer using braille characteres int titleDelay; // Delay when drawing title in track view int cacheLibrary; // Cache the library or not bool quitAfterStopping; // Exit kew when the music stops or not bool hideGlimmeringText; // Glimmering text on the bottom row time_t lastTimeAppRan; // When did this app run last, used for updating the cached library if it has been modified since that time float tweenFactor; // How fast the bars in the visualizer rise (higher value = faster) float tweenFactorFall; // How fast the bars in the visualizer fall (higher value = faster) int progressBarType; // What method for showing the progress in track view. Default 0. } UISettings; typedef struct { int chosenNodeId; // The id of the tree node that is chosen in library view bool allowChooseSongs; // In library view, has the user entered a folder that contains songs bool openedSubDir; // Opening a directory in an open directory. int numSongsAboveSubDir; // How many rows do we need to jump up if we close the parent directory and open one within int numDirectoryTreeEntries; // The number of entries in directory tree in library view int numProgressBars; // The number of progress dots at the bottom of track view volatile sig_atomic_t resizeFlag; // Is the user resizing the terminal window bool resetPlaylistDisplay; // Should the playlist be reset, ie drawn starting from playing song bool doNotifyMPRISSwitched; // Emit mpris song switched signal bool doNotifyMPRISPlaying; // Emit mpris music is playing signal bool collapseView; // Signal that ui needs to collapse the view bool miniMode; } UIState; typedef struct { Cache *tmpCache; // Cache for temporary files ViewState currentView; // The current view (playlist, library, track) that kew is on UIState uiState; UISettings uiSettings; } AppState; static const unsigned char defaultColor = 150; #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 useConfigColors[2]; char visualizerEnabled[2]; char visualizerHeight[6]; char visualizerColorType[2]; char titleDelay[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 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 hardShowPlaylist[6]; char hardShowPlaylistAlt[6]; char showPlaylistAlt[6]; char hardShowKeys[6]; char hardShowKeysAlt[6]; char showKeysAlt[6]; char hardEndOfPlaylist[6]; char hardShowLibrary[6]; char hardShowLibraryAlt[6]; char showLibraryAlt[6]; char hardShowSearch[6]; char hardShowSearchAlt[6]; char hardShowRadioSearch[6]; char hardShowRadioSearchAlt[6]; char showSearchAlt[6]; char showRadioSearchAlt[6]; char hardShowTrack[6]; char hardShowTrackAlt[6]; char showTrackAlt[6]; char hardNextPage[6]; char hardPrevPage[6]; char hardRemove[6]; char hardRemove2[6]; char mouseLeftClick[12]; char mouseMiddleClick[12]; char mouseRightClick[12]; char mouseScrollUp[12]; char mouseScrollDown[12]; char mouseAltScrollUp[12]; char mouseAltScrollDown[12]; char lastVolume[12]; char allowNotifications[2]; char color[2]; char artistColor[2]; char enqueuedColor[2]; char titleColor[2]; char mouseEnabled[2]; char mouseLeftClickAction[3]; char mouseMiddleClickAction[3]; char mouseRightClickAction[3]; char mouseScrollUpAction[3]; char mouseScrollDownAction[3]; char mouseAltScrollUpAction[12]; char mouseAltScrollDownAction[12]; char hideLogo[2]; char hideHelp[2]; char cacheLibrary[6]; char quitAfterStopping[2]; char hideGlimmeringText[2]; char nextView[6]; char prevView[6]; char hardClearPlaylist[6]; char moveSongUp[6]; char moveSongDown[6]; char enqueueAndPlay[6]; char hardStop[6]; char hardAddToRadioFavorites[6]; char sortLibrary[6]; char visualizerBrailleMode[2]; char tweenFactor[12]; char tweenFactorFall[12]; char progressBarType[2]; } AppSettings; #endif #endif kew-3.2.0/src/cache.c000066400000000000000000000023461500206121000142700ustar00rootroot00000000000000#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 *tmp = current; current = current->next; free(tmp->filePath); free(tmp); } 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-3.2.0/src/cache.h000066400000000000000000000006711500206121000142740ustar00rootroot00000000000000#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-3.2.0/src/common.c000066400000000000000000000013021500206121000145040ustar00rootroot00000000000000#include "common.h" const char VERSION[] = "3.2.0"; #define ERROR_MESSAGE_LENGTH 256 char currentErrorMessage[ERROR_MESSAGE_LENGTH]; bool hasPrintedError = true; volatile bool refresh = true; // Should the whole view be refreshed next time it redraws void setErrorMessage(const char *message) { strncpy(currentErrorMessage, message, ERROR_MESSAGE_LENGTH - 1); currentErrorMessage[ERROR_MESSAGE_LENGTH - 1] = '\0'; hasPrintedError = false; refresh = true; } bool hasErrorMessage() { return (currentErrorMessage[0] != '\0'); } char *getErrorMessage() { return currentErrorMessage; } void clearErrorMessage() { currentErrorMessage[0] = '\0'; } kew-3.2.0/src/common.h000066400000000000000000000006041500206121000145150ustar00rootroot00000000000000#ifndef COMMON_H #define COMMON_H #include #include #include #include #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif extern volatile bool refresh; extern const char VERSION[]; extern bool hasPrintedError; void setErrorMessage(const char *message); bool hasErrorMessage(); char *getErrorMessage(); void clearErrorMessage(); #endif kew-3.2.0/src/common_ui.c000066400000000000000000000246571500206121000152230ustar00rootroot00000000000000 #include "common_ui.h" /* common_ui.c UI functions. */ unsigned int updateCounter = 0; // Name scrolling bool finishedScrolling = false; int lastNamePosition = 0; bool isLongName = false; int scrollDelaySkippedCount = 0; bool isSameNameAsLastTime = false; const int startScrollingDelay = 20; // Delay before beginning to scroll const int scrollingInterval = 2; // Interval between scrolling updates void setTextColorRGB2(int r, int g, int b, UISettings *ui) { if (!ui->useConfigColors) setTextColorRGB(r, g, b); } void setColor(UISettings *ui) { setColorAndWeight(0, ui); } void setColorAndWeight(int bold, UISettings *ui) { if (ui->useConfigColors) { printf("\033[%dm", bold); return; } if (ui->color.r == defaultColor && ui->color.g == defaultColor && ui->color.b == defaultColor) printf("\033[%dm", bold); else if (ui->color.r >= 210 && ui->color.g >= 210 && ui->color.b >= 210) { ui->color.r = defaultColor; ui->color.g = defaultColor; ui->color.b = defaultColor; printf("\033[%d;38;2;%03u;%03u;%03um", bold, ui->color.r, ui->color.g, ui->color.b); } else { printf("\033[%d;38;2;%03u;%03u;%03um", bold, ui->color.r, ui->color.g, ui->color.b); } } void resetNameScroll() { lastNamePosition = 0; isLongName = false; finishedScrolling = false; scrollDelaySkippedCount = 0; } /* * Markus Kuhn -- 2007-05-26 (Unicode 5.0) * * Permission to use, copy, modify, and distribute this software * for any purpose and without fee is hereby granted. The author * disclaims all warranties with regard to this software. * * Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c */ struct interval { int first; int last; }; /* auxiliary function for binary search in interval table */ static int bisearch(wchar_t ucs, const struct interval *table, int max) { int min = 0; int mid; if (ucs < table[0].first || ucs > table[max].last) return 0; while (max >= min) { mid = (min + max) / 2; if (ucs > table[mid].last) min = mid + 1; else if (ucs < table[mid].first) max = mid - 1; else return 1; } return 0; } int mk_wcwidth(wchar_t ucs) { /* sorted list of non-overlapping intervals of non-spacing characters */ /* generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c" */ static const struct interval combining[] = { {0x0300, 0x036F}, {0x0483, 0x0486}, {0x0488, 0x0489}, {0x0591, 0x05BD}, {0x05BF, 0x05BF}, {0x05C1, 0x05C2}, {0x05C4, 0x05C5}, {0x05C7, 0x05C7}, {0x0600, 0x0603}, {0x0610, 0x0615}, {0x064B, 0x065E}, {0x0670, 0x0670}, {0x06D6, 0x06E4}, {0x06E7, 0x06E8}, {0x06EA, 0x06ED}, {0x070F, 0x070F}, {0x0711, 0x0711}, {0x0730, 0x074A}, {0x07A6, 0x07B0}, {0x07EB, 0x07F3}, {0x0901, 0x0902}, {0x093C, 0x093C}, {0x0941, 0x0948}, {0x094D, 0x094D}, {0x0951, 0x0954}, {0x0962, 0x0963}, {0x0981, 0x0981}, {0x09BC, 0x09BC}, {0x09C1, 0x09C4}, {0x09CD, 0x09CD}, {0x09E2, 0x09E3}, {0x0A01, 0x0A02}, {0x0A3C, 0x0A3C}, {0x0A41, 0x0A42}, {0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A70, 0x0A71}, {0x0A81, 0x0A82}, {0x0ABC, 0x0ABC}, {0x0AC1, 0x0AC5}, {0x0AC7, 0x0AC8}, {0x0ACD, 0x0ACD}, {0x0AE2, 0x0AE3}, {0x0B01, 0x0B01}, {0x0B3C, 0x0B3C}, {0x0B3F, 0x0B3F}, {0x0B41, 0x0B43}, {0x0B4D, 0x0B4D}, {0x0B56, 0x0B56}, {0x0B82, 0x0B82}, {0x0BC0, 0x0BC0}, {0x0BCD, 0x0BCD}, {0x0C3E, 0x0C40}, {0x0C46, 0x0C48}, {0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0CBC, 0x0CBC}, {0x0CBF, 0x0CBF}, {0x0CC6, 0x0CC6}, {0x0CCC, 0x0CCD}, {0x0CE2, 0x0CE3}, {0x0D41, 0x0D43}, {0x0D4D, 0x0D4D}, {0x0DCA, 0x0DCA}, {0x0DD2, 0x0DD4}, {0x0DD6, 0x0DD6}, {0x0E31, 0x0E31}, {0x0E34, 0x0E3A}, {0x0E47, 0x0E4E}, {0x0EB1, 0x0EB1}, {0x0EB4, 0x0EB9}, {0x0EBB, 0x0EBC}, {0x0EC8, 0x0ECD}, {0x0F18, 0x0F19}, {0x0F35, 0x0F35}, {0x0F37, 0x0F37}, {0x0F39, 0x0F39}, {0x0F71, 0x0F7E}, {0x0F80, 0x0F84}, {0x0F86, 0x0F87}, {0x0F90, 0x0F97}, {0x0F99, 0x0FBC}, {0x0FC6, 0x0FC6}, {0x102D, 0x1030}, {0x1032, 0x1032}, {0x1036, 0x1037}, {0x1039, 0x1039}, {0x1058, 0x1059}, {0x1160, 0x11FF}, {0x135F, 0x135F}, {0x1712, 0x1714}, {0x1732, 0x1734}, {0x1752, 0x1753}, {0x1772, 0x1773}, {0x17B4, 0x17B5}, {0x17B7, 0x17BD}, {0x17C6, 0x17C6}, {0x17C9, 0x17D3}, {0x17DD, 0x17DD}, {0x180B, 0x180D}, {0x18A9, 0x18A9}, {0x1920, 0x1922}, {0x1927, 0x1928}, {0x1932, 0x1932}, {0x1939, 0x193B}, {0x1A17, 0x1A18}, {0x1B00, 0x1B03}, {0x1B34, 0x1B34}, {0x1B36, 0x1B3A}, {0x1B3C, 0x1B3C}, {0x1B42, 0x1B42}, {0x1B6B, 0x1B73}, {0x1DC0, 0x1DCA}, {0x1DFE, 0x1DFF}, {0x200B, 0x200F}, {0x202A, 0x202E}, {0x2060, 0x2063}, {0x206A, 0x206F}, {0x20D0, 0x20EF}, {0x302A, 0x302F}, {0x3099, 0x309A}, {0xA806, 0xA806}, {0xA80B, 0xA80B}, {0xA825, 0xA826}, {0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F}, {0xFE20, 0xFE23}, {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0x10A01, 0x10A03}, {0x10A05, 0x10A06}, {0x10A0C, 0x10A0F}, {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F}, {0x1D167, 0x1D169}, {0x1D173, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244}, {0xE0001, 0xE0001}, {0xE0020, 0xE007F}, {0xE0100, 0xE01EF}}; /* test for 8-bit control characters */ if (ucs == 0) return 0; if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) return -1; /* binary search in table of non-spacing characters */ if (bisearch(ucs, combining, sizeof(combining) / sizeof(struct interval) - 1)) return 0; /* if we arrive here, ucs is not a combining or C0/C1 control character */ return 1 + (ucs >= 0x1100 && (ucs <= 0x115f || /* Hangul Jamo init. consonants */ ucs == 0x2329 || ucs == 0x232a || (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs != 0x303f) || /* CJK ... Yi */ (ucs >= 0xac00 && ucs <= 0xd7a3) || /* Hangul Syllables */ (ucs >= 0xf900 && ucs <= 0xfaff) || /* CJK Compatibility Ideographs */ (ucs >= 0xfe10 && ucs <= 0xfe19) || /* Vertical forms */ (ucs >= 0xfe30 && ucs <= 0xfe6f) || /* CJK Compatibility Forms */ (ucs >= 0xff00 && ucs <= 0xff60) || /* Fullwidth Forms */ (ucs >= 0xffe0 && ucs <= 0xffe6) || (ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd))); } int mk_wcswidth(const wchar_t *pwcs, size_t n) { int w, width = 0; for (; *pwcs && n-- > 0; pwcs++) if ((w = mk_wcwidth(*pwcs)) < 0) return -1; else width += w; return width; } /* End Markus Kuhn code */ void copyHalfOrFullWidthCharsWithMaxWidth(const char *src, char *dst, int maxWidth) { mbstate_t state; memset(&state, 0, sizeof(state)); const char *p = src; char *o = dst; wchar_t wc; size_t len; int width = 0; while (*p) { len = mbrtowc(&wc, p, MB_CUR_MAX, &state); if (len == (size_t)-1 || len == (size_t)-2 || len == 0) break; int w = mk_wcwidth(wc); if (w < 0) break; if (width + w > maxWidth) break; memcpy(o, p, len); o += len; p += len; width += w; } *o = '\0'; } static bool hasFullwidthChars(const char *str) { mbstate_t state; memset(&state, 0, sizeof(state)); const char *p = str; wchar_t wc; size_t len; while (*p) { len = mbrtowc(&wc, p, MB_CUR_MAX, &state); if (len == (size_t)-1 || len == (size_t)-2 || len == 0) break; int w = mk_wcwidth(wc); if (w < 0) { break; } if (w > 1) { return true; } p += len; } return false; } void processName(const char *name, char *output, int maxWidth) { const char *lastDot = strrchr(name, '.'); if (lastDot != NULL) { char tmp[1024]; size_t len = lastDot - name; if (len >= sizeof(tmp)) len = sizeof(tmp) - 1; strncpy(tmp, name, len); tmp[len] = '\0'; copyHalfOrFullWidthCharsWithMaxWidth(tmp, output, maxWidth); } else { copyHalfOrFullWidthCharsWithMaxWidth(name, output, maxWidth); } removeUnneededChars(output, strlen(output)); trim(output, strlen(output)); } void processNameScroll(const char *name, char *output, int maxWidth, bool isSameNameAsLastTime) { const char *lastDot = strrchr(name, '.'); size_t nameLength = strlen(name); size_t scrollableLength = (lastDot != NULL) ? (size_t)(lastDot - name) : nameLength; if (scrollDelaySkippedCount <= startScrollingDelay && scrollableLength > (size_t)maxWidth) { scrollableLength = maxWidth; scrollDelaySkippedCount++; refresh = true; isLongName = true; } int start = (isSameNameAsLastTime) ? lastNamePosition : 0; if (finishedScrolling) scrollableLength = maxWidth; if (hasFullwidthChars(name)) { processName(name, output, maxWidth); } else if (scrollableLength <= (size_t)maxWidth || finishedScrolling) { processName(name, output, scrollableLength); } else { isLongName = true; if ((size_t)(start + maxWidth) > scrollableLength) { start = 0; finishedScrolling = true; } strncpy(output, name + start, maxWidth); output[maxWidth] = '\0'; removeUnneededChars(output, maxWidth); trim(output, maxWidth); lastNamePosition++; refresh = true; } } bool getIsLongName() { return isLongName; } kew-3.2.0/src/common_ui.h000066400000000000000000000011661500206121000152160ustar00rootroot00000000000000#ifndef COMMON_UI_H #define COMMON_UI_H #include #include #include #include "appstate.h" #include "term.h" #include "common.h" extern unsigned int updateCounter; extern const int scrollingInterval; extern bool isSameNameAsLastTime; void setTextColorRGB2(int r, int g, int b, UISettings *ui); void setColor(UISettings *ui); void setColorAndWeight(int bold, UISettings *ui); void processNameScroll(const char *name, char *output, int maxWidth, bool isSameNameAsLastTime); void resetNameScroll(); bool getIsLongName(); void processName(const char *name, char *output, int maxWidth); #endif kew-3.2.0/src/directorytree.c000066400000000000000000000516521500206121000161150ustar00rootroot00000000000000#include "directorytree.h" /* directorytree.c Related to library / directory structure. */ 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 = strnlen(parentPath, MAXPATHLEN) + strnlen(entryName, MAXPATHLEN) + 2; // +2 for '/' and '\0' entry->fullPath = (char *)malloc(fullPathLength); if (entry->fullPath == NULL) { return; } snprintf(entry->fullPath, fullPathLength, "%s/%s", parentPath, entryName); } 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 naturalCompare(const char *a, const char *b) { while (*a && *b) { if (isdigit(*a) && isdigit(*b)) { // Compare numerically char *endA, *endB; long numA = strtol(a, &endA, 10); long numB = strtol(b, &endB, 10); if (numA != numB) { return numA - numB; } // Move pointers past the numeric part a = endA; b = endB; } else { if (*a != *b) { return *a - *b; } a++; b++; } } // If all parts so far are equal, shorter string should come first return *a - *b; } int compareLibEntries(const struct dirent **a, const struct dirent **b) { // All strings need to be uppercased or already uppercased characters will come before all lower-case ones char *nameA = stringToUpper((*a)->d_name); char *nameB = stringToUpper((*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 = naturalCompare(nameA, nameB); free(nameA); free(nameB); return result; } int compareLibEntriesReversed(const struct dirent **a, const struct dirent **b) { int result = compareLibEntries(a, b); return -result; } int compareEntryNatural(const void *a, const void *b) { const FileSystemEntry *entryA = *(const FileSystemEntry **)a; const FileSystemEntry *entryB = *(const FileSystemEntry **)b; // Optional: handle leading underscores like your original comparator char *nameA = stringToUpper(entryA->name); char *nameB = stringToUpper(entryB->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 = naturalCompare(nameA, nameB); free(nameA); free(nameB); return result; } int compareEntryNaturalReversed(const void *a, const void *b) { return -compareEntryNatural(a, b); } 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; } 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, compareLibEntriesReversed); 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[100]; 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 **tmpNodes = resizeNodesArray(nodes, oldCapacity, nodesCapacity); if (!tmpNodes) { 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(1); } nodes = tmpNodes; } FileSystemEntry *node = malloc(sizeof(FileSystemEntry)); if (!node) { perror("Failed to allocate node"); fclose(file); exit(1); } 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; } #ifdef __GNUC__ #ifndef __APPLE__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #endif #endif // 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 utf8_levenshteinDistance(const char *s1, const char *s2) { // Get the length of s1 and s2 in terms of characters, not bytes int len1 = g_utf8_strlen(s1, -1); int len2 = g_utf8_strlen(s2, -1); // Allocate a 2D matrix (only two rows at a time are needed) int *prevRow = (int *)malloc((len2 + 1) * sizeof(int)); int *currRow = (int *)malloc((len2 + 1) * sizeof(int)); // Initialize the first row (for empty s1) for (int j = 0; j <= len2; j++) { prevRow[j] = j; } // Iterate over the characters of both strings const char *p1 = s1; for (int i = 1; i <= len1; i++, p1 = g_utf8_next_char(p1)) { currRow[0] = i; const char *p2 = s2; for (int j = 1; j <= len2; j++, p2 = g_utf8_next_char(p2)) { // Compare Unicode characters using g_utf8_get_char gunichar c1 = g_utf8_get_char(p1); gunichar c2 = g_utf8_get_char(p2); int cost = (c1 == c2) ? 0 : 1; // Fill the current row with the minimum of deletion, insertion, or substitution currRow[j] = MIN(prevRow[j] + 1, // Deletion MIN(currRow[j - 1] + 1, // Insertion prevRow[j - 1] + cost)); // Substitution } // Swap rows (current becomes previous for the next iteration) int *tmp = prevRow; prevRow = currRow; currRow = tmp; } // The last value in prevRow contains the Levenshtein distance int distance = prevRow[len2]; // Free the allocated memory free(prevRow); free(currRow); return distance; } #ifdef __GNUC__ #ifndef __APPLE__ #pragma GCC diagnostic pop #endif #endif char *stripFileExtension(const char *filename) { char *dot = strrchr(filename, '.'); // find last '.' size_t length = (dot != NULL) ? (size_t)(dot - filename) : strlen(filename); char *result = (char *)malloc(length + 1); if (!result) return NULL; // handle malloc failure strncpy(result, filename, length); result[length] = '\0'; return result; } // 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 = g_utf8_casefold((char *)searchTerm, -1); char *lowerName = g_utf8_casefold(node->name, -1); char *strippedName = stripFileExtension(lowerName); int nameDistance = utf8_levenshteinDistance(strippedName, lowerSearchTerm); // Partial matching with lowercase strings if (strstr(strippedName, lowerSearchTerm) != NULL) { callback(node, 0); } else if (nameDistance <= threshold) { callback(node, nameDistance); } // Free the allocated memory for lowercase strings g_free(lowerSearchTerm); g_free(lowerName); free(strippedName); fuzzySearchRecursive(node->children, searchTerm, threshold, callback); fuzzySearchRecursive(node->next, searchTerm, threshold, callback); } FileSystemEntry *findCorrespondingEntry(FileSystemEntry *tmp, const char *fullPath) { if (tmp == NULL) return NULL; if (strcmp(tmp->fullPath, fullPath) == 0) return tmp; FileSystemEntry *found = findCorrespondingEntry(tmp->children, fullPath); if (found != NULL) return found; return findCorrespondingEntry(tmp->next, fullPath); } void copyIsEnqueued(FileSystemEntry *library, FileSystemEntry *tmp) { if (library == NULL) return; if (library->isEnqueued) { FileSystemEntry *tmpEntry = findCorrespondingEntry(tmp, library->fullPath); if (tmpEntry != NULL) { tmpEntry->isEnqueued = library->isEnqueued; } } copyIsEnqueued(library->children, tmp); copyIsEnqueued(library->next, tmp); } int compareFoldersByAgeFilesAlphabetically(const void *a, const void *b) { const FileSystemEntry *entryA = *(const FileSystemEntry **)a; const FileSystemEntry *entryB = *(const FileSystemEntry **)b; // Both are directories → sort by mtime descending if (entryA->isDirectory && entryB->isDirectory) { struct stat statA, statB; if (stat(entryA->fullPath, &statA) != 0 || stat(entryB->fullPath, &statB) != 0) return 0; return (int)(statB.st_mtime - statA.st_mtime); // newer first } // Both are files → sort alphabetically if (!entryA->isDirectory && !entryB->isDirectory) { return strcasecmp(entryA->name, entryB->name); } // Put directories before files return entryB->isDirectory - entryA->isDirectory; } void sortFileSystemEntryChildren(FileSystemEntry *parent, int (*comparator)(const void *, const void *)) { int count = 0; FileSystemEntry *curr = parent->children; while (curr) { count++; curr = curr->next; } if (count < 2) return; FileSystemEntry **entryArray = malloc(count * sizeof(FileSystemEntry *)); curr = parent->children; for (int i = 0; i < count; i++) { entryArray[i] = curr; curr = curr->next; } qsort(entryArray, count, sizeof(FileSystemEntry *), comparator); for (int i = 0; i < count - 1; i++) { entryArray[i]->next = entryArray[i + 1]; } entryArray[count - 1]->next = NULL; parent->children = entryArray[0]; free(entryArray); } void sortFileSystemTree(FileSystemEntry *root, int (*comparator)(const void *, const void *)) { if (!root) return; sortFileSystemEntryChildren(root, comparator); FileSystemEntry *child = root->children; while (child) { if (child->isDirectory) { sortFileSystemTree(child, comparator); } child = child->next; } } kew-3.2.0/src/directorytree.h000066400000000000000000000034211500206121000161110ustar00rootroot00000000000000#ifndef DIRECTORYTREE_H #define DIRECTORYTREE_H #include #include #include #include #include #include #include #include #include "file.h" #include "utils.h" #ifndef PATH_MAX #define PATH_MAX 4096 #endif #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)); void copyIsEnqueued(FileSystemEntry *library, FileSystemEntry *tmp); void sortFileSystemTree(FileSystemEntry *root, int (*comparator)(const void *, const void *)); int compareFoldersByAgeFilesAlphabetically(const void *a, const void *b); int compareLibEntries(const struct dirent **a, const struct dirent **b); int compareLibEntriesReversed(const struct dirent **a, const struct dirent **b); int compareEntryNaturalReversed(const void *a, const void *b); int compareEntryNatural(const void *a, const void *b); #endif kew-3.2.0/src/events.h000066400000000000000000000027531500206121000145400ustar00rootroot00000000000000#ifndef EVENTS_H #define EVENTS_H #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_TOGGLEREPEAT, EVENT_TOGGLEVISUALIZER, EVENT_TOGGLEASCII, EVENT_ADDTOMAINPLAYLIST, EVENT_DELETEFROMMAINPLAYLIST, EVENT_EXPORTPLAYLIST, EVENT_UPDATELIBRARY, EVENT_SHUFFLE, EVENT_KEY_PRESS, EVENT_SHOWKEYBINDINGS, EVENT_SHOWPLAYLIST, EVENT_SHOWSEARCH, EVENT_SHOWRADIOSEARCH, EVENT_GOTOSONG, EVENT_GOTOBEGINNINGOFPLAYLIST, EVENT_GOTOENDOFPLAYLIST, EVENT_TOGGLEPROFILECOLORS, EVENT_SCROLLNEXT, EVENT_SCROLLPREV, EVENT_SEEKBACK, EVENT_SEEKFORWARD, EVENT_SHOWLIBRARY, EVENT_SHOWTRACK, EVENT_NEXTPAGE, EVENT_PREVPAGE, EVENT_REMOVE, EVENT_SEARCH, EVENT_NEXTVIEW, EVENT_PREVVIEW, EVENT_CLEARPLAYLIST, EVENT_RADIOSEARCH, EVENT_MOVESONGUP, EVENT_MOVESONGDOWN, EVENT_ENQUEUEANDPLAY, EVENT_ADDTORADIOFAVORITES, EVENT_STOP, EVENT_SORTLIBRARY }; struct Event { enum EventType type; char key[MAX_SEQ_LEN]; // To store multi-byte characters }; typedef struct { char *seq; enum EventType eventType; } EventMapping; #endif kew-3.2.0/src/file.c000066400000000000000000000257131500206121000141470ustar00rootroot00000000000000#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 path_length = strnlen(path, MAXPATHLEN); char tmpPath[path_length + 1]; c_strcpy(tmpPath, path, sizeof(tmpPath)); char *dir = dirname(tmpPath); // Copy directory name to the output buffer snprintf(directory, MAXPATHLEN, "%s", dir); size_t directory_length = strnlen(directory, MAXPATHLEN); if (directory[directory_length - 1] != '/' && directory_length + 1 < MAXPATHLEN) { // Use snprintf to append '/' at the end of directory snprintf(directory + directory_length, MAXPATHLEN - directory_length, "/"); } } 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 *lowCaseSearching, char *result, const char *allowedExtensions, enum SearchType searchType, bool exactSearch) { DIR *d; struct dirent *dir; struct stat file_stat; char ext[100]; // +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)) { char *name = g_utf8_casefold(dir->d_name, -1); if (((exactSearch && (strcasecmp(name, lowCaseSearching) == 0)) || (!exactSearch && c_strcasestr(name, lowCaseSearching, MAXPATHLEN) != NULL)) && (searchType != FileOnly) && (searchType != SearchPlayList)) { char *curDir = getcwd(NULL, 0); snprintf(result, MAXPATHLEN, "%s/%s", curDir, dir->d_name); free(curDir); free(name); copyresult = true; break; } else { free(name); if (chdir(dir->d_name) == -1) { fprintf(stderr, "Failed to change directory: %s\n", dir->d_name); continue; } if (walker(NULL, lowCaseSearching, 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 (strnlen(filename, 256) <= 4) { continue; } extractExtension(filename, sizeof(ext) - 1, ext); if (match_regex(®ex, ext) != 0) { continue; } char *name = g_utf8_casefold(dir->d_name, -1); if ((exactSearch && (strcasecmp(name, lowCaseSearching) == 0)) || (!exactSearch && c_strcasestr(name, lowCaseSearching, MAXPATHLEN) != NULL)) { char *curDir = getcwd(NULL, 0); snprintf(result, MAXPATHLEN, "%s/%s", curDir, dir->d_name); copyresult = true; free(curDir); free(name); break; } free(name); } } 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 = strnlen(homeDir, MAXPATHLEN); size_t inputPathLen = strnlen(inputPath, MAXPATHLEN); if (homeDirLen + inputPathLen >= MAXPATHLEN) { return -1; // Expanded path exceeds maximum length } c_strcpy(expandedPath, homeDir, MAXPATHLEN); snprintf(expandedPath + homeDirLen, MAXPATHLEN - homeDirLen, "%s", 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 deleteFile(const char *filePath) { if (remove(filePath) == 0) { return 0; } else { return -1; } } int isInTempDir(const char *path) { const char *tmpDir = getenv("TMPDIR"); if (tmpDir == NULL || strnlen(tmpDir, PATH_MAX) >= PATH_MAX) { tmpDir = "/tmp"; } return (pathStartsWith(path, tmpDir)); } void generateTempFilePath(char *filePath, const char *prefix, const char *suffix) { const char *tmpDir = getenv("TMPDIR"); if (tmpDir == NULL || strnlen(tmpDir, PATH_MAX) >= PATH_MAX) { tmpDir = "/tmp"; } struct passwd *pw = getpwuid(getuid()); const char *username = pw ? pw->pw_name : "unknown"; char dirPath[MAXPATHLEN]; snprintf(dirPath, sizeof(dirPath), "%s/kew", tmpDir); createDirectory(dirPath); snprintf(dirPath, sizeof(dirPath), "%s/kew/%s", tmpDir, username); createDirectory(dirPath); char randomString[7]; for (int i = 0; i < 6; ++i) { randomString[i] = 'a' + rand() % 26; } randomString[6] = '\0'; int written = snprintf(filePath, MAXPATHLEN, "%s/%s%.6s%s", dirPath, prefix, randomString, suffix); if (written < 0 || written >= MAXPATHLEN) { filePath[0] = '\0'; } } kew-3.2.0/src/file.h000066400000000000000000000023701500206121000141460ustar00rootroot00000000000000#ifndef FILE_H #define FILE_H #include #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|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 deleteFile(const char *filePath); void generateTempFilePath(char *filePath, const char *prefix, const char *suffix); int isInTempDir(const char *path); int existsFile(const char *fname); #endif kew-3.2.0/src/imgfunc.c000066400000000000000000000421251500206121000146540ustar00rootroot00000000000000#include "imgfunc.h" #include "term.h" /* imgfunc.c Related to displaying an image in the terminal. */ // 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 /* 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 #define MACRO_STRLEN(s) (sizeof(s) / sizeof(s[0])) typedef struct { gint width_cells, height_cells; gint width_pixels, height_pixels; } TermSize; char scale[] = "$@&B%8WM#ZO0QoahkbdpqwmLCJUYXIjft/\\|()1{}[]l?zcvunxr!<>i;:*-+~_,\"^`'. "; unsigned int brightness_levels = MACRO_STRLEN(scale) - 2; 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; } // The function to load and return image data unsigned char *getBitmap(const char *image_path, int *width, int *height) { if (image_path == NULL) return NULL; int channels; unsigned char *image = stbi_load(image_path, width, height, &channels, 4); // Force 4 channels (RGBA) if (!image) { fprintf(stderr, "Failed to load image: %s\n", image_path); return NULL; } return image; } float calcAspectRatio(void) { 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(unsigned char *pixels, int width, int height, int baseHeight) { if (pixels == NULL) { printf("Error: Invalid pixel data.\n"); return; } // Use the provided width and height int pix_width = width; int pix_height = height; int n_channels = 4; // Assuming RGBA format // Validate the image dimensions if (pix_width == 0 || pix_height == 0) { printf("Error: Invalid image dimensions.\n"); return; } 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 cell size for some terminals if (cell_width == -1 || cell_height == -1) { cell_width = 8; cell_height = 16; } // Calculate corrected width based on aspect ratio correction float aspect_ratio_correction = (float)cell_height / (float)cell_width; int correctedWidth = (int)(baseHeight * aspect_ratio_correction); // Convert image to a printable string using Chafa printable = convert_image( pixels, pix_width, pix_height, pix_width * n_channels, // Row stride CHAFA_PIXEL_RGBA8_UNASSOCIATED, // Correct pixel format correctedWidth, baseHeight, cell_width, cell_height); // Ensure the string is null-terminated g_string_append_c(printable, '\0'); // Split the printable string into lines const gchar *delimiters = "\n"; gchar **lines = g_strsplit(printable->str, delimiters, -1); // Calculate indentation to center the image int indentation = ((term_size.width_cells - correctedWidth) / 2); // Print each line with indentation for (int i = 0; lines[i] != NULL; i++) { printf("\n\033[%dC%s", indentation, lines[i]); } // Free allocated memory g_strfreev(lines); g_string_free(printable, TRUE); } unsigned char luminanceFromRGB(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 = luminanceFromRGB(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(unsigned char *pixels, int width, int height, unsigned char *r, unsigned char *g, unsigned char *b) { if (pixels == NULL || width <= 0 || height <= 0) { return -1; } int channels = 4; // RGBA format bool found = false; int numPixels = width * height; for (int i = 0; i < numPixels; i++) { int index = i * channels; unsigned char red = pixels[index + 0]; unsigned char green = pixels[index + 1]; unsigned char blue = pixels[index + 2]; checkIfBrightPixel(red, green, blue, &found); if (found) { *r = red; *g = green; *b = blue; break; } } return found ? 0 : -1; } unsigned char calcAsciiChar(PixelData *p) { unsigned char ch = luminanceFromRGB(p->r, p->g, p->b); int rescaled = ch * brightness_levels / 256; return scale[brightness_levels - rescaled]; } int convertToAscii(const char *filepath, unsigned int height) { /* Modified, originally by Danny Burrows: https://github.com/danny-burrows/img_to_txt MIT 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. */ 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; } float aspect_ratio_correction = (float)cell_height / (float)cell_width; unsigned int correctedWidth = (int)(height * aspect_ratio_correction) - 1; // Calculate indentation to center the image int indent = ((term_size.width_cells - correctedWidth) / 2); int rwidth, rheight, rchannels; unsigned char *read_data = stbi_load(filepath, &rwidth, &rheight, &rchannels, 3); if (read_data == NULL) { return -1; } PixelData *data; if (correctedWidth != (unsigned)rwidth || height != (unsigned)rheight) { // 3 * uint8 for RGB! unsigned char *new_data = malloc(3 * sizeof(unsigned char) * correctedWidth * height); stbir_resize_uint8_srgb( read_data, rwidth, rheight, 0, new_data, correctedWidth, height, 0, 3); stbi_image_free(read_data); data = (PixelData *)new_data; } else { data = (PixelData *)read_data; } printf("\n"); printf("%*s", indent, ""); for (unsigned int d = 0; d < correctedWidth * height; d++) { if (d % correctedWidth == 0 && d != 0) { printf("\n"); printf("%*s", indent, ""); } PixelData *c = data + d; printf("\033[1;38;2;%03u;%03u;%03um%c", c->r, c->g, c->b, calcAsciiChar(c)); } printf("\n"); stbi_image_free(data); return 0; } int printInAscii(const char *pathToImgFile, int height) { printf("\r"); int ret = convertToAscii(pathToImgFile,(unsigned)height); if (ret == -1) printf("\033[0m"); return 0; } kew-3.2.0/src/imgfunc.h000066400000000000000000000013021500206121000146510ustar00rootroot00000000000000#include #include #include #include #include #include #include #ifndef PIXELDATA_STRUCT #define PIXELDATA_STRUCT typedef struct { unsigned char r; unsigned char g; unsigned char b; } PixelData; #endif int printInAscii(const char *pathToImgFile, int height); float calcAspectRatio(void); unsigned char *getBitmap(const char *image_path, int *width, int *height); void printSquareBitmapCentered(unsigned char *pixels, int width, int height, int baseHeight); int getCoverColor(unsigned char *pixels, int width, int height, unsigned char *r, unsigned char *g, unsigned char *b); kew-3.2.0/src/kew.c000066400000000000000000001565161500206121000140240ustar00rootroot00000000000000/* kew - A terminal 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 __USE_POSIX #define __USE_POSIX #endif #ifdef __FreeBSD__ #define __BSD_VISIBLE 1 #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "appstate.h" #include "cache.h" #include "events.h" #include "file.h" #include "mpris.h" #include "notifications.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 #define TMPPIDFILE "/tmp/kew_" FILE *logFile = NULL; struct winsize windowSize; char digitsPressed[MAX_SEQ_LEN]; int digitsPressedCount = 0; bool startFromTop = false; int lastNotifiedId = -1; bool songWasRemoved = false; bool noPlaylist = false; GMainLoop *main_loop; EventMapping keyMappings[NUM_KEY_MAPPINGS]; struct timespec lastInputTime; bool exactSearch = false; int fuzzySearchThreshold = 4; int maxDigitsPressedCount = 9; int isNewSearchTerm = false; bool wasEndOfList = false; void updateLastInputTime(void) { clock_gettime(CLOCK_MONOTONIC, &lastInputTime); } 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 (isCooldownElapsed(COOLDOWN_MS)) cooldownElapsed = true; if (isCooldownElapsed(COOLDOWN2_MS)) cooldown2Elapsed = true; int seqLength = 0; char seq[MAX_SEQ_LEN]; seq[0] = '\0'; // Set initial value int keyReleased = 0; bool foundInput = false; // 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; } foundInput = true; size_t seq_len = strnlen(seq, MAX_SEQ_LEN); size_t remaining_space = MAX_SEQ_LEN - seq_len; if (remaining_space < 1) { break; } snprintf(seq + seq_len, remaining_space, "%s", 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; readInputSequence(tmpSeq, sizeof(tmpSeq)); // Dummy read to prevent scrolling after key released break; } keyReleased = 0; } if (!foundInput && cooldownElapsed) { flushSeek(); return event; } if (keyReleased) return event; event.type = EVENT_NONE; c_strcpy(event.key, seq, MAX_SEQ_LEN); if (appState.currentView == SEARCH_VIEW) { if (strcmp(event.key, "\x7F") == 0 || strcmp(event.key, "\x08") == 0) { removeFromSearchText(); resetSearchResult(); fuzzySearch(getLibrary(), fuzzySearchThreshold); event.type = EVENT_SEARCH; } else if (((strnlen(event.key, sizeof(event.key)) == 1 && event.key[0] != '\033' && event.key[0] != '\n' && event.key[0] != '\t' && event.key[0] != '\r') || strcmp(event.key, " ") == 0 || (unsigned char)event.key[0] >= 0xC0) && strcmp(event.key, "Z") != 0 && strcmp(event.key, "X") != 0 && strcmp(event.key, "C") != 0 && strcmp(event.key, "V") != 0 && strcmp(event.key, "B") != 0 && strcmp(event.key, "N") != 0) { addToSearchText(event.key); resetSearchResult(); fuzzySearch(getLibrary(), fuzzySearchThreshold); event.type = EVENT_SEARCH; } } if (appState.currentView == RADIOSEARCH_VIEW) { if (strcmp(event.key, "\x7F") == 0 || strcmp(event.key, "\x08") == 0) { removeFromRadioSearchText(); resetRadioSearchResult(); isNewSearchTerm = true; event.type = EVENT_RADIOSEARCH; } else if (((strnlen(event.key, sizeof(event.key)) == 1 && event.key[0] != '\033' && event.key[0] != '\n' && event.key[0] != '\t' && event.key[0] != '\r') || strcmp(event.key, " ") == 0 || (unsigned char)event.key[0] >= 0xC0) && strcmp(event.key, "Z") != 0 && strcmp(event.key, "X") != 0 && strcmp(event.key, "C") != 0 && strcmp(event.key, "V") != 0 && strcmp(event.key, "F") != 0 && strcmp(event.key, "S") != 0 && strcmp(event.key, "B") != 0 && strcmp(event.key, "N") != 0) { addToRadioSearchText(event.key); resetRadioSearchResult(); isNewSearchTerm = true; event.type = EVENT_RADIOSEARCH; } else if (event.key[0] == '\n') { if (isNewSearchTerm && hasRadioSearchText()) { radioSearch(); event.type = EVENT_RADIOSEARCH; isNewSearchTerm = false; } } } if (seq[0] == 127) { seq[0] = '\b'; // Treat as Backspace } // Set event for pressed key for (int i = 0; i < NUM_KEY_MAPPINGS; i++) { if (keyMappings[i].seq[0] != '\0' && ((seq[0] == '\033' && strnlen(seq, MAX_SEQ_LEN) > 1 && strcmp(seq, "\033\n") != 0 && strcmp(seq + 1, keyMappings[i].seq) == 0) || strcmp(seq, keyMappings[i].seq) == 0)) { if (event.type == EVENT_SEARCH && keyMappings[i].eventType != EVENT_GOTOSONG) { break; } if (event.type == EVENT_RADIOSEARCH && keyMappings[i].eventType != EVENT_GOTOSONG) { break; } event.type = keyMappings[i].eventType; break; } // Received mouse input instead of keyboard input if (keyMappings[i].seq[0] != '\0' && strnlen(seq, MAX_SEQ_LEN) > 3 && strncmp(seq, "\033[M", 3) == 0 && ((strncmp(seq + 1, keyMappings[i].seq, 3) == 0) || strncmp(seq, keyMappings[i].seq, 3) == 0)) { event.type = keyMappings[i].eventType; break; } } for (int i = 0; i < NUM_KEY_MAPPINGS; i++) { if (strcmp(seq, "\033\n") == 0 && strcmp(keyMappings[i].seq, "^M") == 0) // ALT+ENTER { event.type = keyMappings[i].eventType; break; } if (strcmp(seq, keyMappings[i].seq) == 0 && strnlen(seq, MAX_SEQ_LEN) > 1) // ALT+something { event.type = keyMappings[i].eventType; break; } } // 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; } return event; } void setEndOfListReached(AppState *state) { loadedNextSong = false; audioData.endOfListReached = true; usingSongDataA = false; currentSong = NULL; audioData.currentFileIndex = 0; audioData.restart = true; loadingdata.loadA = true; pthread_mutex_lock(&dataSourceMutex); cleanupPlaybackDevice(); pthread_mutex_unlock(&dataSourceMutex); stopped = true; refresh = true; if (isRepeatListEnabled()) repeatList(); else { emitPlaybackStoppedMpris(); emitMetadataChanged("", "", "", "", "/org/mpris/MediaPlayer2/TrackList/NoTrack", NULL, 0); state->currentView = LIBRARY_VIEW; } } void notifyMPRISSwitch(SongData *currentSongData) { if (currentSongData == NULL) return; gint64 length = getLengthInMicroSec(currentSongData->duration); // Update mpris emitMetadataChanged( currentSongData->metadata->title, currentSongData->metadata->artist, currentSongData->metadata->album, currentSongData->coverArtPath, currentSongData->trackId != NULL ? currentSongData->trackId : "", currentSong, length); } void notifySongSwitch(SongData *currentSongData, UISettings *ui) { if (currentSongData != NULL && currentSongData->hasErrors == 0 && currentSongData->metadata && strnlen(currentSongData->metadata->title, 10) > 0) { #ifdef USE_DBUS displaySongNotification(currentSongData->metadata->artist, currentSongData->metadata->title, currentSongData->coverArtPath, ui); #else (void)ui; #endif notifyMPRISSwitch(currentSongData); lastNotifiedId = currentSong->id; } } void determineSongAndNotify(UISettings *ui) { SongData *currentSongData = NULL; bool isDeleted = determineCurrentSongData(¤tSongData); if (currentSongData && currentSong) currentSong->song.duration = currentSongData->duration; if (lastNotifiedId != currentSong->id) { if (!isDeleted) notifySongSwitch(currentSongData, ui); } } // Checks conditions for refreshing player bool shouldRefreshPlayer() { return !skipping && !isEOFReached() && !isImplSwitchReached(); } // Refreshes the player visually if conditions are met void refreshPlayer(UIState *uis) { int mutexResult = pthread_mutex_trylock(&switchMutex); if (mutexResult != 0) { fprintf(stderr, "Failed to lock switch mutex.\n"); return; } if (uis->doNotifyMPRISPlaying) { uis->doNotifyMPRISPlaying = false; emitStringPropertyChanged("PlaybackStatus", "Playing"); } if (uis->doNotifyMPRISSwitched) { uis->doNotifyMPRISSwitched = false; notifyMPRISSwitch(getCurrentSongData()); } if (shouldRefreshPlayer()) { printPlayer(getCurrentSongData(), elapsedSeconds, &settings, &appState); } pthread_mutex_unlock(&switchMutex); } void resetListAfterDequeuingPlayingSong(AppState *state) { if (lastPlayedId < 0) return; Node *node = findSelectedEntryById(&playlist, lastPlayedId); if (currentSong == NULL && node == NULL) { stopPlayback(); loadedNextSong = false; audioData.endOfListReached = true; audioData.restart = true; emitMetadataChanged("", "", "", "", "/org/mpris/MediaPlayer2/TrackList/NoTrack", NULL, 0); emitPlaybackStoppedMpris(); pthread_mutex_lock(&dataSourceMutex); cleanupPlaybackDevice(); pthread_mutex_unlock(&dataSourceMutex); refresh = true; switchAudioImplementation(); unloadSongA(state); unloadSongB(state); songWasRemoved = true; userData.currentSongData = NULL; audioData.currentFileIndex = 0; audioData.restart = true; waitingForNext = true; startFromTop = true; loadingdata.loadA = true; usingSongDataA = false; ma_data_source_uninit(&audioData); audioData.switchFiles = false; if (playlist.count == 0) songToStartFrom = NULL; } } int getSongNumber(const char *str) { char *endptr; long value = strtol(str, &endptr, 10); if (*endptr != '\0') { return 0; } if (value < 0 || value > INT_MAX) { return 0; } return (int)value; } FileSystemEntry *enqueue(AppState *state, FileSystemEntry *entry) { FileSystemEntry *firstEnqueuedEntry = NULL; 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)); firstEnqueuedEntry = enqueueSongs(entry, &(state->uiState)); resetListAfterDequeuingPlayingSong(state); pthread_mutex_unlock(&(playlist.mutex)); return firstEnqueuedEntry; } void playPreProcessing() { wasEndOfList = false; if (audioData.endOfListReached) wasEndOfList = true; } void playPostProcessing() { if ((songWasRemoved && currentSong != NULL)) { songWasRemoved = false; } if (wasEndOfList) { skipOutOfOrder = false; } audioData.endOfListReached = false; } void handleAddToRadioFavorites(AppState *state) { if (state->currentView == RADIOSEARCH_VIEW) { RadioSearchResult *station = getCurrentRadioSearchEntry(); if (station) { addToRadioFavorites(station); refresh = true; } } } void handleGoToSong(AppState *state) { bool canGoNext = (currentSong != NULL && currentSong->next != NULL); if (state->currentView == LIBRARY_VIEW) { enqueue(state, getCurrentLibEntry()); } else if (state->currentView == SEARCH_VIEW) { pthread_mutex_lock(&(playlist.mutex)); FileSystemEntry *entry = getCurrentSearchEntry(); setChosenDir(entry); enqueueSongs(entry, &(state->uiState)); resetListAfterDequeuingPlayingSong(state); pthread_mutex_unlock(&(playlist.mutex)); } else if (state->currentView == RADIOSEARCH_VIEW) { playRadio(); } else if (state->currentView == PLAYLIST_VIEW) { if (digitsPressedCount == 0) { if (isPaused() && currentSong != NULL && state->uiState.chosenNodeId == currentSong->id) { togglePause(&totalPauseSeconds, &pauseSeconds, &pause_time); } else { if (isRadioPlaying()) { stopRadio(); setEOFReached(); } else { cleanupPlaybackDevice(); } loadedNextSong = true; nextSongNeedsRebuilding = false; unloadSongA(state); unloadSongB(state); usingSongDataA = false; audioData.currentFileIndex = 0; loadingdata.loadA = true; playPreProcessing(); playbackPlay(&totalPauseSeconds, &pauseSeconds); Node *found = NULL; findNodeInList(&playlist, state->uiState.chosenNodeId, &found); play(found); playPostProcessing(); skipOutOfOrder = false; usingSongDataA = true; } } else { state->uiState.resetPlaylistDisplay = true; int songNumber = getSongNumber(digitsPressed); memset(digitsPressed, '\0', sizeof(digitsPressed)); digitsPressedCount = 0; nextSongNeedsRebuilding = false; skipToNumberedSong(songNumber); } } // Handle MPRIS CanGoNext bool couldGoNext = (currentSong != NULL && currentSong->next != NULL); if (canGoNext != couldGoNext) { emitBooleanPropertyChanged("CanGoNext", couldGoNext); } } void enqueueAndPlay(AppState *state) { if (state->currentView == LIBRARY_VIEW || state->currentView == SEARCH_VIEW) { if (isRadioPlaying()) { stopRadio(); setEOFReached(); } } FileSystemEntry *firstEnqueuedEntry = NULL; bool wasEmpty = (playlist.count == 0); playPreProcessing(); if (state->currentView == PLAYLIST_VIEW) { handleGoToSong(state); return; } if (state->currentView == LIBRARY_VIEW) { firstEnqueuedEntry = enqueue(state, getCurrentLibEntry()); } if (state->currentView == SEARCH_VIEW) { FileSystemEntry *entry = getCurrentSearchEntry(); firstEnqueuedEntry = enqueue(state, entry); setChosenDir(entry); } if (firstEnqueuedEntry && !wasEmpty) { Node *song = findPathInPlaylist(firstEnqueuedEntry->fullPath, &playlist); loadedNextSong = true; nextSongNeedsRebuilding = false; cleanupPlaybackDevice(); unloadSongA(state); unloadSongB(state); usingSongDataA = false; audioData.currentFileIndex = 0; loadingdata.loadA = true; playbackPlay(&totalPauseSeconds, &pauseSeconds); play(song); playPostProcessing(); skipOutOfOrder = false; usingSongDataA = true; } } void gotoBeginningOfPlaylist(AppState *state) { digitsPressed[0] = 1; digitsPressed[1] = '\0'; digitsPressedCount = 1; handleGoToSong(state); } void gotoEndOfPlaylist(AppState *state) { if (digitsPressedCount > 0) { handleGoToSong(state); } else { skipToLastSong(); } } void handleInput(AppState *state) { struct Event event = processInput(); switch (event.type) { case EVENT_GOTOBEGINNINGOFPLAYLIST: gotoBeginningOfPlaylist(state); break; case EVENT_GOTOENDOFPLAYLIST: gotoEndOfPlaylist(state); break; case EVENT_GOTOSONG: handleGoToSong(state); break; case EVENT_PLAY_PAUSE: togglePause(&totalPauseSeconds, &pauseSeconds, &pause_time); break; case EVENT_TOGGLEVISUALIZER: toggleVisualizer(&settings, &(state->uiSettings)); break; case EVENT_TOGGLEREPEAT: toggleRepeat(); break; case EVENT_TOGGLEASCII: toggleAscii(&settings, &(state->uiSettings)); break; case EVENT_SHUFFLE: toggleShuffle(); emitShuffleChanged(); break; case EVENT_TOGGLEPROFILECOLORS: toggleColors(&settings, &(state->uiSettings)); break; case EVENT_QUIT: quit(); break; case EVENT_SCROLLNEXT: scrollNext(); break; case EVENT_SCROLLPREV: scrollPrev(); break; case EVENT_VOLUME_UP: adjustVolumePercent(5); emitVolumeChanged(); break; case EVENT_VOLUME_DOWN: adjustVolumePercent(-5); emitVolumeChanged(); break; case EVENT_NEXT: state->uiState.resetPlaylistDisplay = true; skipToNextSong(state); break; case EVENT_PREV: state->uiState.resetPlaylistDisplay = true; skipToPrevSong(state); break; case EVENT_SEEKBACK: seekBack(&(state->uiState)); break; case EVENT_SEEKFORWARD: seekForward(&(state->uiState)); break; case EVENT_ADDTOMAINPLAYLIST: addToSpecialPlaylist(); break; case EVENT_EXPORTPLAYLIST: savePlaylist(settings.path); break; case EVENT_UPDATELIBRARY: updateLibrary(settings.path); break; case EVENT_SHOWKEYBINDINGS: toggleShowView(KEYBINDINGS_VIEW); break; case EVENT_SHOWPLAYLIST: toggleShowView(PLAYLIST_VIEW); break; case EVENT_SHOWSEARCH: toggleShowView(SEARCH_VIEW); break; case EVENT_SHOWRADIOSEARCH: toggleShowView(RADIOSEARCH_VIEW); break; case EVENT_SHOWLIBRARY: toggleShowView(LIBRARY_VIEW); break; case EVENT_NEXTPAGE: flipNextPage(); break; case EVENT_PREVPAGE: flipPrevPage(); break; case EVENT_REMOVE: handleRemove(); resetListAfterDequeuingPlayingSong(state); break; case EVENT_SHOWTRACK: showTrack(); break; case EVENT_NEXTVIEW: switchToNextView(); break; case EVENT_PREVVIEW: switchToPreviousView(); break; case EVENT_CLEARPLAYLIST: updatePlaylistToPlayingSong(); state->uiState.resetPlaylistDisplay = true; break; case EVENT_RADIOSEARCH: refresh = true; break; case EVENT_MOVESONGUP: moveSongUp(); break; case EVENT_MOVESONGDOWN: moveSongDown(); break; case EVENT_ENQUEUEANDPLAY: enqueueAndPlay(state); break; case EVENT_ADDTORADIOFAVORITES: handleAddToRadioFavorites(state); break; case EVENT_STOP: stop(); break; case EVENT_SORTLIBRARY: sortLibrary(); break; default: fastForwarding = false; rewinding = false; break; } } void resize(UIState *uis) { alarm(1); // Timer while (uis->resizeFlag) { uis->resizeFlag = 0; c_sleep(100); } alarm(0); // Cancel timer printf("\033[1;1H"); clearScreen(); refresh = true; } void updatePlayer(UIState *uis) { 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) { uis->resizeFlag = 1; windowSize = ws; } // resizeFlag can also be set by handleResize if (uis->resizeFlag) resize(uis); else { refreshPlayer(uis); } } void loadAudioData(AppState *state) { if (audioData.restart == true) { if (playlist.head != NULL && (waitingForPlaylist || waitingForNext)) { songLoading = true; if (waitingForPlaylist) { currentSong = playlist.head; } else if (waitingForNext) { if (songToStartFrom != NULL) { // Make sure it still exists in the playlist findNodeInList(&playlist, songToStartFrom->id, ¤tSong); songToStartFrom = NULL; } else 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; songWasRemoved = false; if (isShuffleEnabled()) reshufflePlaylist(); unloadSongA(state); unloadSongB(state); if (isRadioPlaying()) { stopRadio(); audioData.currentFileIndex = 0; loadingdata.loadA = true; usingSongDataA = false; } int res = loadFirst(currentSong, state); finishLoading(); if (res >= 0) { res = createAudioDevice(); } if (res >= 0) { resumePlayback(); } else { setEndOfListReached(state); } loadedNextSong = false; nextSong = NULL; refresh = true; clock_gettime(CLOCK_MONOTONIC, &start_time); } } else if (currentSong != NULL && (nextSongNeedsRebuilding || nextSong == NULL) && !songLoading) { loadNextSong(); determineSongAndNotify(&(state->uiSettings)); } } 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 handleSkipOutOfOrder(void) { if (!skipOutOfOrder && !isRepeatEnabled()) { setCurrentSongToNext(); } else { skipOutOfOrder = false; } } void prepareNextSong(AppState *state) { resetClock(); handleSkipOutOfOrder(); finishLoading(); nextSong = NULL; refresh = true; if (!isRepeatEnabled() || currentSong == NULL) { unloadPreviousSong(state); } if (currentSong == NULL) { if (state->uiSettings.quitAfterStopping) { quit(); } else { setEndOfListReached(state); } } else { determineSongAndNotify(&(state->uiSettings)); } } void handleSkipFromStopped() { // If we don't do this the song gets loaded in the wrong slot if (skipFromStopped) { usingSongDataA = !usingSongDataA; skipOutOfOrder = false; skipFromStopped = false; } } void updatePlayerStatus(AppState *state) { updatePlayer(&(state->uiState)); reconnectRadioIfNeeded(); if (playlist.head != NULL) { if ((skipFromStopped || !loadedNextSong || nextSongNeedsRebuilding) && !audioData.endOfListReached) { loadAudioData(state); } if (songHasErrors) tryLoadNext(); if (isPlaybackDone()) { updateLastSongSwitchTime(); prepareNextSong(state); switchAudioImplementation(); } } else { setEOFNotReached(); } } void processDBusEvents(void) { while (g_main_context_pending(global_main_context)) { g_main_context_iteration(global_main_context, FALSE); } } gboolean mainloop_callback(gpointer data) { (void)data; calcElapsedTime(); handleInput(&appState); updateCounter++; // Different views run at different speeds to lower the impact on system requirements if ((updateCounter % 4 == 0 && (appState.currentView == SEARCH_VIEW || appState.currentView == RADIOSEARCH_VIEW)) || (appState.currentView == TRACK_VIEW || appState.uiState.miniMode) || updateCounter % 6 == 0) { processDBusEvents(); updatePlayerStatus(&appState); } return TRUE; } static gboolean quitOnSignal(gpointer user_data) { GMainLoop *loop = (GMainLoop *)user_data; g_main_loop_quit(loop); quit(); return G_SOURCE_REMOVE; // Remove the signal source } void initFirstPlay(Node *song, AppState *state) { updateLastInputTime(); updateLastSongSwitchTime(); userData.currentSongData = NULL; userData.songdataA = NULL; userData.songdataB = NULL; userData.songdataADeleted = true; userData.songdataBDeleted = true; int res = 0; if (song != NULL) { audioData.currentFileIndex = 0; loadingdata.loadA = true; res = loadFirst(song, state); if (res >= 0) { res = createAudioDevice(); } if (res >= 0) { resumePlayback(); } if (res < 0) setEndOfListReached(state); } 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, quitOnSignal, main_loop); g_unix_signal_add(SIGHUP, quitOnSignal, main_loop); if (song != NULL) emitStartPlayingMpris(); else emitPlaybackStoppedMpris(); g_timeout_add(17, mainloop_callback, NULL); g_main_loop_run(main_loop); g_main_loop_unref(main_loop); } void cleanupOnExit() { stopRadio(); pthread_mutex_lock(&dataSourceMutex); resetAllDecoders(); if (isContextInitialized) { cleanupPlaybackDevice(); cleanupAudioContext(); } emitPlaybackStoppedMpris(); bool noMusicFound = false; FileSystemEntry *library = getLibrary(); if (library == NULL || library->children == NULL) { noMusicFound = true; } if (!userData.songdataADeleted) { userData.songdataADeleted = true; unloadSongData(&(loadingdata.songdataA), &appState); } if (!userData.songdataBDeleted) { userData.songdataBDeleted = true; unloadSongData(&(loadingdata.songdataB), &appState); } freeSearchResults(); freeRadioSearchResults(); freeCurrentlyPlayingRadioStation(); curl_global_cleanup(); cleanupMpris(); restoreTerminalMode(); enableInputBuffering(); setConfig(&settings, &(appState.uiSettings)); saveSpecialPlaylist(settings.path); deleteCache(appState.tmpCache); freeMainDirectoryTree(&appState); freeAndwriteRadioFavorites(); deletePlaylist(&playlist); deletePlaylist(originalPlaylist); deletePlaylist(specialPlaylist); free(specialPlaylist); free(originalPlaylist); setDefaultTextColor(); pthread_mutex_destroy(&(loadingdata.mutex)); pthread_mutex_destroy(&(playlist.mutex)); pthread_mutex_destroy(&(switchMutex)); pthread_mutex_unlock(&dataSourceMutex); pthread_mutex_destroy(&(dataSourceMutex)); destroyRadioMutexes(); freeVisuals(); freeLastCover(); #ifdef USE_DBUS cleanupDbusConnection(); #endif #ifdef DEBUG fclose(logFile); #endif if (freopen("/dev/stderr", "w", stderr) == NULL) { perror("freopen error"); } printf("\n"); showCursor(); exitAlternateScreenBuffer(); if (appState.uiSettings.mouseEnabled) disableTerminalMouseButtons(); if (noMusicFound) { 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"); } else if (noPlaylist) { printf("Music not found.\n"); } fflush(stdout); } void run(AppState *state) { if (originalPlaylist == NULL) { originalPlaylist = malloc(sizeof(PlayList)); *originalPlaylist = deepCopyPlayList(&playlist); } if (playlist.head == NULL) { state->currentView = LIBRARY_VIEW; } initMpris(); currentSong = playlist.head; initFirstPlay(currentSong, state); clearScreen(); fflush(stdout); } void handleResize(int sig) { (void)sig; appState.uiState.resizeFlag = 1; } void resetResizeFlag(int sig) { (void)sig; appState.uiState.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 init(AppState *state) { disableInputBuffering(); initResize(); ioctl(STDOUT_FILENO, TIOCGWINSZ, &windowSize); enableScrolling(); setNonblockingMode(); state->tmpCache = 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; unsigned int seed = (unsigned int)time(NULL); srand(seed); pthread_mutex_init(&dataSourceMutex, NULL); pthread_mutex_init(&switchMutex, NULL); pthread_mutex_init(&(loadingdata.mutex), NULL); pthread_mutex_init(&(playlist.mutex), NULL); initRadioMutexes(); createLibrary(&settings, state); createRadioFavorites(); curl_global_init(CURL_GLOBAL_DEFAULT); setlocale(LC_CTYPE, ""); 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 *state) { state->currentView = LIBRARY_VIEW; init(state); playlist.head = NULL; run(state); } void playSpecialPlaylist(AppState *state) { 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); } init(state); deepCopyPlayListOntoList(specialPlaylist, &playlist); shufflePlaylist(&playlist); run(state); } void playAll(AppState *state) { init(state); FileSystemEntry *library = getLibrary(); createPlayListFromFileSystemEntry(library, &playlist, MAX_FILES); if (playlist.count == 0) { exit(0); } shufflePlaylist(&playlist); run(state); } void playAllAlbums(AppState *state) { init(state); FileSystemEntry *library = getLibrary(); addShuffledAlbumsToPlayList(library, &playlist, MAX_FILES); if (playlist.count == 0) { exit(0); } run(state); } 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[], UISettings *ui) { 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 maxLen = 1000; int idx = -1; for (int i = 0; i < *argc; i++) { if (c_strcasestr(argv[i], noUiOption, maxLen)) { ui->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, maxLen)) { ui->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, maxLen) || c_strcasestr(argv[i], quitOnStop2, maxLen)) { ui->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, maxLen) || c_strcasestr(argv[i], exactOption2, maxLen)) { exactSearch = true; idx = i; } } if (idx >= 0) removeArgElement(argv, idx, argc); } /* * Checks if a process with the given PID is running * * Returns: * 1 if the process is running, 0 otherwise. */ int isProcessRunning(pid_t pid) { if (pid <= 0) { return 0; // Invalid PID } // Send signal 0 to check if the process exists if (kill(pid, 0) == 0) { return 1; // Process exists } // Check errno for detailed status if (errno == ESRCH) { return 0; // No such process } else if (errno == EPERM) { return 1; // Process exists but we don't have permission } return 0; // Other errors } // Ensures only a single instance of kew can run at a time for the current user. void exitIfAlreadyRunning() { char pidfile_path[256]; snprintf(pidfile_path, sizeof(pidfile_path), "%s%d.pid", TMPPIDFILE, getuid()); FILE *pidfile; pid_t pid; pidfile = fopen(pidfile_path, "r"); if (pidfile != NULL) { if (fscanf(pidfile, "%d", &pid) == 1) { fclose(pidfile); if (isProcessRunning(pid)) { fprintf(stderr, "An instance of kew is already running. Pid: %d. Type 'kill %d' to remove it.\n", pid, pid); exit(1); } else { unlink(pidfile_path); } } else { fclose(pidfile); unlink(pidfile_path); } } // Create a new PID file pidfile = fopen(pidfile_path, "w"); if (pidfile == NULL) { perror("Unable to create PID file"); exit(1); } fprintf(pidfile, "%d\n", getpid()); fclose(pidfile); } int directoryExists(const char *path) { DIR *dir = opendir(path); if (dir != NULL) { closedir(dir); return 1; } return 0; } void clearInputBuffer() { int c; while ((c = getchar()) != '\n' && c != EOF) ; } void setMusicPath() { struct passwd *pw = getpwuid(getuid()); char *user = NULL; clearScreen(); if (pw) { user = pw->pw_name; } else { printf("Error: Could not retrieve user information.\n"); printf("Please set a path to your music library.\n"); printf("To set it, type: kew path \"/path/to/Music\".\n"); exit(1); } // Music folder names in different languages const char *musicFolderNames[] = { "Music", "Música", "Musique", "Musik", "Musica", "Muziek", "Музыка", "音乐", "音楽", "음악", "موسيقى", "संगीत", "Müzik", "Musikk", "Μουσική", "Muzyka", "Hudba", "Musiikki", "Zene", "Muzică", "เพลง", "מוזיקה"}; char path[PATH_MAX]; int found = 0; int result = -1; char choice[2]; for (size_t i = 0; i < sizeof(musicFolderNames) / sizeof(musicFolderNames[0]); i++) { #ifdef __APPLE__ snprintf(path, sizeof(path), "/Users/%s/%s", user, musicFolderNames[i]); #else snprintf(path, sizeof(path), "/home/%s/%s", user, musicFolderNames[i]); #endif if (directoryExists(path)) { found = 1; printf("Do you want to use %s as your music library folder?\n", path); printf("y = Yes\nn = Enter a path\n"); result = scanf("%1s", choice); if (choice[0] == 'y' || choice[0] == 'Y') { c_strcpy(settings.path, path, sizeof(settings.path)); return; } else if (choice[0] == 'n' || choice[0] == 'N') { break; } else { printf("Invalid choice. Please try again.\n"); i--; } } } if (!found || (found && (choice[0] == 'n' || choice[0] == 'N'))) { printf("Please enter the path to your music library (/path/to/Music):\n"); clearInputBuffer(); if (fgets(path, sizeof(path), stdin) == NULL) { printf("Error reading input.\n"); exit(1); } path[strcspn(path, "\n")] = '\0'; if (directoryExists(path)) { c_strcpy(settings.path, path, sizeof(settings.path)); } else { printf("The entered path does not exist or is inaccessible.\n"); exit(1); } } if (result == -1) exit(1); } void enableMouse(UISettings *ui) { if (ui->mouseEnabled) { enableTerminalMouseButtons(); } } void initState(AppState *state) { state->uiSettings.uiEnabled = true; state->uiSettings.color.r = 125; state->uiSettings.color.g = 125; state->uiSettings.color.b = 125; state->uiSettings.tweenFactor = 0.23f; state->uiSettings.tweenFactorFall = 0.13f; state->uiSettings.visualizerEnabled = true; state->uiSettings.coverEnabled = true; state->uiSettings.hideLogo = false; state->uiSettings.hideHelp = false; state->uiSettings.quitAfterStopping = false; state->uiSettings.hideGlimmeringText = false; state->uiSettings.coverAnsi = false; state->uiSettings.visualizerHeight = 5; state->uiSettings.visualizerColorType = 0; state->uiSettings.titleDelay = 9; state->uiSettings.visualizerBrailleMode = false; state->uiSettings.cacheLibrary = -1; state->uiSettings.useConfigColors = false; state->uiSettings.mouseEnabled = true; state->uiSettings.mouseLeftClickAction = 0; state->uiSettings.mouseMiddleClickAction = 1; state->uiSettings.mouseRightClickAction = 2; state->uiSettings.mouseScrollUpAction = 3; state->uiSettings.mouseScrollDownAction = 4; state->uiSettings.mouseAltScrollUpAction = 7; state->uiSettings.mouseAltScrollDownAction = 8; state->uiSettings.progressBarType = 0; state->uiState.numDirectoryTreeEntries = 0; state->uiState.numProgressBars = 35; state->uiState.chosenNodeId = 0; state->uiState.resetPlaylistDisplay = true; state->uiState.allowChooseSongs = false; state->uiState.openedSubDir = false; state->uiState.numSongsAboveSubDir = 0; state->uiState.resizeFlag = 0; state->uiState.doNotifyMPRISSwitched = false; state->uiState.doNotifyMPRISPlaying = false; state->uiState.collapseView = false; state->tmpCache = NULL; radioContext.buf.stale = false; } void initializeStateAndSettings(AppState *appState, AppSettings *settings) { initState(appState); getConfig(settings, &(appState->uiSettings)); mapSettingsToKeys(settings, &(appState->uiSettings), keyMappings); enableMouse(&(appState->uiSettings)); } int main(int argc, char *argv[]) { UISettings *ui = &(appState.uiSettings); exitIfAlreadyRunning(); 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, ui); exit(0); } initializeStateAndSettings(&appState, &settings); if (argc == 3 && (strcmp(argv[1], "path") == 0)) { c_strcpy(settings.path, argv[2], sizeof(settings.path)); setConfig(&settings, ui); exit(0); } enterAlternateScreenBuffer(); atexit(cleanupOnExit); if (settings.path[0] == '\0') { setMusicPath(); } handleOptions(&argc, argv, ui); loadSpecialPlaylist(settings.path); if (argc == 1) { openLibrary(&appState); } else if (argc == 2 && strcmp(argv[1], "all") == 0) { playAll(&appState); } else if (argc == 2 && strcmp(argv[1], "albums") == 0) { playAllAlbums(&appState); } else if (argc == 2 && strcmp(argv[1], ".") == 0 && specialPlaylist->count != 0) { playSpecialPlaylist(&appState); } else if (argc >= 2) { init(&appState); makePlaylist(argc, argv, exactSearch, settings.path); if (playlist.count == 0) { noPlaylist = true; exit(0); } run(&appState); } return 0; } kew-3.2.0/src/m4a.c000066400000000000000000000006371500206121000137070ustar00rootroot00000000000000 /* This implements a data source that handles m4a streams via minimp4 and faad2 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. */ #define MINIMP4_IMPLEMENTATION #include "../include/minimp4/minimp4.h" #include #ifdef USE_FAAD #include "m4a.h" #endif kew-3.2.0/src/m4a.h000066400000000000000000001661721500206121000137230ustar00rootroot00000000000000 /* 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 "neaacdec.h" #include "../include/minimp4/minimp4.h" #include "../include/alac/codec/alac_wrapper.h" #include "../include/alac/codec/EndianPortable.h" #include #include #include #include #include "common.h" typedef enum { k_unknown = 0, k_aac = 1, k_rawAAC = 2, // Raw aac (.aac file) decoding is included here for convenience although they are not .m4a files k_ALAC = 3, k_FLAC = 4 } k_m4adec_filetype; 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; // faad2 related fields... NeAACDecHandle hDecoder; NeAACDecFrameInfo frameInfo; unsigned char *buffer; unsigned int buffer_size; ma_uint32 sampleSize; int bitDepth; ma_uint32 sampleRate; ma_uint32 channels; ma_uint32 avgBitRate; double duration; unsigned long totalFrames; k_m4adec_filetype fileType; alac_decoder_t *alacDecoder; // minimp4 fields... MP4D_demux_t mp4; MP4D_track_t *track; int32_t audio_track_index; uint32_t current_sample; uint32_t total_samples; // For m4a_decoder_init_file FILE *file; ma_uint64 cursor; } m4a_decoder; #define FOUR_CHAR_INT(a, b, c, d) (((uint32_t)(a) << 24) | ((b) << 16) | ((c) << 8) | (d)) 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); 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); extern ma_result m4a_decoder_ds_read(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead); extern ma_result m4a_decoder_ds_seek(ma_data_source *pDataSource, ma_uint64 frameIndex); extern ma_result m4a_decoder_ds_get_cursor(ma_data_source *pDataSource, ma_uint64 *pCursor); extern ma_result m4a_decoder_ds_get_length(ma_data_source *pDataSource, ma_uint64 *pLength); #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; 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}; static ma_result file_on_read(void *pUserData, void *pBufferOut, size_t bytesToRead, size_t *pBytesRead) { FILE *fp = (FILE *)pUserData; size_t bytesRead = fread(pBufferOut, 1, bytesToRead, fp); if (bytesRead < bytesToRead && ferror(fp)) { return MA_ERROR; } if (pBytesRead) { *pBytesRead = bytesRead; } return MA_SUCCESS; } static ma_result file_on_seek(void *pUserData, ma_int64 offset, ma_seek_origin origin) { FILE *fp = (FILE *)pUserData; int whence = (origin == ma_seek_origin_start) ? SEEK_SET : SEEK_CUR; if (fseeko(fp, offset, whence) != 0) { return MA_ERROR; } return MA_SUCCESS; } static int minimp4_read_callback(int64_t offset, void *buffer, size_t size, void *token) { m4a_decoder *pM4a = (m4a_decoder *)token; // Cast int64_t to ma_int64 for onSeek ma_int64 ma_offset = (ma_int64)offset; if (file_on_seek(pM4a->file, ma_offset, ma_seek_origin_start) != MA_SUCCESS) { return 1; // Error } size_t bytesRead = 0; if (file_on_read(pM4a->file, buffer, size, &bytesRead) != MA_SUCCESS || bytesRead != size) { return 1; // Error } return 0; // Success } int64_t minimp4_seek_callback(void *user_data, int64_t offset) { m4a_decoder *pM4a = (m4a_decoder *)user_data; ma_result result = file_on_seek(pM4a->file, offset, ma_seek_origin_start); if (result != MA_SUCCESS) { return -1; // Signal error } return offset; // Return the new position if possible } 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; } // 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)pAllocationCallbacks; if (pM4a == NULL || onRead == NULL || onSeek == NULL || onTell == NULL) { return MA_INVALID_ARGS; } ma_result result = m4a_decoder_init_internal(pConfig, pM4a); if (result != MA_SUCCESS) { return result; } // Store the custom read, seek, and tell functions pM4a->pReadSeekTellUserData = pReadSeekTellUserData; // Get the size of the data source ma_int64 currentPos = 0; if (pM4a->onTell(pM4a->pReadSeekTellUserData, ¤tPos) != MA_SUCCESS) { return MA_ERROR; } if (pM4a->onSeek(pM4a->pReadSeekTellUserData, 0, ma_seek_origin_end) != MA_SUCCESS) { return MA_ERROR; } ma_int64 fileSize = 0; if (pM4a->onTell(pM4a->pReadSeekTellUserData, &fileSize) != MA_SUCCESS) { return MA_ERROR; } // Seek back to original position if (pM4a->onSeek(pM4a->pReadSeekTellUserData, currentPos, ma_seek_origin_start) != MA_SUCCESS) { return MA_ERROR; } // Initialize minimp4 with custom read_callback if (MP4D_open(&pM4a->mp4, minimp4_read_callback, pM4a, fileSize) != 0) { return MA_ERROR; } // Find the audio track pM4a->audio_track_index = -1; for (unsigned int i = 0; i < pM4a->mp4.track_count; i++) { MP4D_track_t *track = &pM4a->mp4.track[i]; if (track->handler_type == MP4D_HANDLER_TYPE_SOUN) { pM4a->audio_track_index = i; pM4a->track = track; break; } } if (pM4a->audio_track_index == -1) { // No audio track found MP4D_close(&pM4a->mp4); return MA_ERROR; } pM4a->current_sample = 0; pM4a->total_samples = pM4a->track->sample_count; // Initialize faad2 decoder pM4a->hDecoder = NeAACDecOpen(); // Extract the decoder configuration const uint8_t *decoder_config = pM4a->track->dsi; uint32_t decoder_config_len = pM4a->track->dsi_bytes; unsigned long sampleRate; unsigned char channels; if (NeAACDecInit2(pM4a->hDecoder, (unsigned char *)decoder_config, decoder_config_len, &sampleRate, &channels) < 0) { // Error initializing decoder NeAACDecClose(pM4a->hDecoder); MP4D_close(&pM4a->mp4); return MA_ERROR; } // Configure output format NeAACDecConfigurationPtr config = NeAACDecGetCurrentConfiguration(pM4a->hDecoder); if (pM4a->format == ma_format_s16) { config->outputFormat = FAAD_FMT_16BIT; pM4a->sampleSize = sizeof(int16_t); pM4a->bitDepth = 16; } else if (pM4a->format == ma_format_f32) { config->outputFormat = FAAD_FMT_FLOAT; pM4a->sampleSize = sizeof(float); pM4a->bitDepth = 32; } else { // Unsupported format NeAACDecClose(pM4a->hDecoder); MP4D_close(&pM4a->mp4); return MA_ERROR; } NeAACDecSetConfiguration(pM4a->hDecoder, config); // Initialize other fields leftoverSampleCount = 0; pM4a->cursor = 0; return MA_SUCCESS; } double calculate_aac_duration(FILE *fp, unsigned long sampleRate, unsigned long *totalFrames) { if (fp == NULL || sampleRate == 0 || totalFrames == NULL) { return -1.0; } unsigned char buffer[7]; unsigned long fileSize = 0; *totalFrames = 0; // Get file size fseek(fp, 0, SEEK_END); fileSize = ftell(fp); fseek(fp, 0, SEEK_SET); // Loop to count frames while (ftell(fp) < (long)(fileSize - 7)) // Ensure at least an ADTS header remains { // Read header if (fread(buffer, 1, 7, fp) < 7) break; // Extract frame size unsigned int frameSize = ((buffer[3] & 0x03) << 11) | ((buffer[4] & 0xFF) << 3) | ((buffer[5] & 0xE0) >> 5); if (frameSize <= 7) break; // Skip to next frame fseek(fp, frameSize - 7, SEEK_CUR); (*totalFrames)++; } // Compute duration using: duration = (totalFrames * 1024) / sampleRate double duration = (double)(*totalFrames * 1024) / sampleRate; fseek(fp, 0, SEEK_SET); return duration; } uint32_t read_u32be(FILE *fp) { unsigned char b[4]; if (fread(b, 1, 4, fp) != 4) return 0; return ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) | ((uint32_t)b[2] << 8) | ((uint32_t)b[3]); } int find_atom(FILE *fp, uint32_t atom_name, long max_search_length, uint32_t *atom_size_out) { long start_pos = ftell(fp); while ((ftell(fp) - start_pos) < max_search_length) { unsigned char header[8]; if (fread(header, 1, 8, fp) != 8) return 0; uint32_t atom_size = (header[0] << 24) | (header[1] << 16) | (header[2] << 8) | header[3]; uint32_t atom_type = (header[4] << 24) | (header[5] << 16) | (header[6] << 8) | header[7]; if (atom_size < 8) return 0; // Invalid atom size if (atom_type == atom_name) { if (atom_size_out) *atom_size_out = atom_size - 8; return 1; // Found } if (fseek(fp, atom_size - 8, SEEK_CUR) != 0) return 0; } return 0; // Not found } static uint32_t read_u32be_ptr(const uint8_t *b) { return ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) | ((uint32_t)b[2] << 8) | b[3]; } static uint16_t read_u16be_ptr(const uint8_t *b) { return ((uint16_t)b[0] << 8) | b[1]; } int parse_alac_config(const uint8_t *dsi, size_t dsi_size, ALACSpecificConfig *cfg) { if (dsi_size < 24) return -1; cfg->frameLength = read_u32be_ptr(dsi); cfg->compatibleVersion = dsi[4]; cfg->bitDepth = dsi[5]; cfg->pb = dsi[6]; cfg->mb = dsi[7]; cfg->kb = dsi[8]; cfg->numChannels = dsi[9]; cfg->maxRun = read_u16be_ptr(dsi + 10); cfg->maxFrameBytes = read_u32be_ptr(dsi + 12); cfg->avgBitRate = read_u32be_ptr(dsi + 16); cfg->sampleRate = read_u32be_ptr(dsi + 20); return 0; } int is_alac(FILE *fp, uint8_t *dsi_out, size_t *dsi_size_out) { fseek(fp, 0, SEEK_SET); uint32_t atom_size; if (!find_atom(fp, FOUR_CHAR_INT('m', 'o', 'o', 'v'), 0x7FFFFFFF, &atom_size)) return 0; if (!find_atom(fp, FOUR_CHAR_INT('t', 'r', 'a', 'k'), atom_size, &atom_size)) return 0; if (!find_atom(fp, FOUR_CHAR_INT('m', 'd', 'i', 'a'), atom_size, &atom_size)) return 0; if (!find_atom(fp, FOUR_CHAR_INT('m', 'i', 'n', 'f'), atom_size, &atom_size)) return 0; if (!find_atom(fp, FOUR_CHAR_INT('s', 't', 'b', 'l'), atom_size, &atom_size)) return 0; if (!find_atom(fp, FOUR_CHAR_INT('s', 't', 's', 'd'), atom_size, &atom_size)) return 0; fseek(fp, 8, SEEK_CUR); // Skip stsd header (version+entry) read_u32be(fp); // uint32_t sample_entry_size uint32_t sample_entry_fourcc = read_u32be(fp); if (sample_entry_fourcc != FOUR_CHAR_INT('a', 'l', 'a', 'c')) return 0; fseek(fp, 28, SEEK_CUR); // Skip audio sample entry fields uint32_t config_atom_size = read_u32be(fp); uint32_t config_atom_fourcc = read_u32be(fp); if (config_atom_fourcc != FOUR_CHAR_INT('a', 'l', 'a', 'c')) return 0; fseek(fp, 4, SEEK_CUR); // Skip 1-byte version and 3-byte flags (4 bytes total)! uint32_t alac_dsi_size = config_atom_size - 12; // size(4)+fourcc(4)+version/flags(4) total=12 bytes overhead if (alac_dsi_size < 24 || alac_dsi_size > 64) return 0; // Sanity check if (fread(dsi_out, 1, alac_dsi_size, fp) != alac_dsi_size) return 0; *dsi_size_out = alac_dsi_size; return 1; } 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; } FILE *fp = fopen(pFilePath, "rb"); if (fp == NULL) { return MA_INVALID_FILE; } // Get the file size if (fseeko(fp, 0, SEEK_END) != 0) { fclose(fp); return MA_ERROR; } ma_int64 fileSize = ftello(fp); if (fileSize < 0) { fclose(fp); return MA_ERROR; } if (fseeko(fp, 0, SEEK_SET) != 0) { fclose(fp); return MA_ERROR; } // Store the FILE pointer in the decoder struct pM4a->file = fp; // Try to detect the file format (ADTS, MP4, LATM, etc.) unsigned char buffer[7]; size_t bytesRead = fread(buffer, 1, sizeof(buffer), fp); // Check for ADTS header if (bytesRead >= 7 && buffer[0] == 0xFF && (buffer[1] & 0xF0) == 0xF0) { pM4a->fileType = k_rawAAC; } else { // Check if it's an MP4 file (using MP4D_open or similar) if (MP4D_open(&pM4a->mp4, minimp4_read_callback, pM4a, fileSize) == 1) { pM4a->fileType = k_unknown; // It's an MP4 container } else { fclose(fp); return MA_ERROR; // Unknown format } } if (pM4a->fileType == k_rawAAC) { // Raw AAC handling // Extract the frame size from the ADTS header unsigned int frameSize = ((buffer[3] & 0x03) << 11) | ((buffer[4] & 0xFF) << 3) | ((buffer[5] & 0xE0) >> 5); if (frameSize <= 7) { fclose(fp); return MA_ERROR; // Invalid frame size } unsigned char *frameData = malloc(frameSize); if (frameData == NULL) { fclose(fp); return MA_ERROR; // Memory allocation failed } // The first 7 bytes are already in the buffer, so copy them to frameData memcpy(frameData, buffer, 7); // Read the rest of the frame (audio data) size_t remainingBytes = frameSize - 7; size_t additionalBytesRead = fread(frameData + 7, 1, remainingBytes, fp); if (additionalBytesRead < remainingBytes) { free(frameData); fclose(fp); return MA_ERROR; // Failed to read the full frame } // Allocate decoder config unsigned char *decoder_config = malloc(2); if (decoder_config == NULL) { return MA_OUT_OF_MEMORY; } unsigned long decoder_config_size = 2; decoder_config[0] = ((buffer[2] & 0xC0) >> 6) + 1; // Object type decoder_config[0] |= ((buffer[2] & 0x3C) >> 2) << 2; decoder_config[1] = ((buffer[2] & 0x07) << 1) | ((buffer[3] & 0x80) >> 7); // Channels and sampleRate decoder_config[1] <<= 4; // Shift to upper 4 bits unsigned char objectType = decoder_config[0]; if (objectType == 5 || objectType >= 29) { setErrorMessage("File is encoded with HE-AAC which is not supported"); free(frameData); free(decoder_config); fclose(fp); return MA_ERROR; } unsigned long sampleRate = 0; unsigned char channels = 0; pM4a->hDecoder = NeAACDecOpen(); int initResult = NeAACDecInit2(pM4a->hDecoder, (unsigned char *)decoder_config, decoder_config_size, &sampleRate, &channels); if (initResult < 0) { printf("Error initializing decoder. Code: %d\n", initResult); free(frameData); free(decoder_config); NeAACDecClose(pM4a->hDecoder); fclose(fp); return MA_ERROR; } free(decoder_config); // Check if the sampleRate and channels are correctly initialized if (sampleRate == 0 || channels == 0) { printf("Error: Invalid sample rate or channel count.\n"); free(frameData); NeAACDecClose(pM4a->hDecoder); fclose(fp); return MA_ERROR; } pM4a->sampleRate = (ma_uint32)sampleRate; pM4a->channels = (ma_uint32)channels; pM4a->duration = calculate_aac_duration(fp, pM4a->sampleRate, &pM4a->totalFrames); // Clean up the frame data after processing free(frameData); // Configure output format NeAACDecConfigurationPtr config_ptr = NeAACDecGetCurrentConfiguration(pM4a->hDecoder); if (pM4a->format == ma_format_s16) { config_ptr->outputFormat = FAAD_FMT_16BIT; pM4a->sampleSize = sizeof(int16_t); pM4a->bitDepth = 16; } else if (pM4a->format == ma_format_f32) { config_ptr->outputFormat = FAAD_FMT_FLOAT; pM4a->sampleSize = sizeof(float); pM4a->bitDepth = 32; } else { // Unsupported format NeAACDecClose(pM4a->hDecoder); fclose(fp); return MA_ERROR; } NeAACDecSetConfiguration(pM4a->hDecoder, config_ptr); // Initialize other fields leftoverSampleCount = 0; pM4a->cursor = 0; fseek(pM4a->file, 0, SEEK_SET); return MA_SUCCESS; } else { // Find the audio track pM4a->audio_track_index = -1; for (unsigned int i = 0; i < pM4a->mp4.track_count; i++) { MP4D_track_t *track = &pM4a->mp4.track[i]; if (track->handler_type == MP4D_HANDLER_TYPE_SOUN) { pM4a->audio_track_index = i; pM4a->track = track; break; } } // M4A (MP4-wrapped AAC) handling if (pM4a->audio_track_index == -1) { // No audio track found MP4D_close(&pM4a->mp4); fclose(fp); return MA_ERROR; } pM4a->current_sample = 0; pM4a->total_samples = pM4a->track->sample_count; pM4a->avgBitRate = pM4a->track->avg_bitrate_bps; uint8_t alac_dsi[32]; size_t alac_dsi_size; long original_position = ftell(pM4a->file); if (is_alac(fp, alac_dsi, &alac_dsi_size)) { ALACSpecificConfig alacConfig; if (parse_alac_config(alac_dsi, alac_dsi_size, &alacConfig) != 0) { MP4D_close(&pM4a->mp4); fclose(fp); return MA_ERROR; } pM4a->bitDepth = alacConfig.bitDepth; pM4a->sampleRate = (ma_uint32)alacConfig.sampleRate; pM4a->channels = (ma_uint32)alacConfig.numChannels; pM4a->avgBitRate = (ma_uint32)alacConfig.avgBitRate; if (pM4a->bitDepth == 32) { pM4a->format = ma_format_s32; pM4a->sampleSize = 4; } else if (pM4a->bitDepth == 24) { pM4a->format = ma_format_s24; pM4a->sampleSize = 3; } else if (pM4a->bitDepth == 16) { pM4a->format = ma_format_s16; pM4a->sampleSize = sizeof(int16_t); } else { // Unsupported format MP4D_close(&pM4a->mp4); fclose(fp); return MA_ERROR; } pM4a->alacDecoder = alac_decoder_init_from_config(&alacConfig); if (!pM4a->alacDecoder) { MP4D_close(&pM4a->mp4); fclose(fp); return MA_ERROR; } leftoverSampleCount = 0; pM4a->cursor = 0; pM4a->fileType = k_ALAC; fseek(pM4a->file, original_position, SEEK_SET); return MA_SUCCESS; } else // AAC { pM4a->fileType = k_aac; fseek(pM4a->file, original_position, SEEK_SET); // Initialize faad2 decoder pM4a->hDecoder = NeAACDecOpen(); // Extract the decoder configuration const uint8_t *decoder_config = pM4a->track->dsi; uint32_t decoder_config_len = pM4a->track->dsi_bytes; unsigned long sampleRate; unsigned char channels; if (NeAACDecInit2(pM4a->hDecoder, (unsigned char *)decoder_config, decoder_config_len, &sampleRate, &channels) < 0) { // Error initializing decoder NeAACDecClose(pM4a->hDecoder); MP4D_close(&pM4a->mp4); fclose(fp); return MA_ERROR; } pM4a->sampleRate = (ma_uint32)sampleRate; pM4a->channels = (ma_uint32)channels; // Configure output format NeAACDecConfigurationPtr config_ptr = NeAACDecGetCurrentConfiguration(pM4a->hDecoder); if (pM4a->format == ma_format_s16) { config_ptr->outputFormat = FAAD_FMT_16BIT; pM4a->sampleSize = sizeof(int16_t); pM4a->bitDepth = 16; } else if (pM4a->format == ma_format_f32) { config_ptr->outputFormat = FAAD_FMT_FLOAT; pM4a->sampleSize = sizeof(float); pM4a->bitDepth = 32; } else { // Unsupported format NeAACDecClose(pM4a->hDecoder); MP4D_close(&pM4a->mp4); fclose(fp); return MA_ERROR; } NeAACDecSetConfiguration(pM4a->hDecoder, config_ptr); // Initialize other fields leftoverSampleCount = 0; pM4a->cursor = 0; return MA_SUCCESS; } } } MA_API void m4a_decoder_uninit(m4a_decoder *pM4a, const ma_allocation_callbacks *pAllocationCallbacks) { (void)pAllocationCallbacks; if (pM4a == NULL) { return; } if (pM4a->hDecoder) { NeAACDecClose(pM4a->hDecoder); pM4a->hDecoder = NULL; } if (pM4a->fileType != k_rawAAC) { MP4D_close(&pM4a->mp4); } else if (pM4a->fileType == k_ALAC) { alac_decoder_free(pM4a->alacDecoder); pM4a->alacDecoder = NULL; } if (pM4a->file) { fclose(pM4a->file); pM4a->file = NULL; } } MA_API ma_result m4a_decoder_read_pcm_frames( m4a_decoder *pM4a, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { if (pM4a == NULL || pFramesOut == NULL || frameCount == 0) { return MA_INVALID_ARGS; } ma_result result = MA_SUCCESS; ma_uint32 channels = pM4a->channels; ma_uint32 sampleSize = pM4a->sampleSize; ma_uint64 totalFramesProcessed = 0; // Handle any leftover samples from previous call using the global/static leftover buffer if (leftoverSampleCount > 0) { ma_uint64 leftoverToProcess = (leftoverSampleCount < frameCount) ? leftoverSampleCount : frameCount; ma_uint64 leftoverBytes = leftoverToProcess * channels * sampleSize; memcpy(pFramesOut, leftoverBuffer, leftoverBytes); totalFramesProcessed += leftoverToProcess; // Shift the leftover buffer ma_uint64 samplesLeft = leftoverSampleCount - leftoverToProcess; if (samplesLeft > 0) { memmove(leftoverBuffer, leftoverBuffer + leftoverBytes, samplesLeft * channels * sampleSize); } leftoverSampleCount = samplesLeft; } while (totalFramesProcessed < frameCount) { if (pM4a->fileType == k_rawAAC) { unsigned int headerSize = 7; uint8_t buffer[headerSize]; if (fread(buffer, 1, headerSize, pM4a->file) != headerSize) { result = MA_ERROR; break; } unsigned int frame_bytes = ((buffer[3] & 0x03) << 11) | ((buffer[4] & 0xFF) << 3) | ((buffer[5] & 0xE0) >> 5); if (frame_bytes < headerSize || frame_bytes > 8192) { result = MA_ERROR; break; } // Allocate memory for the frame unsigned char *sample_data = (unsigned char *)malloc(frame_bytes); if (!sample_data) { result = MA_OUT_OF_MEMORY; break; } // Copy the header to the sample_data buffer memcpy(sample_data, buffer, headerSize); // Read the rest of the frame (audio data) size_t remaining_bytes = frame_bytes - headerSize; size_t additionalBytesRead = fread(sample_data + headerSize, 1, remaining_bytes, pM4a->file); if (additionalBytesRead < remaining_bytes) { free(sample_data); result = MA_ERROR; break; // Failed to read full frame } pM4a->current_sample++; // Decode the AAC frame using faad2 void *decodedData = NeAACDecDecode(pM4a->hDecoder, &(pM4a->frameInfo), sample_data + 7, frame_bytes - 7); free(sample_data); if (pM4a->frameInfo.error > 0) { // Error in decoding, skip to the next frame. continue; } // Remove support for HE-AAC components (SBR or PS) if (pM4a->frameInfo.sbr || pM4a->frameInfo.ps) { // File is encoded with HE-AAC which is not supported return MA_ERROR; } unsigned long samplesDecoded = pM4a->frameInfo.samples; // Total samples decoded (channels * frames) ma_uint64 framesDecoded = samplesDecoded / channels; // Calculate how many frames we can process in this call ma_uint64 framesNeeded = frameCount - totalFramesProcessed; ma_uint64 framesToCopy = (framesDecoded < framesNeeded) ? framesDecoded : framesNeeded; ma_uint64 bytesToCopy = framesToCopy * channels * sampleSize; memcpy((uint8_t *)pFramesOut + totalFramesProcessed * channels * sampleSize, decodedData, bytesToCopy); totalFramesProcessed += framesToCopy; // Handle leftover frames using the global/static leftover buffer if (framesToCopy < framesDecoded) { // There are leftover frames leftoverSampleCount = framesDecoded - framesToCopy; ma_uint64 leftoverBytes = leftoverSampleCount * channels * sampleSize; if (leftoverBytes > sizeof(leftoverBuffer)) { // Safety check to avoid overflow in the buffer. leftoverSampleCount = sizeof(leftoverBuffer) / (channels * sampleSize); leftoverBytes = leftoverSampleCount * channels * sampleSize; } memcpy(leftoverBuffer, (uint8_t *)decodedData + bytesToCopy, leftoverBytes); } else { leftoverSampleCount = 0; } } else if (pM4a->fileType == k_ALAC) { unsigned channels = pM4a->channels; unsigned sampleSize = pM4a->sampleSize; if (pM4a->current_sample >= pM4a->total_samples) { result = MA_AT_END; break; } unsigned int frame_bytes = 0, timestamp = 0, duration = 0; ma_int64 sample_offset = MP4D_frame_offset(&pM4a->mp4, pM4a->audio_track_index, pM4a->current_sample, &frame_bytes, ×tamp, &duration); if (sample_offset == (ma_int64)(MP4D_file_offset_t)-1 || frame_bytes == 0) { result = MA_ERROR; break; } uint8_t *sample_data = (uint8_t *)malloc(frame_bytes); if (!sample_data) { result = MA_OUT_OF_MEMORY; break; } // Ensure sample_data is aligned on a 2-byte boundary uintptr_t ptr = (uintptr_t)sample_data; if ((ptr % sizeof(uint8_t)) != 0) { free(sample_data); result = MA_ERROR; break; } if (fseek(pM4a->file, sample_offset, SEEK_SET) != 0 || fread(sample_data, 1, frame_bytes, pM4a->file) != frame_bytes) { free(sample_data); result = MA_ERROR; break; } static int32_t decodedBuffer[8192] = {0}; uint32_t samplesDecodedPerChannel; int alac_ret = alac_decoder_decode( pM4a->alacDecoder, sample_data, frame_bytes, decodedBuffer, &samplesDecodedPerChannel); free(sample_data); if (alac_ret != 0 || samplesDecodedPerChannel == 0) { result = MA_ERROR; break; } ma_uint64 framesDecoded = samplesDecodedPerChannel; ma_uint64 framesNeeded = frameCount - totalFramesProcessed; ma_uint64 framesToCopy = framesDecoded < framesNeeded ? framesDecoded : framesNeeded; ma_uint64 bytesToCopy = framesToCopy * channels * sampleSize; memcpy((uint8_t *)pFramesOut + totalFramesProcessed * channels * sampleSize, decodedBuffer, bytesToCopy); totalFramesProcessed += framesToCopy; leftoverSampleCount -= framesToCopy; if (framesToCopy < framesDecoded) { // There are leftover frames leftoverSampleCount = framesDecoded - framesToCopy; ma_uint64 leftoverBytes = leftoverSampleCount * channels * sampleSize; if (leftoverBytes > sizeof(leftoverBuffer)) { // Safety check to avoid overflow in the buffer. leftoverSampleCount = sizeof(leftoverBuffer) / (channels * sampleSize); leftoverBytes = leftoverSampleCount * channels * sampleSize; } memcpy(leftoverBuffer, (uint8_t *)decodedBuffer + bytesToCopy, leftoverBytes); } else { leftoverSampleCount = 0; } pM4a->current_sample++; pM4a->cursor += totalFramesProcessed; if (pFramesRead != NULL) { *pFramesRead = totalFramesProcessed; } return (totalFramesProcessed > 0) ? MA_SUCCESS : result; } else { if (pM4a->current_sample >= pM4a->total_samples) { result = MA_AT_END; break; // No more samples } unsigned int frame_bytes = 0; unsigned int timestamp = 0; unsigned int duration = 0; // Get the sample offset and size using minimp4 ma_int64 sample_offset = MP4D_frame_offset( &pM4a->mp4, pM4a->audio_track_index, pM4a->current_sample, &frame_bytes, ×tamp, &duration); if (sample_offset == (ma_int64)(MP4D_file_offset_t)-1 || frame_bytes == 0) { // Error getting sample info result = MA_ERROR; break; } // Allocate buffer for the sample data uint8_t *sample_data = (uint8_t *)malloc(frame_bytes); if (sample_data == NULL) { result = MA_OUT_OF_MEMORY; break; } // Read the sample data directly from the file size_t bytesRead = 0; if (file_on_read(pM4a->file, sample_data, frame_bytes, &bytesRead) != MA_SUCCESS || bytesRead != frame_bytes) { free(sample_data); result = MA_ERROR; break; } pM4a->current_sample++; // Decode the AAC frame using faad2 void *decodedData = NeAACDecDecode(pM4a->hDecoder, &(pM4a->frameInfo), sample_data, frame_bytes); free(sample_data); // Free the sample data buffer if (pM4a->frameInfo.error > 0) { // Error in decoding, skip to the next frame. continue; } // Remove support for HE-AAC components (SBR or PS) if (pM4a->frameInfo.sbr || pM4a->frameInfo.ps) { // HE-AAC detected (either SBR or PS is present), skip processing continue; } unsigned long samplesDecoded = pM4a->frameInfo.samples; // Total samples decoded (channels * frames) ma_uint64 framesDecoded = samplesDecoded / channels; // Calculate how many frames we can process in this call ma_uint64 framesNeeded = frameCount - totalFramesProcessed; ma_uint64 framesToCopy = (framesDecoded < framesNeeded) ? framesDecoded : framesNeeded; ma_uint64 bytesToCopy = framesToCopy * channels * sampleSize; memcpy((uint8_t *)pFramesOut + totalFramesProcessed * channels * sampleSize, decodedData, bytesToCopy); totalFramesProcessed += framesToCopy; // Handle leftover frames using the global/static leftover buffer if (framesToCopy < framesDecoded) { // There are leftover frames leftoverSampleCount = framesDecoded - framesToCopy; ma_uint64 leftoverBytes = leftoverSampleCount * channels * sampleSize; if (leftoverBytes > sizeof(leftoverBuffer)) { // Safety check to avoid overflow in the buffer. leftoverSampleCount = sizeof(leftoverBuffer) / (channels * sampleSize); leftoverBytes = leftoverSampleCount * channels * sampleSize; } memcpy(leftoverBuffer, (uint8_t *)decodedData + bytesToCopy, leftoverBytes); } else { leftoverSampleCount = 0; } } } pM4a->cursor += totalFramesProcessed; if (pFramesRead != NULL) { *pFramesRead = totalFramesProcessed; } return (totalFramesProcessed > 0) ? MA_SUCCESS : result; } MA_API ma_result m4a_decoder_seek_to_pcm_frame(m4a_decoder *pM4a, ma_uint64 frameIndex) { if (pM4a == NULL) return MA_INVALID_ARGS; if (frameIndex >= pM4a->total_samples) return MA_INVALID_ARGS; pM4a->current_sample = (uint32_t)frameIndex; if (pM4a->fileType == k_rawAAC) { return MA_ERROR; } else if (pM4a->fileType == k_ALAC) { unsigned int frame_bytes = 0; unsigned int timestamp = 0; unsigned int duration = 0; ma_int64 sample_offset = MP4D_frame_offset( &pM4a->mp4, pM4a->audio_track_index, pM4a->current_sample, &frame_bytes, ×tamp, &duration); if (sample_offset == (ma_int64)(MP4D_file_offset_t)-1 || frame_bytes == 0) { return MA_ERROR; } if (file_on_seek(pM4a->file, sample_offset, ma_seek_origin_start) != MA_SUCCESS) { return MA_ERROR; } leftoverSampleCount = 0; pM4a->cursor = frameIndex; return MA_SUCCESS; } else { unsigned int frame_bytes = 0; unsigned int timestamp = 0; unsigned int duration = 0; ma_int64 sample_offset = MP4D_frame_offset( &pM4a->mp4, pM4a->audio_track_index, pM4a->current_sample, &frame_bytes, ×tamp, &duration); if (sample_offset == (ma_int64)(MP4D_file_offset_t)-1 || frame_bytes == 0) { return MA_ERROR; } if (file_on_seek(pM4a->file, sample_offset, ma_seek_origin_start) != MA_SUCCESS) { return MA_ERROR; } NeAACDecPostSeekReset(pM4a->hDecoder, (long)pM4a->current_sample); leftoverSampleCount = 0; pM4a->cursor = frameIndex; 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) { // Initialize output variables 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) { return MA_INVALID_OPERATION; } if (pM4a->fileType != k_rawAAC) { if (pM4a->track == NULL) { return MA_INVALID_OPERATION; } } if (pFormat != NULL) { *pFormat = pM4a->format; } if (pChannels != NULL) { *pChannels = pM4a->channels; } if (pSampleRate != NULL) { *pSampleRate = pM4a->sampleRate; } // Set a standard channel map if requested 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) { return MA_INVALID_ARGS; } *pCursor = pM4a->cursor; return MA_SUCCESS; } 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->track == NULL) { return MA_INVALID_ARGS; } // Calculate the length in PCM frames using the total number of samples and the sample rate. if (pM4a->total_samples > 0 && pM4a->sampleRate > 0) { *pLength = (ma_uint64)pM4a->total_samples; return MA_SUCCESS; } return MA_ERROR; } #endif #ifdef __cplusplus } #endif #endif kew-3.2.0/src/mpris.c000066400000000000000000001376471500206121000143740ustar00rootroot00000000000000#include "mpris.h" /* mpris.c Functions related to mpris implementation. */ guint registration_id; guint player_registration_id; guint bus_name_id; #ifndef __APPLE__ 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; g_dbus_method_invocation_return_value(invocation, NULL); } 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(&appState); 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(&appState); 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); g_dbus_method_invocation_return_value(invocation, NULL); } 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; if (!isStopped()) stop(); 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); g_dbus_method_invocation_return_value(invocation, NULL); } 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)user_data; gint64 offset; g_variant_get(parameters, "(x)", &offset); gboolean success = seekPosition(offset); if (success) { g_dbus_method_invocation_return_value(invocation, NULL); } else { g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Failed to seek to position"); } } 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)user_data; const gchar *track_id; gint64 new_position; // - "o" is an object path (or track identifier) // - "x" is a 64-bit integer representing the position g_variant_get(parameters, "(&ox)", &track_id, &new_position); gboolean success = setPosition(new_position); if (success) { // If setting the position was successful, return success with no additional value g_dbus_method_invocation_return_value(invocation, NULL); } else { // If setting the position failed, return an error g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Failed to set position for track %s", track_id); } } #endif #ifndef __APPLE__ 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"); } } #endif #ifndef __APPLE__ 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 || isStopped()) { 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(); if (Volume >= 1) Volume = Volume / 100; *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; CanGoNext = (isRepeatListEnabled() && playlist.head != NULL) ? TRUE : CanGoNext; *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; if (currentSong == NULL) CanPlay = FALSE; else CanPlay = TRUE; *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; if (currentSong == NULL) CanPause = FALSE; else CanPause = TRUE; *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; } #endif #ifndef __APPLE__ 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); if (new_volume > 1.0) new_volume = 1.0; if (new_volume < 0.0) new_volume = 0.0; new_volume *= 100; 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); return setPosition(new_position); } 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; } } #endif #ifndef __APPLE__ // 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}}; #endif void emitPlaybackStoppedMpris() { #ifndef __APPLE__ 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); } #endif } void cleanupMpris(void) { #ifndef __APPLE__ 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; } #endif } void initMpris(void) { #ifndef __APPLE__ 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"; GError *error = NULL; bus_name_id = g_bus_own_name_on_connection(connection, app_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", app_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); #endif } void emitStartPlayingMpris() { #ifndef __APPLE__ GVariant *parameters = g_variant_new("(s)", "Playing"); g_dbus_connection_emit_signal(connection, NULL, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "PlaybackStatusChanged", parameters, NULL); #endif } gchar *sanitizeTitle(const gchar *title) { gchar *sanitized = g_strdup(title); // Replace underscores with hyphens, otherwise some widgets have a problem g_strdelimit(sanitized, "_", '-'); // Duplicate string otherwise widgets have a problem with certain strings for some reason gchar *sanitized_dup = g_strdup_printf("%s", sanitized); g_free(sanitized); return sanitized_dup; } #ifndef __APPLE__ static guint64 last_emit_time = 0; #endif void emit_properties_changed(GDBusConnection *connection, const gchar *property_name, GVariant *new_value) { #ifndef __APPLE__ GVariantBuilder changed_properties_builder; if (connection == NULL || property_name == NULL || new_value == NULL) return; // Initialize the builder for changed properties g_variant_builder_init(&changed_properties_builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&changed_properties_builder, "{sv}", property_name, new_value); GError *error = NULL; gboolean result = 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), &error); if (!result) { g_critical("Failed to emit PropertiesChanged signal: %s", error->message); g_error_free(error); } else { g_debug("PropertiesChanged signal emitted successfully."); } g_variant_builder_clear(&changed_properties_builder); #else (void)connection; (void)property_name; (void)new_value; #endif } void emitVolumeChanged(void) { #ifndef __APPLE__ gdouble newVolume = (gdouble)getCurrentVolume() / 100; if (newVolume > 1.0) return; // Emit the PropertiesChanged signal for the volume property GVariant *volume_variant = g_variant_new_double(newVolume); emit_properties_changed(connection, "Volume", volume_variant); #endif } void emitShuffleChanged(void) { #ifndef __APPLE__ gboolean shuffleEnabled = isShuffleEnabled(); // Emit the PropertiesChanged signal for the volume property GVariant *volume_variant = g_variant_new_boolean(shuffleEnabled); emit_properties_changed(connection, "Shuffle", volume_variant); #endif } void emitMetadataChanged(const gchar *title, const gchar *artist, const gchar *album, const gchar *coverArtPath, const gchar *trackId, Node *currentSong, gint64 length) { #ifndef __APPLE__ guint64 current_time = g_get_monotonic_time(); if (current_time - last_emit_time < 500000) // 0.5 seconds { g_debug("Debounced signal emission."); return; } last_emit_time = current_time; if (!title || !album || !trackId) { g_warning("Invalid metadata: title, album, or trackId is NULL."); return; } gchar *coverArtUrl = NULL; gchar *sanitizedTitle = sanitizeTitle(title); g_debug("Starting to build metadata."); 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(sanitizedTitle)); g_free(sanitizedTitle); 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)); if (coverArtPath && *coverArtPath != '\0') { coverArtUrl = g_strdup_printf("file://%s", coverArtPath); g_variant_builder_add(&metadata_builder, "{sv}", "mpris:artUrl", g_variant_new_string(coverArtUrl)); g_debug("Cover art URL added: %s", coverArtUrl); g_free(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)); GVariant *metadata_variant = g_variant_builder_end(&metadata_builder); if (!metadata_variant) { g_warning("Failed to end metadata GVariantBuilder."); return; } g_debug("Metadata built successfully."); 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", metadata_variant); g_variant_builder_add(&changed_properties_builder, "{sv}", "CanGoPrevious", g_variant_new_boolean((currentSong != NULL && currentSong->prev != NULL))); CanGoNext = (currentSong == NULL || currentSong->next != NULL) ? TRUE : FALSE; CanGoNext = (isRepeatListEnabled() && playlist.head != NULL) ? TRUE : CanGoNext; g_variant_builder_add(&changed_properties_builder, "{sv}", "CanGoNext", g_variant_new_boolean(CanGoNext)); g_variant_builder_add(&changed_properties_builder, "{sv}", "Shuffle", g_variant_new_boolean(isShuffleEnabled())); g_variant_builder_add(&changed_properties_builder, "{sv}", "CanPlay", g_variant_new_boolean(length != 0 ? true : false)); g_variant_builder_add(&changed_properties_builder, "{sv}", "CanPause", g_variant_new_boolean(length != 0 ? true : false)); if (isRepeatEnabled()) g_variant_builder_add(&changed_properties_builder, "{sv}", "LoopStatus", g_variant_new_string("Track")); else if (isRepeatListEnabled()) g_variant_builder_add(&changed_properties_builder, "{sv}", "LoopStatus", g_variant_new_string("List")); else g_variant_builder_add(&changed_properties_builder, "{sv}", "LoopStatus", g_variant_new_string("None")); CanSeek = true; g_variant_builder_add(&changed_properties_builder, "{sv}", "CanSeek", g_variant_new_boolean(CanSeek)); g_debug("PropertiesChanged signal is ready to be emitted."); GError *error = NULL; gboolean result = 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), &error); if (!result) { g_critical("Failed to emit PropertiesChanged signal: %s", error->message); g_error_free(error); } else { g_debug("PropertiesChanged signal emitted successfully."); } g_variant_builder_clear(&changed_properties_builder); g_variant_builder_clear(&metadata_builder); #else (void)title; (void)artist; (void)album; (void)coverArtPath; (void)trackId; (void)currentSong; (void)length; #endif } kew-3.2.0/src/mpris.h000066400000000000000000000012601500206121000143560ustar00rootroot00000000000000#ifndef MPRIS_H #define MPRIS_H #include #include #include "playerops.h" #include "playlist.h" #include "sound.h" #include "soundcommon.h" void initMpris(void); void emitStringPropertyChanged(const gchar *propertyName, const gchar *newValue); void emitBooleanPropertyChanged(const gchar *propertyName, gboolean newValue); void emitVolumeChanged(void); void emitShuffleChanged(void); 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-3.2.0/src/notifications.c000066400000000000000000000264121500206121000160760ustar00rootroot00000000000000#include "notifications.h" /* notifications.c Related to desktop notifications. */ char *lastCover = NULL; bool isValidFilepath(const char *path) { if (path == NULL || strnlen(path, PATH_MAX) == 0 || strnlen(path, PATH_MAX) >= PATH_MAX) { return false; } int fd = open(path, O_RDONLY); if (fd == -1) { return false; } close(fd); return true; } void removeBlacklistedChars(const char *input, const char *blacklist, char *output, size_t output_size) { if (!input || !blacklist || !output || output_size == 0) { return; } const char *in_ptr = input; char *out_ptr = output; size_t chars_copied = 0; while (*in_ptr && chars_copied < output_size - 1) { // Copy characters not in blacklist if (!strchr(blacklist, *in_ptr)) { *out_ptr++ = *in_ptr; chars_copied++; } in_ptr++; } *out_ptr = '\0'; } void ensureNonEmpty(char *str) { if (str[0] == '\0') { str[0] = ' '; str[1] = '\0'; } } #ifdef USE_DBUS #define NOTIFICATION_INTERVAL_MICROSECONDS 500000 // 0.5 seconds struct timeval lastNotificationTime = {0, 0}; static char sanitizedArtist[512]; static char sanitizedTitle[512]; int canShowNotification(void) { struct timeval now; gettimeofday(&now, NULL); long seconds = now.tv_sec - lastNotificationTime.tv_sec; long microseconds = now.tv_usec - lastNotificationTime.tv_usec; long elapsed = seconds * 1000000 + microseconds; // Total elapsed time in microseconds if (elapsed >= NOTIFICATION_INTERVAL_MICROSECONDS) { lastNotificationTime = now; return 1; } return 0; } void onNotificationClosed(void) { } static GDBusConnection *connection = NULL; static guint last_notification_id = 0; static guint signal_subscription_id = 0; static void on_dbus_call_complete(GObject *source_object, GAsyncResult *res, gpointer user_data) { GError *error = NULL; GVariant *result = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error); if (error) { fprintf(stderr, "D-Bus call failed or timed out: %s\n", error->message); g_error_free(error); return; } // Extract the notification ID from the result guint32 *last_notification_id = (guint32 *)user_data; g_variant_get(result, "(u)", last_notification_id); g_variant_unref(result); } static void on_notification_closed_signal(GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { (void)connection; (void)sender_name; (void)object_path; (void)interface_name; (void)signal_name; (void)user_data; guint32 id, reason; g_variant_get(parameters, "(uu)", &id, &reason); if (id == last_notification_id) { last_notification_id = 0; } } static void on_close_notification_complete(GObject *source_object, GAsyncResult *res, gpointer user_data) { (void)user_data; GError *error = NULL; GVariant *result = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source_object), res, &error); if (error) { fprintf(stderr, "Failed to close notification: %s\n", error->message); g_error_free(error); } else if (result) { g_variant_unref(result); } last_notification_id = 0; } typedef struct { GMainLoop *loop; gboolean connected; GDBusConnection *connection; gboolean timeout_triggered; int ref_count; } BusConnectionData; static void bus_connection_data_ref(BusConnectionData *data) { g_atomic_int_inc(&(data->ref_count)); } static void bus_connection_data_unref(BusConnectionData *data) { if (g_atomic_int_dec_and_test(&(data->ref_count))) { g_main_loop_unref(data->loop); g_free(data); } } static gboolean on_timeout(gpointer user_data) { BusConnectionData *data = (BusConnectionData *)user_data; if (!data->connected) { fprintf(stderr, "D-Bus connection timed out.\n"); data->timeout_triggered = TRUE; g_main_loop_quit(data->loop); } // Decrement reference count bus_connection_data_unref(data); return FALSE; // Stop the timeout callback from repeating } static void on_bus_get_complete(GObject *source_object, GAsyncResult *res, gpointer user_data) { (void)source_object; BusConnectionData *data = (BusConnectionData *)user_data; GError *error = NULL; data->connection = g_bus_get_finish(res, &error); if (error) { fprintf(stderr, "Failed to connect to D-Bus: %s\n", error->message); g_error_free(error); } else { data->connected = TRUE; } if (!data->timeout_triggered) { g_main_loop_quit(data->loop); } // Decrement reference count bus_connection_data_unref(data); } GDBusConnection *get_dbus_connection_with_timeout(GBusType bus_type, guint timeout_ms) { // Allocate and initialize the data structure BusConnectionData *data = g_new0(BusConnectionData, 1); data->loop = g_main_loop_new(NULL, FALSE); data->connected = FALSE; data->connection = NULL; data->timeout_triggered = FALSE; data->ref_count = 1; // Start with a single reference // Increment reference count for each callback bus_connection_data_ref(data); // For on_timeout bus_connection_data_ref(data); // For on_bus_get_complete // Start the asynchronous bus connection g_bus_get(bus_type, NULL, on_bus_get_complete, data); // Add a timeout callback g_timeout_add(timeout_ms, on_timeout, data); // Run the main loop g_main_loop_run(data->loop); // Store the connection result before cleaning up GDBusConnection *connection = data->connection; // Decrement reference count for the main loop bus_connection_data_unref(data); return connection; } void cleanupPreviousNotification() { if (last_notification_id != 0) { // Send CloseNotification call for the active notification g_dbus_connection_call( connection, "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "CloseNotification", g_variant_new("(u)", last_notification_id), NULL, // No return value expected G_DBUS_CALL_FLAGS_NONE, 100, // Timeout in milliseconds NULL, // Cancellable on_close_notification_complete, NULL); } } int displaySongNotification(const char *artist, const char *title, const char *cover, UISettings *ui) { if (!ui->allowNotifications || !canShowNotification()) { return 0; } if (connection == NULL) { connection = get_dbus_connection_with_timeout(G_BUS_TYPE_SESSION, 100); if (connection == NULL) { fprintf(stderr, "Failed to connect to session bus\n"); return -1; } signal_subscription_id = g_dbus_connection_signal_subscribe( connection, "org.freedesktop.Notifications", "org.freedesktop.Notifications", "NotificationClosed", "/org/freedesktop/Notifications", NULL, G_DBUS_SIGNAL_FLAGS_NONE, on_notification_closed_signal, NULL, NULL); } const char *blacklist = "&;|*~<>^()[]{}$\\\""; removeBlacklistedChars(artist, blacklist, sanitizedArtist, sizeof(sanitizedArtist)); removeBlacklistedChars(title, blacklist, sanitizedTitle, sizeof(sanitizedTitle)); ensureNonEmpty(sanitizedArtist); ensureNonEmpty(sanitizedTitle); int coverExists = isValidFilepath(cover); // Free and update the cover if (lastCover != NULL) { free(lastCover); lastCover = NULL; } lastCover = cover != NULL ? strdup(cover) : NULL; cleanupPreviousNotification(); // Create a new notification const gchar *app_name = "kew"; const gchar *app_icon = (coverExists && cover) ? cover : ""; const gchar *summary = sanitizedArtist; const gchar *body = sanitizedTitle; GVariantBuilder actions_builder; g_variant_builder_init(&actions_builder, G_VARIANT_TYPE("as")); GVariantBuilder hints_builder; g_variant_builder_init(&hints_builder, G_VARIANT_TYPE("a{sv}")); gint32 expire_timeout = -1; g_dbus_connection_call( connection, "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "Notify", g_variant_new("(susssasa{sv}i)", app_name, 0, // New notification, no replaces_id app_icon, summary, body, &actions_builder, &hints_builder, expire_timeout), G_VARIANT_TYPE("(u)"), G_DBUS_CALL_FLAGS_NONE, 100, // Timeout in milliseconds NULL, // Cancellable on_dbus_call_complete, &last_notification_id); return 0; } void cleanupDbusConnection() { cleanupPreviousNotification(); // Unsubscribe from signals if (signal_subscription_id != 0) { g_dbus_connection_signal_unsubscribe(connection, signal_subscription_id); signal_subscription_id = 0; } // Release the connection if (connection != NULL) { g_object_unref(connection); connection = NULL; } } #endif void freeLastCover(void) { if (lastCover != NULL) { free(lastCover); lastCover = NULL; } } kew-3.2.0/src/notifications.h000066400000000000000000000005771500206121000161070ustar00rootroot00000000000000 #ifndef NOTIFICATIONS_H #define NOTIFICATIONS_H #include #include #include #include #include #include "appstate.h" #ifndef PATH_MAX #define PATH_MAX 4096 #endif #ifdef USE_DBUS int displaySongNotification(const char *artist, const char *title, const char *cover, UISettings *ui); #endif void freeLastCover(void); #endif kew-3.2.0/src/player.c000066400000000000000000001760211500206121000145230ustar00rootroot00000000000000#include "player.h" #include "playerops.h" /* player.c Functions related to printing the player to the screen. */ #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef METADATA_MAX_SIZE #define METADATA_MAX_SIZE 256 #endif #ifdef __APPLE__ const int ABSOLUTE_MIN_WIDTH = 80; #else const int ABSOLUTE_MIN_WIDTH = 68; #endif bool fastForwarding = false; bool rewinding = false; double pauseSeconds = 0.0; double totalPauseSeconds = 0.0; double seekAccumulatedSeconds = 0.0; int minHeight = 0; int elapsedBars = 0; int preferredWidth = 0; int preferredHeight = 0; int textWidth = 0; int indent = 0; int maxListSize = 0; int maxSearchListSize = 0; int maxRadioSearchListSize = 0; int numTopLevelSongs = 0; int startLibIter = 0; int startSearchIter = 0; int startRadioSearchIter = 0; int maxLibListSize = 0; int chosenRow = 0; // The row that is chosen in playlist view int chosenLibRow = 0; // The row that is chosen in library view int chosenSearchResultRow = 0; // The row that is chosen in search view int chosenRadioSearchResultRow = 0; // The row that is chosen in radio search view int libIter = 0; int libSongIter = 0; int libTopLevelSongIter = 0; int previousChosenLibRow = 0; int libCurrentDirSongCount = 0; PixelData lastRowColor = {120, 120, 120}; const char LIBRARY_FILE[] = "kewlibrary"; FileSystemEntry *currentEntry = NULL; FileSystemEntry *lastEntry = NULL; FileSystemEntry *chosenDir = NULL; FileSystemEntry *library = NULL; const char *LOGO[] = { " __\n", "| |--.-----.--.--.--.\n", "| <| -__| | | |\n", "|__|__|_____|________|"}; 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); const int timeDisplayHeight = 1; const int heightMargin = 4; const int minHeight = visualizerHeight + metatagHeight + timeDisplayHeight + heightMargin + 1; const int minBorderWidth = 0; *height = term_h - minHeight; *width = (int)ceil((double)(*height) * aspectRatio); if (*width > term_w) { *width = term_w - minBorderWidth; *height = (int)floor((double)(*width) / aspectRatio); } if (*width > INT_MAX) *width = INT_MAX; if (*height > INT_MAX) *height = INT_MAX; *height -= 1; return 0; } void calcPreferredSize(UISettings *ui) { minHeight = 2 + (ui->visualizerEnabled ? ui->visualizerHeight : 0); int metadataHeight = 4; calcIdealImgSize(&preferredWidth, &preferredHeight, (ui->visualizerEnabled ? ui->visualizerHeight : 0), metadataHeight); } void printHelp() { printf(" kew - A terminal music player.\n" "\n" " \033[1;4mUsage:\033[0m kew path \"path to music library\"\n" " (Saves the music library path. Use this the first time. Ie: kew path \"/home/joe/Music/\")\n" " kew (no argument, opens library)\n" " kew all (loads all your songs up to 10 000)\n" " kew albums (plays all albums up to 2000 randomly one after the other)" " kew \n" " kew --help, -? or -h\n" " kew --version or -v\n" " kew dir (Sometimes it's necessary to specify it's a directory you want)\n" " kew song \n" " kew list \n" " kew shuffle (random and rand works too)\n" " kew artistA:artistB (plays artistA and artistB shuffled)\n" " kew . (plays kew.m3u file)\n" "\n" " \033[1;4mExample:\033[0m kew moon\n" " (Plays the first song or directory it finds that has the word moon, ie moonlight sonata)\n" "\n" " kew returns the first directory or file whose name partially matches the string you provide.\n" "\n" " Use quotes when providing strings with single quotes in them (') or vice versa.\n" " Enter to select or replay a song.\n" " Switch tracks with ←, → or h, l keys.\n" " Volume is adjusted with + (or =) and -.\n" " Space, p or right mouse to play or pause.\n" " Shift+s to stop.\n" " F2 to show/hide playlist view.\n" " F3 to show/hide library view.\n" " F4 to show/hide track view.\n" " F5 to show/hide search view.\n" " F6 to show/hide radio search view.\n" " F7 to show/hide show/hide key bindings view.\n" " u to update the library.\n" " v to toggle the spectrum visualizer.\n" " i to switch between using your regular color scheme or colors derived from the track cover.\n" " b to toggle album covers drawn in ascii or as a normal image.\n" " r to repeat the current song after playing.\n" " s to shuffle the playlist.\n" " a to seek back.\n" " d to seek forward.\n" " x to save the currently loaded playlist to a m3u file in your music folder.\n" " Tab to switch to next view.\n" " Shift+Tab to switch to previous view.\n" " Backspace to clear the playlist.\n" " Delete to remove a single playlist entry.\n" " gg go to first song.\n" " number + G or Enter to go to specific song number in the playlist.\n" " G to go to last song.\n" " . to add current song to kew.m3u (run with \"kew .\").\n" " Esc or q to quit.\n" "\n"); } int printLogo(SongData *songData, UISettings *ui) { if (ui->useConfigColors) setTextColor(ui->mainColor); else setColor(ui); int height = 0; int logoWidth = 0; if (!ui->hideLogo) { for (size_t i = 0; i < sizeof(LOGO) / sizeof(LOGO[0]); i++) { printBlankSpaces(indent); printf("%s", LOGO[i]); } 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}; int titleLength = strnlen(songData->metadata->title, METADATA_MAX_SIZE); char prettyTitle[titleLength + 1]; strncpy(prettyTitle, songData->metadata->title, titleLength); prettyTitle[titleLength] = '\0'; removeUnneededChars(prettyTitle, titleLength); trim(prettyTitle, titleLength); if (ui->hideLogo && songData->metadata->artist[0] != '\0') { printBlankSpaces(indent); snprintf(title, MAXPATHLEN, "%s - %s", songData->metadata->artist, prettyTitle); } else { if (ui->hideLogo) printBlankSpaces(indent); c_strcpy(title, prettyTitle, METADATA_MAX_SIZE - 1); title[MAXPATHLEN - 1] = '\0'; } shortenString(title, term_w - indent - indent - logoWidth - 4); if (ui->useConfigColors) setTextColor(ui->titleColor); printf(" %s\n\n", title); height += 2; } else if (isRadioPlaying()) { int term_w, term_h; getTermSize(&term_w, &term_h); RadioSearchResult *station = getCurrentPlayingRadioStation(); if (station != NULL) { char title[MAXPATHLEN] = {0}; snprintf(title, MAXPATHLEN, "%s (%s)", station->name, station->country); shortenString(title, term_w - indent - indent - logoWidth - 4); if (ui->useConfigColors) setTextColor(ui->titleColor); printf(" %s\n\n", title); height += 2; } else { printf("\n\n"); 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; } void printCover(SongData *songdata, UISettings *ui) { clearScreen(); if (songdata != NULL && songdata->cover != NULL && ui->coverEnabled) { if (!ui->coverAnsi) { printSquareBitmapCentered(songdata->cover, songdata->coverWidth, songdata->coverHeight, preferredHeight); } else { printInAscii(songdata->coverArtPath, preferredHeight); } } else { for (int i = 0; i <= preferredHeight; ++i) { printf("\n"); } } printf("\n\n"); } void printTitleWithDelay(const char *text, int delay, int maxWidth) { int max = strnlen(text, maxWidth); if (max == maxWidth) // For long names max -= 2; // Accommodate for the cursor that we display after the name. for (int i = 0; i <= max && delay; i++) { printf("\r "); printBlankSpaces(indent); for (int j = 0; j < i; j++) { printf("%c", text[j]); } printf("█"); fflush(stdout); c_sleep(delay); } if (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, UISettings *ui) { int term_w, term_h; getTermSize(&term_w, &term_h); int maxWidth = textWidth; // term_w - 3 - (indent * 2); printf("\n"); if (strnlen(metadata->artist, METADATA_MAX_LENGTH) > 0) { printBlankSpaces(indent); printf(" %.*s\n", maxWidth, metadata->artist); } else { printf("\n"); } if (strnlen(metadata->album, METADATA_MAX_LENGTH) > 0) { printBlankSpaces(indent); printf(" %.*s\n", maxWidth, metadata->album); } else { printf("\n"); } if (strnlen(metadata->date, METADATA_MAX_LENGTH) > 0) { printBlankSpaces(indent); int year = getYear(metadata->date); if (year == -1) printf(" %s\n", metadata->date); else printf(" %d\n", year); } else { printf("\n"); } cursorJump(4); if (strnlen(metadata->title, METADATA_MAX_LENGTH) > 0) { PixelData pixel = increaseLuminosity(ui->color, 20); if (ui->useConfigColors) { setDefaultTextColor(); } else 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); } // Clean up title before printing int titleLength = strnlen(metadata->title, maxWidth); char prettyTitle[titleLength + 1]; strncpy(prettyTitle, metadata->title, titleLength); prettyTitle[titleLength] = '\0'; removeUnneededChars(prettyTitle, titleLength); trim(prettyTitle, titleLength); printTitleWithDelay(prettyTitle, ui->titleDelay, maxWidth); } else { printf("\n"); } cursorJumpDown(3); } 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, ma_uint32 sampleRate, int avgBitRate) { 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); double rate = ((float)sampleRate) / 1000; if (term_w > progressWidth + 10) { if (rate == (int)rate) printf(" %dkHz", (int)rate); // No decimals else printf(" %.1fkHz", rate); } if (term_w > progressWidth + 19) { if (avgBitRate > 0) printf(" %dkb/s ", avgBitRate); } // Restore the cursor position printf("\033[u"); } void printMetadata(TagSettings const *metadata, UISettings *ui) { if (appState.currentView == LIBRARY_VIEW || appState.currentView == PLAYLIST_VIEW || appState.currentView == SEARCH_VIEW) return; if (ui->titleDelay) c_sleep(100); if (ui->useConfigColors) setDefaultTextColor(); else setTextColorRGB(ui->color.r, ui->color.g, ui->color.b); printBasicMetadata(metadata, ui); } void printTime(double elapsedSeconds, ma_uint32 sampleRate, int avgBitRate, AppState *state) { if (state->uiSettings.useConfigColors) setDefaultTextColor(); else setTextColorRGB(state->uiSettings.color.r, state->uiSettings.color.g, state->uiSettings.color.b); int term_w, term_h; getTermSize(&term_w, &term_h); printBlankSpaces(indent); if (term_h > minHeight) { double duration = getCurrentSongDuration(); printProgress(elapsedSeconds, duration, sampleRate, avgBitRate); } } int calcIndentNormal(void) { int textWidth = (ABSOLUTE_MIN_WIDTH > preferredWidth) ? ABSOLUTE_MIN_WIDTH : preferredWidth; return getIndentation(textWidth - 1) - 1; } int calcIndentTrackView(TagSettings *metadata) { if (metadata == NULL) return calcIndentNormal(); int titleLength = strnlen(metadata->title, METADATA_MAX_LENGTH); int albumLength = strnlen(metadata->album, METADATA_MAX_LENGTH); 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 (maxTextLength > 0 && maxTextLength < maxSize && maxTextLength > textWidth) textWidth = maxTextLength; if (textWidth > maxSize) textWidth = maxSize; return getIndentation(textWidth - 1) - 1; } void calcIndent(SongData *songdata) { if ((appState.currentView == TRACK_VIEW && songdata == NULL) || appState.currentView != TRACK_VIEW) { indent = calcIndentNormal(); } else { indent = calcIndentTrackView(songdata->metadata); } } void printGlimmeringText(char *text, int textLength, char *nerdFontText, PixelData color, int indent) { int brightIndex = 0; PixelData vbright = increaseLuminosity(color, 120); PixelData bright = increaseLuminosity(color, 60); 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 printErrorRow(void) { int indent = calcIndentNormal(); int term_w, term_h; getTermSize(&term_w, &term_h); #ifndef __APPLE__ // Move to next to lastRow printf("\033[%d;1H", term_h - 1); #endif if (term_w < ABSOLUTE_MIN_WIDTH) { printf("\n"); return; } if (!hasPrintedError && hasErrorMessage()) { setTextColorRGB(lastRowColor.r, lastRowColor.g, lastRowColor.b); printBlankSpaces(indent); printf(" %s\n", getErrorMessage()); hasPrintedError = true; fflush(stdout); } else { printf("\n"); } } void printLastRow(UISettings *ui) { int term_w, term_h; getTermSize(&term_w, &term_h); if (preferredWidth < 0 || preferredHeight < 0) // mini view return; if (term_w < ABSOLUTE_MIN_WIDTH) return; #ifndef __APPLE__ // Move to lastRow printf("\033[%d;1H", term_h); #endif setTextColorRGB(lastRowColor.r, lastRowColor.g, lastRowColor.b); #ifdef __APPLE__ char text[100] = " Sh+Z List|Sh+X Lib|Sh+C Track|Sh+V Search|Sh+B Radio|Sh+N Help"; #else char text[100] = " [F2 Playlist|F3 Library|F4 Track|F5 Search|F6 Radio|F7 Help]"; #endif char nerdFontText[100] = ""; printf("\r"); size_t maxLength = sizeof(nerdFontText); size_t currentLength = strnlen(nerdFontText, maxLength); if (isPaused()) { char pauseText[] = " ⏸"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", pauseText); currentLength += strnlen(pauseText, maxLength - currentLength); } else if (isStopped()) { char pauseText[] = " ■"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", pauseText); currentLength += strnlen(pauseText, maxLength - currentLength); } else { char pauseText[] = " ▶"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", pauseText); currentLength += strnlen(pauseText, maxLength - currentLength); } if (isRepeatEnabled()) { char repeatText[] = " \u27f3"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", repeatText); currentLength += strnlen(repeatText, maxLength - currentLength); } else if (isRepeatListEnabled()) { char repeatText[] = " \u27f3L"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", repeatText); currentLength += strnlen(repeatText, maxLength - currentLength); } if (isShuffleEnabled()) { char shuffleText[] = " \uf074"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", shuffleText); currentLength += strnlen(shuffleText, maxLength - currentLength); } if (fastForwarding) { char forwardText[] = " \uf04e"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", forwardText); currentLength += strnlen(forwardText, maxLength - currentLength); } if (rewinding) { char rewindText[] = " \uf04a"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", rewindText); currentLength += strnlen(rewindText, maxLength - currentLength); } printf("\033[K"); // Clear the line int indent = calcIndentNormal(); int textLength = strnlen(text, 100); int randomNumber = getRandomNumber(1, 808); if (randomNumber == 808 && !ui->hideGlimmeringText) printGlimmeringText(text, textLength, nerdFontText, lastRowColor, indent); else { printBlankSpaces(indent); printf("%s", text); printf("%s", nerdFontText); } } int printAbout(SongData *songdata, UISettings *ui) { clearScreen(); int numRows = printLogo(songdata, ui); setDefaultTextColor(); printBlankSpaces(indent); printf(" kew version: %s\n\n", VERSION); numRows += 2; return numRows; } int showKeyBindings(SongData *songdata, AppSettings *settings, UISettings *ui) { int numPrintedRows = 0; int term_w, term_h; getTermSize(&term_w, &term_h); maxListSize = term_h - 4; numPrintedRows += printAbout(songdata, ui); setDefaultTextColor(); printBlankSpaces(indent); printf(" - Switch tracks with ←, → or %s, %s keys.\n", settings->previousTrackAlt, settings->nextTrackAlt); printBlankSpaces(indent); printf(" - Volume is adjusted with %s (or %s) and %s.\n", settings->volumeUp, settings->volumeUpAlt, settings->volumeDown); printBlankSpaces(indent); printf(" - Press F2 for Playlist View:\n"); printBlankSpaces(indent); printf(" Use ↑, ↓ keys, %s, %s keys, or mouse scroll to scroll.\n", settings->scrollUpAlt, settings->scrollDownAlt); printBlankSpaces(indent); printf(" Press Enter or middle click to play.\n"); printBlankSpaces(indent); printf(" Press Backspace to clear the list or Delete to remove an entry.\n"); printBlankSpaces(indent); printf(" - Press F3 for Library View:\n"); printBlankSpaces(indent); printf(" Use ↑, ↓ keys, %s, %s keys, or mouse scroll to scroll.\n", settings->scrollUpAlt, settings->scrollDownAlt); printBlankSpaces(indent); printf(" Press Enter or middle click to add/remove songs.\n"); printBlankSpaces(indent); printf(" - Press F4 for Track View.\n"); printBlankSpaces(indent); printf(" - Space, %s, or right click to play or pause.\n", settings->togglePause); printBlankSpaces(indent); printf(" - Shift+s to stop.\n"); 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 toggle album covers drawn in ascii.\n", settings->toggleAscii); printBlankSpaces(indent); printf(" - %s to repeat the current song after playing.\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\n", settings->quit); printBlankSpaces(indent); printf(" Project URL: https://github.com/ravachol/kew\n"); printBlankSpaces(indent); printf(" Copyright © 2022-2025 Ravachol.\n"); printf("\n"); numPrintedRows += 27; while (numPrintedRows < maxListSize) { printf("\n"); numPrintedRows++; } printErrorRow(); printLastRow(ui); numPrintedRows++; return numPrintedRows; } void toggleShowView(ViewState viewToShow) { refresh = true; if (appState.currentView == viewToShow) { appState.currentView = TRACK_VIEW; } else { appState.currentView = viewToShow; } } void switchToNextView(void) { switch (appState.currentView) { case PLAYLIST_VIEW: appState.currentView = LIBRARY_VIEW; break; case LIBRARY_VIEW: appState.currentView = (currentSong != NULL || isRadioPlaying()) ? TRACK_VIEW : SEARCH_VIEW; break; case TRACK_VIEW: appState.currentView = SEARCH_VIEW; break; case SEARCH_VIEW: appState.currentView = RADIOSEARCH_VIEW; break; case RADIOSEARCH_VIEW: appState.currentView = KEYBINDINGS_VIEW; break; case KEYBINDINGS_VIEW: appState.currentView = PLAYLIST_VIEW; break; } refresh = true; } void switchToPreviousView(void) { switch (appState.currentView) { case PLAYLIST_VIEW: appState.currentView = KEYBINDINGS_VIEW; break; case LIBRARY_VIEW: appState.currentView = PLAYLIST_VIEW; break; case TRACK_VIEW: appState.currentView = LIBRARY_VIEW; break; case SEARCH_VIEW: appState.currentView = (currentSong != NULL || isRadioPlaying()) ? TRACK_VIEW : LIBRARY_VIEW; break; case RADIOSEARCH_VIEW: appState.currentView = SEARCH_VIEW; break; case KEYBINDINGS_VIEW: appState.currentView = RADIOSEARCH_VIEW; break; } refresh = true; } void showTrack(void) { refresh = true; appState.currentView = TRACK_VIEW; } void flipNextPage(void) { 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; } else if (appState.currentView == RADIOSEARCH_VIEW) { chosenRadioSearchResultRow += maxRadioSearchListSize - 1; chosenRadioSearchResultRow = (chosenRadioSearchResultRow >= getRadioSearchResultsCount()) ? getRadioSearchResultsCount() - 1 : chosenRadioSearchResultRow; startRadioSearchIter += maxRadioSearchListSize - 1; refresh = true; } } void flipPrevPage(void) { 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; } else if (appState.currentView == RADIOSEARCH_VIEW) { chosenRadioSearchResultRow -= maxRadioSearchListSize; chosenRadioSearchResultRow = (chosenRadioSearchResultRow > 0) ? chosenRadioSearchResultRow : 0; startRadioSearchIter -= maxRadioSearchListSize; refresh = true; } } void scrollNext(void) { if (appState.currentView == PLAYLIST_VIEW) { chosenRow++; chosenRow = (chosenRow >= originalPlaylist->count) ? originalPlaylist->count - 1 : chosenRow; refresh = true; } else if (appState.currentView == LIBRARY_VIEW) { previousChosenLibRow = chosenLibRow; chosenLibRow++; refresh = true; } else if (appState.currentView == SEARCH_VIEW) { chosenSearchResultRow++; refresh = true; } else if (appState.currentView == RADIOSEARCH_VIEW) { chosenRadioSearchResultRow++; refresh = true; } } void scrollPrev(void) { if (appState.currentView == PLAYLIST_VIEW) { chosenRow--; chosenRow = (chosenRow > 0) ? chosenRow : 0; refresh = true; } else if (appState.currentView == LIBRARY_VIEW) { previousChosenLibRow = chosenLibRow; chosenLibRow--; refresh = true; } else if (appState.currentView == SEARCH_VIEW) { chosenSearchResultRow--; chosenSearchResultRow = (chosenSearchResultRow > 0) ? chosenSearchResultRow : 0; refresh = true; } else if (appState.currentView == RADIOSEARCH_VIEW) { chosenRadioSearchResultRow--; chosenRadioSearchResultRow = (chosenRadioSearchResultRow > 0) ? chosenRadioSearchResultRow : 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, UISettings *ui, int indentation) { int aboutRows = printLogo(songData, ui); if (termWidth > 52 && !ui->hideHelp) { setDefaultTextColor(); printBlankSpaces(indentation); printf(" Use ↑, ↓ or k, j to choose. Enter=Accept.\n"); printBlankSpaces(indentation); #ifndef __APPLE__ printf(" Pg Up and Pg Dn to scroll. Del to remove entry.\n"); #else printf(" Fn+Arrow Up and Fn+Arrow Down to scroll. Del to remove entry.\n"); #endif printBlankSpaces(indentation); printf(" Backspace to clear. Use t, g to move the songs.\n\n"); return aboutRows + 4; } return aboutRows; } void showSearch(SongData *songData, int *chosenRow, UISettings *ui) { int term_w, term_h; getTermSize(&term_w, &term_h); maxSearchListSize = term_h - 3; int aboutRows = printLogo(songData, ui); maxSearchListSize -= aboutRows; setDefaultTextColor(); if (term_w > indent + 38 && !ui->hideHelp) { printBlankSpaces(indent); printf(" Use ↑, ↓ to choose. Enter=Enqueue. Alt+Enter=Play.\n\n"); maxSearchListSize -= 2; } displaySearch(maxSearchListSize, indent, chosenRow, startSearchIter, ui); printErrorRow(); printLastRow(ui); } void showRadioSearch(SongData *songData, int *chosenRow, UISettings *ui) { int term_w, term_h; getTermSize(&term_w, &term_h); maxRadioSearchListSize = term_h - 3; int aboutRows = printLogo(songData, ui); maxRadioSearchListSize -= aboutRows; setDefaultTextColor(); if (term_w > indent + 73 && !ui->hideHelp) { printBlankSpaces(indent); printf(" Use ↑, ↓ to choose. Enter to search and then enter to accept a station.\n"); printBlankSpaces(indent); printf(" Shift+f to add, del to remove favorites. Empty search to show favorites.\n\n"); maxRadioSearchListSize -= 3; } displayRadioSearch(maxRadioSearchListSize, indent, chosenRow, startSearchIter, ui); printErrorRow(); printLastRow(ui); } void showPlaylist(SongData *songData, PlayList *list, int *chosenSong, int *chosenNodeId, AppState *state) { int term_w, term_h; getTermSize(&term_w, &term_h); maxListSize = term_h - 3; // Setup scrolling names if (getIsLongName() && isSameNameAsLastTime && updateCounter % scrollingInterval != 0) { updateCounter++; refresh = true; return; } else refresh = false; clearScreen(); int aboutRows = printLogoAndAdjustments(songData, term_w, &(state->uiSettings), indent); maxListSize -= aboutRows; if (state->uiSettings.useConfigColors) setTextColor(state->uiSettings.artistColor); else setColor(&(state->uiSettings)); printBlankSpaces(indent); printf(" ─ PLAYLIST ─\n"); maxListSize -= 1; displayPlaylist(list, maxListSize, indent, chosenSong, chosenNodeId, state->uiState.resetPlaylistDisplay, state); printErrorRow(); printLastRow(&(state->uiSettings)); } void resetSearchResult(void) { chosenSearchResultRow = 0; } void resetRadioSearchResult(void) { chosenRadioSearchResultRow = 0; } void printElapsedLine(int elapsedBars, int numProgressBars, PixelData color, bool useConfigColors) { printBlankSpaces(indent); printf(" "); for (int i = 0; i < numProgressBars; i++) { if (i > elapsedBars) { if (!useConfigColors) { PixelData tmp = increaseLuminosity(color, 50); printf("\033[38;2;%d;%d;%dm", tmp.r, tmp.g, tmp.b); } else { setTextColorRGB(lastRowColor.r, lastRowColor.g, lastRowColor.b); } printf("─"); } else if (i <= elapsedBars) { if (!useConfigColors) { printf("\033[38;2;%d;%d;%dm", color.r, color.g, color.b); } else { setDefaultTextColor(); } printf("─"); } } } void printElapsedBars(int elapsedBars, int numProgressBars, PixelData color, int height, bool useConfigColors) { if (!useConfigColors) { PixelData tmp = increaseLuminosity(color, round(height * 4)); printf("\033[38;2;%d;%d;%dm", tmp.r, tmp.g, tmp.b); } else { setDefaultTextColor(); } printBlankSpaces(indent); printf(" "); for (int i = 0; i < numProgressBars; i++) { if (i == 0) { printf("■ "); } else if (i < elapsedBars) printf("■ "); else { printf("= "); } } } void printVisualizer(double elapsedSeconds, AppState *state) { UISettings *ui = &(state->uiSettings); UIState *uis = &(state->uiState); int term_w, term_h; getTermSize(&term_w, &term_h); if (ui->visualizerEnabled) { printf("\n"); 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; visualizerWidth -= 1; uis->numProgressBars = (int)visualizerWidth / 2; double duration = getCurrentSongDuration(); #ifndef __APPLE__ saveCursorPosition(); #endif drawSpectrumVisualizer(state, indent); if (ui->progressBarType == 1) printElapsedLine(calcElapsedBars(elapsedSeconds, duration, uis->numProgressBars * 2), uis->numProgressBars * 2 - 1, ui->color, ui->useConfigColors); else printElapsedBars(calcElapsedBars(elapsedSeconds, duration, uis->numProgressBars), uis->numProgressBars, ui->color, ui->visualizerHeight, ui->useConfigColors); printErrorRow(); printLastRow(&(state->uiSettings)); #ifndef __APPLE__ restoreCursorPosition(); cursorJump(1); #else int jumpAmount = ui->visualizerHeight + 2; cursorJump(jumpAmount); #endif } else if (!ui->visualizerEnabled) { if (term_w >= ABSOLUTE_MIN_WIDTH) { #ifdef __APPLE__ printf("\n"); printErrorRow(); saveCursorPosition(); printLastRow(ui); restoreCursorPosition(); cursorJump(2); #else saveCursorPosition(); printErrorRow(); restoreCursorPosition(); printf("\n"); saveCursorPosition(); printLastRow(ui); restoreCursorPosition(); cursorJump(1); #endif } } } FileSystemEntry *getCurrentLibEntry(void) { return currentEntry; } FileSystemEntry *getLibrary(void) { return library; } FileSystemEntry *getChosenDir(void) { return chosenDir; } void setChosenDir(FileSystemEntry *entry) { if (entry == NULL) { return; } if (entry->isDirectory) { currentEntry = chosenDir = entry; } } void setCurrentAsChosenDir(void) { if (currentEntry->isDirectory) chosenDir = currentEntry; } void resetChosenDir(void) { chosenDir = NULL; } int displayTree(FileSystemEntry *root, int depth, int maxListSize, int maxNameWidth, AppState *state) { if (maxNameWidth < 0) maxNameWidth = 0; char dirName[maxNameWidth + 1]; char filename[maxNameWidth + 1]; bool foundChosen = false; int foundCurrent = 0; int extraIndent = 0; UISettings *ui = &(state->uiSettings); UIState *uis = &(state->uiState); if (currentSong != NULL && (strcmp(currentSong->song.filePath, root->fullPath) == 0)) { foundCurrent = 1; } if (startLibIter < 0) startLibIter = 0; if (libIter >= startLibIter + maxListSize) { return false; } int threshold = startLibIter + (maxListSize + 1) / 2; if (chosenLibRow > threshold) { startLibIter = chosenLibRow - maxListSize / 2 + 1; } if (chosenLibRow < 0) startLibIter = chosenLibRow = libIter = 0; if (root == NULL) return false; if (root->isDirectory || (!root->isDirectory && depth == 1) || (root->isDirectory && depth == 0) || (chosenDir != NULL && uis->allowChooseSongs && root->parent != NULL && (strcmp(root->parent->fullPath, chosenDir->fullPath) == 0 || strcmp(root->fullPath, chosenDir->fullPath) == 0))) { if (depth >= 0) { if (currentEntry != NULL && currentEntry != lastEntry && !currentEntry->isDirectory && currentEntry->parent != NULL && currentEntry->parent == chosenDir) { FileSystemEntry *tmpc = currentEntry->parent->children; libCurrentDirSongCount = 0; while (tmpc != NULL) { if (!tmpc->isDirectory) libCurrentDirSongCount++; tmpc = tmpc->next; } lastEntry = currentEntry; } if (libIter >= startLibIter) { if (depth <= 1) { if (ui->useConfigColors) setTextColor(ui->artistColor); else setColor(ui); } else { setDefaultTextColor(); } if (depth >= 2) printf(" "); // If more than two levels deep add an extra indentation extraIndent = (depth - 2 <= 0) ? 0 : depth - 2; printBlankSpaces(indent + extraIndent); if (chosenLibRow == libIter) { if (root->isEnqueued) { if (ui->useConfigColors) setTextColor(ui->enqueuedColor); else setColorAndWeight(0, ui); printf("\x1b[7m * "); } else { printf(" \x1b[7m "); } currentEntry = root; if (uis->allowChooseSongs == true && (chosenDir == NULL || (currentEntry != NULL && currentEntry->parent != NULL && chosenDir != NULL && (strcmp(currentEntry->parent->fullPath, chosenDir->fullPath) != 0) && strcmp(root->fullPath, chosenDir->fullPath) != 0))) { uis->collapseView = true; refresh = true; if (!uis->openedSubDir) { uis->allowChooseSongs = false; chosenDir = NULL; } } foundChosen = true; } else { if (root->isEnqueued) { if (ui->useConfigColors) printf("\033[%d;3%dm", foundCurrent, ui->enqueuedColor); else setColorAndWeight(foundCurrent, ui); printf(" * "); } else { printf(" "); } } if (maxNameWidth < extraIndent) maxNameWidth = extraIndent; if (root->isDirectory) { dirName[0] = '\0'; if (strcmp(root->name, "root") == 0) snprintf(dirName, maxNameWidth + 1 - extraIndent, "%s", "─ MUSIC LIBRARY ─"); else snprintf(dirName, maxNameWidth + 1 - extraIndent, "%s", root->name); char *upperDirName = stringToUpper(dirName); if (depth == 1) printf("%s \n", upperDirName); else { printf("%s \n", dirName); } free(upperDirName); } else { filename[0] = '\0'; isSameNameAsLastTime = (previousChosenLibRow == chosenLibRow); if (foundChosen) { previousChosenLibRow = chosenLibRow; } if (!isSameNameAsLastTime) { resetNameScroll(); } if (foundChosen) { processNameScroll(root->name, filename, maxNameWidth - extraIndent, isSameNameAsLastTime); } else { processName(root->name, filename, maxNameWidth - extraIndent); } printf("└─ "); if (foundCurrent && chosenLibRow != libIter) { printf("\e[4m\e[1m"); } printf("%s\n", filename); libSongIter++; } setColor(ui); } libIter++; } FileSystemEntry *child = root->children; while (child != NULL) { if (displayTree(child, depth + 1, maxListSize, maxNameWidth, state)) foundChosen = true; child = child->next; } } return foundChosen; } char *getLibraryFilePath(void) { return getFilePath(LIBRARY_FILE); } void showLibrary(SongData *songData, AppState *state) { // For scrolling names, update every nth time if (getIsLongName() && isSameNameAsLastTime && updateCounter % scrollingInterval != 0) { refresh = true; return; } else refresh = false; clearScreen(); if (state->uiState.collapseView) { if (previousChosenLibRow < chosenLibRow) { if (!state->uiState.openedSubDir) { chosenLibRow -= libCurrentDirSongCount; libCurrentDirSongCount = 0; } else { chosenLibRow -= state->uiState.numSongsAboveSubDir; state->uiState.openedSubDir = false; state->uiState.numSongsAboveSubDir = 0; state->uiState.collapseView = false; } } else { if (state->uiState.openedSubDir) { chosenLibRow -= state->uiState.numSongsAboveSubDir; } libCurrentDirSongCount = 0; state->uiState.openedSubDir = false; state->uiState.numSongsAboveSubDir = 0; } state->uiState.collapseView = false; } UISettings *ui = &(state->uiSettings); 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(ui); int aboutSize = printLogo(songData, ui); int maxNameWidth = term_w - 10 - indent; maxLibListSize -= aboutSize + 2; setDefaultTextColor(); if (term_w > 67 && !ui->hideHelp) { maxLibListSize -= 3; printBlankSpaces(indent); printf(" Use ↑, ↓ or k, j to choose. Enter=Enqueue/Dequeue. Alt+Enter=Play.\n"); printBlankSpaces(indent); #ifndef __APPLE__ printf(" Pg Up and Pg Dn to scroll. Press u to update, o to sort.\n\n"); #else printf(" Fn+Arrow Up and Fn+Arrow Down to scroll. u to update, o to sort.\n\n"); #endif } numTopLevelSongs = 0; FileSystemEntry *tmp = library->children; while (tmp != NULL) { if (!tmp->isDirectory) numTopLevelSongs++; tmp = tmp->next; } bool foundChosen = displayTree(library, 0, maxLibListSize, maxNameWidth, state); if (!foundChosen) { chosenLibRow--; refresh = true; } for (int i = libIter - startLibIter; i < maxLibListSize; i++) { printf("\n"); } printErrorRow(); printLastRow(ui); if (!foundChosen && refresh) { printf("\033[1;1H"); clearScreen(); showLibrary(songData, state); } } void createMetadataForRadio(TagSettings **metadata, RadioSearchResult *station) { if (station) { *metadata = malloc(sizeof(TagSettings)); if (!*metadata) return; if (station->bitrate > 0) { // Reserve space for " %dkb/s" const int maxBitratePartLen = 18; size_t maxNameLen = sizeof((*metadata)->title) - maxBitratePartLen; // Copy a truncated name safely strncpy((*metadata)->title, station->name, maxNameLen); (*metadata)->title[maxNameLen] = '\0'; // ensure null termination // Now append the bitrate string safely snprintf((*metadata)->title + strlen((*metadata)->title), sizeof((*metadata)->title) - strlen((*metadata)->title), " %dkb/s", station->bitrate); } else { c_strcpy((*(metadata))->title, station->name, sizeof((*(metadata))->title) - 1); (*metadata)->title[sizeof((*metadata)->title) - 1] = '\0'; } (*metadata)->album[0] = '\0'; (*metadata)->artist[0] = '\0'; (*metadata)->date[0] = '\0'; calcIndentTrackView(*metadata); } } void showTrackViewMini(SongData *songdata, AppState *state, double elapsedSeconds) { TagSettings *metadata = NULL; int avgBitRate = 0; if (refresh) { printf("\n"); clearScreen(); if (currentSong == NULL && !isRadioPlaying()) { int term_w, term_h; getTermSize(&term_w, &term_h); if (term_w > 21 && term_h > 4) { for (size_t i = 0; i < sizeof(LOGO) / sizeof(LOGO[0]); i++) { printBlankSpaces(indent); printf("%s", LOGO[i]); } printf("\n"); printBlankSpaces(indent); printf(" kew version: %s", VERSION); } return; } if (songdata) { metadata = songdata->metadata; } else if (isRadioPlaying()) { RadioSearchResult *station = getCurrentPlayingRadioStation(); createMetadataForRadio(&metadata, station); } calcIndentTrackView(metadata); if (state->uiSettings.useConfigColors) setDefaultTextColor(); else setTextColorRGB(state->uiSettings.color.r, state->uiSettings.color.g, state->uiSettings.color.b); if (strnlen(metadata->artist, METADATA_MAX_LENGTH) > 0 && strnlen(metadata->title, METADATA_MAX_LENGTH) > 0) { printBlankSpaces(indent); int text_len = textWidth - 3; char combined[text_len + 1]; snprintf(combined, sizeof(combined), "%s - %s", metadata->artist, metadata->title); printf(" %.*s\n", text_len, combined); } else if (strnlen(metadata->title, METADATA_MAX_LENGTH) > 0) { printBlankSpaces(indent); printf(" %.*s\n", textWidth, metadata->title); } if (!songdata && metadata) free(metadata); refresh = false; } int term_w, term_h; getTermSize(&term_w, &term_h); bool doPrintTime = term_h > (state->uiSettings.visualizerHeight + 3); bool doPrintVis = term_h > (state->uiSettings.visualizerHeight + 2); if (songdata && doPrintTime) { ma_uint32 sampleRate; ma_format format; avgBitRate = songdata->avgBitRate; getCurrentFormatAndSampleRate(&format, &sampleRate); printTime(elapsedSeconds, sampleRate, avgBitRate, state); } if (doPrintVis) printVisualizer(elapsedSeconds, state); } void showTrackView(SongData *songdata, AppState *state, double elapsedSeconds) { TagSettings *metadata = NULL; int avgBitRate = 0; if (refresh) { if (songdata) { metadata = songdata->metadata; } else if (isRadioPlaying()) { RadioSearchResult *station = getCurrentPlayingRadioStation(); avgBitRate = station->bitrate; createMetadataForRadio(&metadata, station); } clearScreen(); printf("\n"); printCover(songdata, &(state->uiSettings)); printMetadata(metadata, &(state->uiSettings)); if (!songdata && metadata) free(metadata); refresh = false; } if (songdata) { ma_uint32 sampleRate; ma_format format; avgBitRate = songdata->avgBitRate; getCurrentFormatAndSampleRate(&format, &sampleRate); printTime(elapsedSeconds, sampleRate, avgBitRate, state); } printVisualizer(elapsedSeconds, state); } int printPlayer(SongData *songdata, double elapsedSeconds, AppSettings *settings, AppState *state) { UISettings *ui = &(state->uiSettings); UIState *uis = &(state->uiState); if (hasPrintedError && refresh) clearErrorMessage(); if (!ui->uiEnabled) { return 0; } if (refresh) { hideCursor(); setColor(ui); if (songdata != NULL && songdata->metadata != NULL && !songdata->hasErrors && (songdata->hasErrors < 1)) { ui->color.r = songdata->red; ui->color.g = songdata->green; ui->color.b = songdata->blue; } else { if (state->currentView == TRACK_VIEW && !isRadioPlaying()) { state->currentView = LIBRARY_VIEW; } ui->color.r = defaultColor; ui->color.g = defaultColor; ui->color.b = defaultColor; } calcPreferredSize(ui); calcIndent(songdata); } int term_w, term_h; getTermSize(&term_w, &term_h); state->uiState.miniMode = false; if ((term_w <= 10 || term_h <= 8) || (preferredHeight <= 0 || preferredWidth <= 0)) { state->uiState.miniMode = true; showTrackViewMini(songdata, state, elapsedSeconds); fflush(stdout); return 0; } if (state->currentView != PLAYLIST_VIEW) state->uiState.resetPlaylistDisplay = true; if (state->currentView == KEYBINDINGS_VIEW && refresh) { clearScreen(); showKeyBindings(songdata, settings, ui); saveCursorPosition(); refresh = false; fflush(stdout); } else if (state->currentView == PLAYLIST_VIEW && refresh) { showPlaylist(songdata, originalPlaylist, &chosenRow, &(uis->chosenNodeId), state); state->uiState.resetPlaylistDisplay = false; fflush(stdout); } else if (state->currentView == SEARCH_VIEW && refresh) { clearScreen(); showSearch(songdata, &chosenSearchResultRow, ui); refresh = false; fflush(stdout); } else if (state->currentView == RADIOSEARCH_VIEW && refresh) { clearScreen(); showRadioSearch(songdata, &chosenRadioSearchResultRow, ui); refresh = false; fflush(stdout); } else if (state->currentView == LIBRARY_VIEW && refresh) { showLibrary(songdata, state); fflush(stdout); } else if (state->currentView == TRACK_VIEW) { showTrackView(songdata, state, elapsedSeconds); fflush(stdout); } return 0; } void showHelp(void) { printHelp(); } void freeMainDirectoryTree(AppState *state) { if (library == NULL) return; char *filepath = getLibraryFilePath(); if (state->uiSettings.cacheLibrary) freeAndWriteTree(library, filepath); else freeTree(library); free(filepath); } int getChosenRow(void) { return chosenRow; } void setChosenRow(int row) { chosenRow = row; } kew-3.2.0/src/player.h000066400000000000000000000027611500206121000145270ustar00rootroot00000000000000#ifndef PLAYER_H #define PLAYER_H #include #include #include #include #include "appstate.h" #include "imgfunc.h" #include "directorytree.h" #include "playlist.h" #include "playlist_ui.h" #include "search_ui.h" #include "searchradio_ui.h" #include "songloader.h" #include "sound.h" #include "term.h" #include "utils.h" #include "visuals.h" #include "common_ui.h" #include "common.h" extern int numProgressBars; extern bool fastForwarding; extern bool rewinding; extern double pauseSeconds; extern double totalPauseSeconds; extern double seekAccumulatedSeconds; extern FileSystemEntry *library; int printPlayer(SongData *songdata, double elapsedSeconds, AppSettings *settings, AppState *appState); void flipNextPage(void); void flipPrevPage(void); void showHelp(void); void setChosenDir(FileSystemEntry *entry); int printAbout(SongData *songdata, UISettings *ui); FileSystemEntry *getCurrentLibEntry(void); FileSystemEntry *getChosenDir(void); FileSystemEntry *getLibrary(void); void scrollNext(void); void scrollPrev(void); void setCurrentAsChosenDir(void); void toggleShowView(ViewState VIEW_TO_SHOW); void toggleShowRadioSearch(void); void showTrack(void); void freeMainDirectoryTree(AppState *state); char *getLibraryFilePath(void); void resetChosenDir(void); void switchToNextView(void); void switchToPreviousView(void); void resetSearchResult(void); void resetRadioSearchResult(void); int getChosenRow(void); void setChosenRow(int row); #endif kew-3.2.0/src/playerops.c000066400000000000000000001664111500206121000152470ustar00rootroot00000000000000#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 lastUpdateTime = {0, 0}; bool nextSongNeedsRebuilding = false; bool skipFromStopped = false; bool usingSongDataA = true; LoadingThreadData loadingdata; volatile bool loadedNextSong = false; bool waitingForPlaylist = false; bool waitingForNext = false; Node *nextSong = NULL; Node *tryNextSong = NULL; Node *songToStartFrom = NULL; Node *prevSong = NULL; int lastPlayedId = -1; bool songHasErrors = false; bool skipOutOfOrder = false; bool skipping = false; bool forceSkip = false; volatile bool clearingErrors = false; volatile bool songLoading = false; GDBusConnection *connection = NULL; GMainContext *global_main_context = NULL; typedef struct { char *path; UISettings *ui; } UpdateLibraryThreadArgs; void reshufflePlaylist(void) { if (isShuffleEnabled()) { if (currentSong != NULL) shufflePlaylistStartingFromSong(&playlist, currentSong); else shufflePlaylist(&playlist); nextSongNeedsRebuilding = true; } } void skip(void) { setCurrentImplementationType(NONE); setRepeatEnabled(false); audioData.endOfListReached = false; if (!isPlaying()) { switchAudioImplementation(); skipFromStopped = true; } else { setSkipToNext(true); } if (!skipOutOfOrder) refresh = true; } void updateLastSongSwitchTime(void) { clock_gettime(CLOCK_MONOTONIC, &start_time); } void updatePlaybackPosition(double elapsedSeconds) { #ifndef __APPLE__ 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); #else (void)elapsedSeconds; #endif } void emitSeekedSignal(double newPositionSeconds) { #ifndef __APPLE__ 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); #else (void)newPositionSeconds; #endif } void emitStringPropertyChanged(const gchar *propertyName, const gchar *newValue) { #ifndef __APPLE__ 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); #else (void)propertyName; (void)newValue; #endif } void emitBooleanPropertyChanged(const gchar *propertyName, gboolean newValue) { #ifndef __APPLE__ 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); #else (void)propertyName; (void)newValue; #endif } void playbackPause(struct timespec *pause_time) { if (!isPaused()) { emitStringPropertyChanged("PlaybackStatus", "Paused"); clock_gettime(CLOCK_MONOTONIC, pause_time); } pausePlayback(); } void play(Node *song) { if (song != NULL) currentSong = song; else { return; } skipping = true; skipOutOfOrder = true; loadedNextSong = false; songLoading = true; forceSkip = false; // Cancel starting from top if (waitingForPlaylist || audioData.restart) { waitingForPlaylist = false; audioData.restart = false; if (isShuffleEnabled()) reshufflePlaylist(); } loadingdata.loadA = !usingSongDataA; loadingdata.loadingFirstDecoder = true; loadSong(currentSong, &loadingdata); int maxNumTries = 50; int numtries = 0; while (!loadedNextSong && numtries < maxNumTries) { c_sleep(100); numtries++; } if (songHasErrors) { songHasErrors = false; forceSkip = true; if (currentSong->next != NULL) skipToSong(currentSong->next->id, true); } resetClock(); skip(); } void skipToSong(int id, bool startPlaying) { if (songLoading || !loadedNextSong || skipping || clearingErrors) if (!forceSkip) return; Node *found = NULL; findNodeInList(&playlist, id, &found); if (startPlaying) { playbackPlay(&totalPauseSeconds, &pauseSeconds); } play(found); } void skipToBegginningOfSong(void) { if (currentSong != NULL) { seekPercentage(0); emitSeekedSignal(0.0); } } void prepareIfSkippedSilent(void) { if (hasSilentlySwitched) { skipping = true; hasSilentlySwitched = false; updateLastSongSwitchTime(); setCurrentImplementationType(NONE); setRepeatEnabled(false); audioData.endOfListReached = false; usingSongDataA = !usingSongDataA; skipping = false; } } void playbackPlay(double *totalPauseSeconds, double *pauseSeconds) { if (isPaused()) { *totalPauseSeconds += *pauseSeconds; emitStringPropertyChanged("PlaybackStatus", "Playing"); } else if (isStopped()) { emitStringPropertyChanged("PlaybackStatus", "Playing"); } if (isStopped() && !hasSilentlySwitched) { skipToBegginningOfSong(); } resumePlayback(); if (hasSilentlySwitched) { *totalPauseSeconds = 0; prepareIfSkippedSilent(); } } void togglePause(double *totalPauseSeconds, double *pauseSeconds, struct timespec *pause_time) { togglePausePlayback(); if (isPaused()) { emitStringPropertyChanged("PlaybackStatus", "Paused"); clock_gettime(CLOCK_MONOTONIC, pause_time); } else { if (hasSilentlySwitched && !skipping) { *totalPauseSeconds = 0; prepareIfSkippedSilent(); } else { *totalPauseSeconds += *pauseSeconds; } emitStringPropertyChanged("PlaybackStatus", "Playing"); } } void toggleRepeat(void) { bool repeatEnabled = isRepeatEnabled(); bool repeatListEnabled = isRepeatListEnabled(); if (repeatEnabled) { setRepeatEnabled(false); setRepeatListEnabled(true); emitStringPropertyChanged("LoopStatus", "List"); } else if (repeatListEnabled) { setRepeatEnabled(false); setRepeatListEnabled(false); emitStringPropertyChanged("LoopStatus", "None"); } else { setRepeatEnabled(true); setRepeatListEnabled(false); emitStringPropertyChanged("LoopStatus", "Track"); } if (appState.currentView != TRACK_VIEW) refresh = true; } void addToSpecialPlaylist(void) { 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(void) { 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 toggleAscii(AppSettings *settings, UISettings *ui) { ui->coverAnsi = !ui->coverAnsi; c_strcpy(settings->coverAnsi, ui->coverAnsi ? "1" : "0", sizeof(settings->coverAnsi)); if (ui->coverEnabled) { clearScreen(); refresh = true; } } void toggleColors(AppSettings *settings, UISettings *ui) { ui->useConfigColors = !ui->useConfigColors; c_strcpy(settings->useConfigColors, ui->useConfigColors ? "1" : "0", sizeof(settings->useConfigColors)); clearScreen(); refresh = true; } void toggleVisualizer(AppSettings *settings, UISettings *ui) { ui->visualizerEnabled = !ui->visualizerEnabled; c_strcpy(settings->visualizerEnabled, ui->visualizerEnabled ? "1" : "0", sizeof(settings->visualizerEnabled)); restoreCursorPosition(); refresh = true; } void quit(void) { exit(0); } bool isCurrentSongDeleted(void) { 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 = NULL; bool isDeleted = determineCurrentSongData(&songData); if (isDeleted) return NULL; if (!isValidSong(songData)) return NULL; return songData; } double getCurrentSongDuration(void) { double duration = 0.0; SongData *currentSongData = getCurrentSongData(); if (currentSongData != NULL) duration = currentSongData->duration; return duration; } void calcElapsedTime(void) { if (isStopped()) return; 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); double duration = getCurrentSongDuration(); 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) { 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(void) { if (seekAccumulatedSeconds != 0.0) { if (currentSong != NULL) { #ifdef USE_FAAD if (pathEndsWith(currentSong->song.filePath, "aac")) { m4a_decoder *decoder = getCurrentM4aDecoder(); if (decoder->fileType == k_rawAAC) return; } #endif } setSeekElapsed(getSeekElapsed() + seekAccumulatedSeconds); seekAccumulatedSeconds = 0.0; calcElapsedTime(); double duration = getCurrentSongDuration(); float percentage = elapsedSeconds / (float)duration * 100.0; if (percentage < 0.0) { setSeekElapsed(0.0); percentage = 0.0; } seekPercentage(percentage); emitSeekedSignal(elapsedSeconds); } } bool setPosition(gint64 newPosition) { if (isPaused()) return false; gint64 currentPositionMicroseconds = llround(elapsedSeconds * G_USEC_PER_SEC); double duration = getCurrentSongDuration(); if (duration != 0.0) { gint64 step = newPosition - currentPositionMicroseconds; step = step / G_USEC_PER_SEC; seekAccumulatedSeconds += step; return true; } else { return false; } } bool seekPosition(gint64 offset) { if (isPaused()) return false; double duration = getCurrentSongDuration(); if (duration != 0.0) { gint64 step = offset; step = step / G_USEC_PER_SEC; seekAccumulatedSeconds += step; return true; } else { return false; } } void seekForward(UIState *uis) { if (currentSong != NULL) { #ifdef USE_FAAD if (pathEndsWith(currentSong->song.filePath, "aac")) { m4a_decoder *decoder = getCurrentM4aDecoder(); if (decoder != NULL && decoder->fileType == k_rawAAC) return; } #endif if (isPaused()) return; double duration = currentSong->song.duration; if (duration != 0.0) { float step = 100 / uis->numProgressBars; seekAccumulatedSeconds += step * duration / 100.0; } fastForwarding = true; } } void seekBack(UIState *uis) { if (currentSong != NULL) { #ifdef USE_FAAD if (pathEndsWith(currentSong->song.filePath, "aac")) { m4a_decoder *decoder = getCurrentM4aDecoder(); if (decoder != NULL && decoder->fileType == k_rawAAC) return; } #endif if (isPaused()) return; double duration = currentSong->song.duration; if (duration != 0.0) { float step = 100 / uis->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(void) { 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 silentSwitchToNext(bool loadSong, AppState *state) { skipping = true; nextSong = NULL; setCurrentSongToNext(); activateSwitch(&audioData); skipOutOfOrder = true; usingSongDataA = (audioData.currentFileIndex == 0); if (loadSong) { loadNextSong(); finishLoading(); loadedNextSong = true; state->uiState.doNotifyMPRISSwitched = true; } resetTimeCount(); clock_gettime(CLOCK_MONOTONIC, &start_time); refresh = true; skipping = false; hasSilentlySwitched = true; nextSongNeedsRebuilding = true; nextSong = NULL; } void removeCurrentlyPlayingSong(void) { if (currentSong != NULL) { stopPlayback(); emitStringPropertyChanged("PlaybackStatus", "Stopped"); clearCurrentTrack(); } loadedNextSong = false; audioData.restart = true; audioData.endOfListReached = true; if (currentSong != NULL) { lastPlayedId = currentSong->id; songToStartFrom = getListNext(currentSong); } waitingForNext = true; currentSong = NULL; } void playRadio() { pthread_mutex_lock(&(playlist.mutex)); RadioSearchResult *station = getCurrentRadioSearchEntry(); if (station != NULL) { removeCurrentlyPlayingSong(); playRadioStation(station); } pthread_mutex_unlock(&(playlist.mutex)); } void moveSongUp() { if (appState.currentView != PLAYLIST_VIEW) { return; } bool rebuild = false; int chosenRow = getChosenRow(); Node *node = findSelectedEntry(originalPlaylist, chosenRow); if (node == NULL) { return; } int id = node->id; pthread_mutex_lock(&(playlist.mutex)); if (node != NULL && currentSong != NULL) { // Rebuild if current song, the next song or the song after are affected if (currentSong != NULL) { Node *tmp = currentSong; for (int i = 0; i < 3; i++) { if (tmp == NULL) break; if (tmp->id == id) { rebuild = true; } tmp = tmp->next; } } } moveUpList(originalPlaylist, node); Node *plNode = findSelectedEntryById(&playlist, node->id); if (!isShuffleEnabled()) moveUpList(&playlist, plNode); chosenRow--; chosenRow = (chosenRow > 0) ? chosenRow : 0; setChosenRow(chosenRow); if (rebuild && currentSong != NULL) { node = NULL; nextSong = NULL; tryNextSong = currentSong->next; nextSongNeedsRebuilding = false; nextSong = NULL; nextSong = getListNext(currentSong); rebuildNextSong(nextSong); loadedNextSong = true; } pthread_mutex_unlock(&(playlist.mutex)); refresh = true; } void moveSongDown() { if (appState.currentView != PLAYLIST_VIEW) { return; } bool rebuild = false; int chosenRow = getChosenRow(); Node *node = findSelectedEntry(originalPlaylist, chosenRow); if (node == NULL) { return; } int id = node->id; pthread_mutex_lock(&(playlist.mutex)); if (node != NULL && currentSong != NULL) { // Rebuild if current song, the next song or the previous song are affected if (currentSong != NULL) { Node *tmp = currentSong; for (int i = 0; i < 2; i++) { if (tmp == NULL) break; if (tmp->id == id) { rebuild = true; } tmp = tmp->next; } if (currentSong->prev != NULL && currentSong->prev->id == id) rebuild = true; } } moveDownList(originalPlaylist, node); Node *plNode = findSelectedEntryById(&playlist, node->id); if (!isShuffleEnabled()) moveDownList(&playlist, plNode); chosenRow++; chosenRow = (chosenRow >= originalPlaylist->count) ? originalPlaylist->count - 1 : chosenRow; setChosenRow(chosenRow); if (rebuild && currentSong != NULL) { node = NULL; nextSong = NULL; tryNextSong = currentSong->next; nextSongNeedsRebuilding = false; nextSong = NULL; nextSong = getListNext(currentSong); rebuildNextSong(nextSong); loadedNextSong = true; } pthread_mutex_unlock(&(playlist.mutex)); refresh = true; } void dequeueSong(FileSystemEntry *child) { Node *node1 = findLastPathInPlaylist(child->fullPath, originalPlaylist); if (node1 == NULL) return; if (currentSong != NULL && currentSong->id == node1->id) { removeCurrentlyPlayingSong(); } else { if (songToStartFrom != NULL) { songToStartFrom = getListNext(node1); } } 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, FileSystemEntry **firstEnqueuedEntry) { while (child != NULL) { if (child->isDirectory && child->children != NULL) { child->isEnqueued = 1; enqueueChildren(child->children, firstEnqueuedEntry); } else if (!child->isEnqueued) { if (*firstEnqueuedEntry == NULL) *firstEnqueuedEntry = child; 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; } FileSystemEntry *enqueueSongs(FileSystemEntry *entry, UIState *uis) { FileSystemEntry *chosenDir = getChosenDir(); bool hasEnqueued = false; bool shuffle = false; FileSystemEntry *firstEnqueuedEntry = NULL; if (entry != NULL) { if (entry->isDirectory) { if (!hasSongChildren(entry) || entry->parent == NULL || (chosenDir != NULL && strcmp(entry->fullPath, chosenDir->fullPath) == 0)) { if (hasDequeuedChildren(entry)) { if (entry->parent == NULL) // Shuffle playlist if it's the root shuffle = true; entry->isEnqueued = 1; entry = entry->children; enqueueChildren(entry, &firstEnqueuedEntry); nextSongNeedsRebuilding = true; hasEnqueued = true; } else { dequeueChildren(entry); entry->isEnqueued = 0; nextSongNeedsRebuilding = true; } } if ((chosenDir != NULL && entry->parent != NULL && strcmp(entry->parent->fullPath, chosenDir->fullPath) == 0) && uis->allowChooseSongs == true) { uis->openedSubDir = true; FileSystemEntry *tmpc = entry->parent->children; uis->numSongsAboveSubDir = 0; while (tmpc != NULL) { if (strcmp(entry->fullPath, tmpc->fullPath) == 0) break; tmpc = tmpc->next; uis->numSongsAboveSubDir++; } } setCurrentAsChosenDir(); if (uis->allowChooseSongs == true) { uis->collapseView = true; refresh = true; } uis->allowChooseSongs = true; } else { if (!entry->isEnqueued) { nextSong = NULL; nextSongNeedsRebuilding = true; firstEnqueuedEntry = entry; enqueueSong(entry); hasEnqueued = true; } else { nextSong = NULL; nextSongNeedsRebuilding = true; dequeueSong(entry); } } refresh = true; } if (hasEnqueued) { waitingForNext = true; audioData.endOfListReached = false; if (firstEnqueuedEntry != NULL) songToStartFrom = findPathInPlaylist(firstEnqueuedEntry->fullPath, &playlist); lastPlayedId = -1; } if (shuffle) { shufflePlaylist(&playlist); songToStartFrom = NULL; } if (nextSongNeedsRebuilding) { reshufflePlaylist(); } return firstEnqueuedEntry; } void handleRemove(void) { if (appState.currentView == RADIOSEARCH_VIEW) { RadioSearchResult *station = getCurrentRadioSearchEntry(); if (station) { removeFromRadioFavorites(station); } } else if (appState.currentView == PLAYLIST_VIEW) { bool rebuild = false; Node *node = findSelectedEntry(originalPlaylist, getChosenRow()); if (node == NULL) { return; } Node *song = getNextSong(); int id = node->id; int currentId = (currentSong != NULL) ? currentSong->id : -1; if (currentId == node->id) { removeCurrentlyPlayingSong(); } else { if (songToStartFrom != NULL) { songToStartFrom = getListNext(node); } } pthread_mutex_lock(&(playlist.mutex)); if (node != NULL && song != NULL && currentSong != 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); if (isShuffleEnabled()) rebuild = true; currentSong = findSelectedEntryById(&playlist, currentId); if (rebuild && currentSong != NULL) { node = NULL; nextSong = NULL; reshufflePlaylist(); tryNextSong = currentSong->next; nextSongNeedsRebuilding = false; nextSong = NULL; nextSong = getListNext(currentSong); rebuildNextSong(nextSong); loadedNextSong = true; } pthread_mutex_unlock(&(playlist.mutex)); } else { return; } 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 (pathEndsWith(songData->filePath, "opus")) result = prepareNextOpusDecoder(songData->filePath); else if (pathEndsWith(songData->filePath, "ogg")) result = prepareNextVorbisDecoder(songData->filePath); #ifdef USE_FAAD else if (pathEndsWith(songData->filePath, "m4a") || pathEndsWith(songData->filePath, "aac")) result = prepareNextM4aDecoder(songData); #endif } } return result; } int assignLoadedData(void) { 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, loadingdata->filePath, sizeof(filepath)); SongData *songdata = NULL; if (loadingdata->loadA) { if (!userData.songdataADeleted) { userData.songdataADeleted = true; unloadSongData(&(loadingdata->songdataA), &appState); } } else { if (!userData.songdataBDeleted) { userData.songdataBDeleted = true; unloadSongData(&(loadingdata->songdataB), &appState); } } if (filepath[0] != '\0') { songdata = loadSongData(filepath, &appState); } 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, song->song.filePath, sizeof(loadingdata->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, nextSong->song.filePath, sizeof(loadingdata->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 && numtries < maxNumTries) { c_sleep(100); numtries++; } songLoading = false; } void stop(void) { stopPlayback(); if (isStopped()) { skipToBegginningOfSong(); emitStringPropertyChanged("PlaybackStatus", "Stopped"); } } int currentSort = 0; void sortLibrary(void) { if (currentSort == 0) { sortFileSystemTree(library, compareFoldersByAgeFilesAlphabetically); currentSort = 1; } else { sortFileSystemTree(library, compareEntryNatural); currentSort = 0; } refresh = true; } void loadNextSong(void) { songLoading = true; nextSongNeedsRebuilding = false; skipFromStopped = false; loadingdata.loadA = !usingSongDataA; tryNextSong = nextSong = getListNext(currentSong); loadingdata.loadingFirstDecoder = false; loadSong(nextSong, &loadingdata); } bool determineCurrentSongData(SongData **currentSongData) { *currentSongData = (audioData.currentFileIndex == 0) ? userData.songdataA : userData.songdataB; bool isDeleted = (audioData.currentFileIndex == 0) ? userData.songdataADeleted == true : userData.songdataBDeleted == true; if (isDeleted) { *currentSongData = (audioData.currentFileIndex != 0) ? userData.songdataA : userData.songdataB; isDeleted = (audioData.currentFileIndex != 0) ? userData.songdataADeleted == true : userData.songdataBDeleted == true; if (!isDeleted) { activateSwitch(&audioData); audioData.switchFiles = false; } } return isDeleted; } void setCurrentSongToNext(void) { if (currentSong != NULL) lastPlayedId = currentSong->id; currentSong = getNextSong(); } void finishLoading(void) { int maxNumTries = 20; int numtries = 0; while (!loadedNextSong && numtries < maxNumTries) { c_sleep(100); numtries++; } loadedNextSong = true; } void resetTimeCount(void) { elapsedSeconds = 0.0; pauseSeconds = 0.0; totalPauseSeconds = 0.0; } void resetClock(void) { resetTimeCount(); clock_gettime(CLOCK_MONOTONIC, &start_time); } void repeatList() { waitingForPlaylist = true; nextSongNeedsRebuilding = true; audioData.endOfListReached = false; } void skipToNextSong(AppState *state) { // Stop if there is no song or no next song if (currentSong == NULL || currentSong->next == NULL) { if (isRepeatListEnabled()) { currentSong = NULL; } else if (!isStopped() && !isPaused()) { stop(); return; } else { return; } } if (songLoading || nextSongNeedsRebuilding || skipping || clearingErrors) return; if (isStopped() || isPaused()) { silentSwitchToNext(true, state); return; } playbackPlay(&totalPauseSeconds, &pauseSeconds); skipping = true; skipOutOfOrder = false; resetClock(); skip(); } void setCurrentSongToPrev(void) { if (currentSong != NULL && currentSong->prev != NULL) { lastPlayedId = currentSong->id; currentSong = currentSong->prev; } } void silentSwitchToPrev(AppState *state) { skipping = true; setCurrentSongToPrev(); activateSwitch(&audioData); loadedNextSong = false; songLoading = true; forceSkip = false; usingSongDataA = !usingSongDataA; loadingdata.loadA = usingSongDataA; loadingdata.loadingFirstDecoder = true; loadSong(currentSong, &loadingdata); state->uiState.doNotifyMPRISSwitched = true; finishLoading(); resetTimeCount(); clock_gettime(CLOCK_MONOTONIC, &start_time); refresh = true; skipping = false; nextSongNeedsRebuilding = true; nextSong = NULL; skipOutOfOrder = true; hasSilentlySwitched = true; } void skipToPrevSong(AppState *state) { if (currentSong == NULL) { if (!isStopped() && !isPaused()) stop(); return; } if (songLoading || skipping || clearingErrors) if (!forceSkip) return; if (isStopped() || isPaused()) { silentSwitchToPrev(state); return; } Node *song = currentSong; setCurrentSongToPrev(); if (song == currentSong) { resetTimeCount(); updatePlaybackPosition(0); // We need to signal to mpris that the song was reset to the beginning } 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 && numtries < maxNumTries) { c_sleep(100); numtries++; } if (songHasErrors) { songHasErrors = false; forceSkip = true; skipToPrevSong(state); } resetClock(); 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 && numtries < maxNumTries) { c_sleep(100); numtries++; } if (songHasErrors) { songHasErrors = false; forceSkip = true; if (songNumber < playlist.count) skipToNumberedSong(songNumber + 1); } resetClock(); skip(); } void skipToLastSong(void) { Node *song = playlist.head; if (!song) return; int count = 1; while (song->next != NULL) { song = getListNext(song); count++; } skipToNumberedSong(count); } void loadFirstSong(Node *song, UISettings *ui) { if (song == NULL) return; loadingdata.loadingFirstDecoder = true; loadSong(song, &loadingdata); int i = 0; while (!loadedNextSong && i < 10000) { if (i != 0 && i % 1000 == 0 && ui->uiEnabled) printf("."); c_sleep(10); fflush(stdout); i++; } } void unloadSongA(AppState *state) { if (userData.songdataADeleted == false) { userData.songdataADeleted = true; unloadSongData(&(loadingdata.songdataA), state); userData.songdataA = NULL; } } void unloadSongB(AppState *state) { if (userData.songdataBDeleted == false) { userData.songdataBDeleted = true; unloadSongData(&(loadingdata.songdataB), state); userData.songdataB = NULL; } } void unloadPreviousSong(AppState *state) { 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)))) { unloadSongA(state); if (!audioData.endOfListReached) loadedNextSong = false; usingSongDataA = 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)))) { unloadSongB(state); if (!audioData.endOfListReached) loadedNextSong = false; usingSongDataA = true; } pthread_mutex_unlock(&(loadingdata.mutex)); } int loadFirst(Node *song, AppState *state) { loadFirstSong(song, &(state->uiSettings)); usingSongDataA = true; while (songHasErrors && currentSong->next != NULL) { songHasErrors = false; loadedNextSong = false; currentSong = currentSong->next; loadFirstSong(currentSong, &(state->uiSettings)); } if (songHasErrors) { // Couldn't play any of the songs unloadPreviousSong(state); 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 *tmp = createDirectoryTree(path, &tmpDirectoryTreeEntries); if (!tmp) { perror("createDirectoryTree"); pthread_mutex_unlock(&switchMutex); return NULL; } pthread_mutex_lock(&switchMutex); copyIsEnqueued(library, tmp); freeTree(library); library = tmp; appState.uiState.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(UISettings *ui) { if (ui->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"); ui->cacheLibrary = 1; } else { printf("N\n"); ui->cacheLibrary = 0; } setNonblockingMode(); disableInputBuffering(); hideCursor(); } void createLibrary(AppSettings *settings, AppState *state) { if (state->uiSettings.cacheLibrary > 0) { char *libFilepath = getLibraryFilePath(); library = reconstructTreeFromFile(libFilepath, settings->path, &(state->uiState.numDirectoryTreeEntries)); free(libFilepath); updateLibraryIfChangedDetected(); } if (library == NULL || library->children == NULL) { struct timeval start, end; gettimeofday(&start, NULL); library = createDirectoryTree(settings->path, &(state->uiState.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(&(state->uiSettings)); } } if (library == NULL || library->children == NULL) { char message[MAXPATHLEN + 64]; snprintf(message, MAXPATHLEN + 64, "No music found at %s.", settings->path); setErrorMessage(message); } } time_t getModificationTime(struct stat *path_stat) { if (path_stat->st_mtime != 0) { return path_stat->st_mtime; } else { #ifdef __APPLE__ return path_stat->st_mtimespec.tv_sec; // macOS-specific member. #else return path_stat->st_mtim.tv_sec; // Linux-specific member. #endif } } void *updateIfTopLevelFoldersMtimesChangedThread(void *arg) { UpdateLibraryThreadArgs *args = (UpdateLibraryThreadArgs *)arg; // Cast `arg` back to the structure pointer char *path = args->path; UISettings *ui = args->ui; struct stat path_stat; if (stat(path, &path_stat) == -1) { perror("stat"); free(args); pthread_exit(NULL); } if (getModificationTime(&path_stat) > ui->lastTimeAppRan && ui->lastTimeAppRan > 0) { updateLibrary(path); free(args); pthread_exit(NULL); } DIR *dir = opendir(path); if (!dir) { perror("opendir"); free(args); pthread_exit(NULL); } struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } char fullPath[1024]; snprintf(fullPath, sizeof(fullPath), "%s/%s", path, entry->d_name); if (stat(fullPath, &path_stat) == -1) { perror("stat"); continue; } if (S_ISDIR(path_stat.st_mode)) { if (getModificationTime(&path_stat) > ui->lastTimeAppRan && ui->lastTimeAppRan > 0) { updateLibrary(path); break; } } } closedir(dir); free(args); pthread_exit(NULL); } // This only checks the library mtime and toplevel subfolders mtimes void updateLibraryIfChangedDetected(void) { pthread_t tid; UpdateLibraryThreadArgs *args = malloc(sizeof(UpdateLibraryThreadArgs)); if (args == NULL) { perror("malloc"); return; } args->path = settings.path; args->ui = &(appState.uiSettings); if (pthread_create(&tid, NULL, updateIfTopLevelFoldersMtimesChangedThread, (void *)args) != 0) { perror("pthread_create"); free(args); } } // Go through the display playlist and the shuffle playlist to remove all songs except the current one. // If no active song (if stopped rather than paused for example) entire playlist will be removed void updatePlaylistToPlayingSong(void) { bool clearAll = false; int currentID = -1; // Do we need to clear the entire playlist? if (currentSong == NULL) { clearAll = true; } else { currentID = currentSong->id; } int nextInPlaylistID; pthread_mutex_lock(&(playlist.mutex)); Node *songToBeRemoved; Node *nextInPlaylist = originalPlaylist->head; while (nextInPlaylist != NULL) { nextInPlaylistID = nextInPlaylist->id; if (clearAll || nextInPlaylistID != currentID) { songToBeRemoved = nextInPlaylist; int id = songToBeRemoved->id; // Update Library if (songToBeRemoved != NULL) markAsDequeued(getLibrary(), songToBeRemoved->song.filePath); // Remove from Display playlist if (songToBeRemoved != NULL) deleteFromList(originalPlaylist, songToBeRemoved); // Remove from Shuffle playlist Node *node2 = findSelectedEntryById(&playlist, id); if (node2 != NULL) deleteFromList(&playlist, node2); } nextInPlaylist = nextInPlaylist->next; } pthread_mutex_unlock(&(playlist.mutex)); nextSongNeedsRebuilding = true; nextSong = NULL; // Only refresh the screen if it makes sense to do so if (appState.currentView == PLAYLIST_VIEW || appState.currentView == LIBRARY_VIEW) { refresh = true; } } kew-3.2.0/src/playerops.h000066400000000000000000000070311500206121000152440ustar00rootroot00000000000000 #ifndef PLAYEROPS_H #define PLAYEROPS_H #include #include #include #include #include #include #include #include "appstate.h" #include "player.h" #include "songloader.h" #include "settings.h" #include "soundcommon.h" #ifdef USE_FAAD #include "m4a.h" #endif #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 GMainContext *global_main_context; extern LoadingThreadData loadingdata; extern struct timespec start_time; extern struct timespec pause_time; extern volatile bool loadedNextSong; extern bool nextSongNeedsRebuilding; extern bool waitingForPlaylist; extern bool waitingForNext; extern bool usingSongDataA; extern Node *nextSong; extern Node *songToStartFrom; extern int lastPlayedId; extern bool songHasErrors; extern volatile bool clearingErrors; extern volatile bool songLoading; extern bool skipping; extern bool skipOutOfOrder; extern Node *tryNextSong; extern bool skipFromStopped; extern UserData userData; SongData *getCurrentSongData(void); Node *getNextSong(void); void handleRemove(void); FileSystemEntry *enqueueSongs(FileSystemEntry *entry, UIState *uis); void updateLastSongSwitchTime(void); void playbackPause(struct timespec *pause_time); void playbackPlay(double *totalPauseSeconds, double *pauseSeconds); void togglePause(double *totalPauseSeconds, double *pauseSeconds, struct timespec *pause_time); void stop(void); void toggleRepeat(void); void toggleShuffle(void); void addToSpecialPlaylist(void); void toggleAscii(AppSettings *settings, UISettings *ui); void toggleColors(AppSettings *settings, UISettings *ui); void toggleVisualizer(AppSettings *settings, UISettings *ui); void quit(void); void calcElapsedTime(void); Node *getSongByNumber(PlayList *playlist, int songNumber); void skipToNextSong(AppState *state); void skipToPrevSong(AppState *state); void skipToSong(int id, bool startPlaying); void seekForward(UIState *uis); void seekBack(UIState *uis); void skipToNumberedSong(int songNumber); void skipToLastSong(void); void loadSong(Node *song, LoadingThreadData *loadingdata); void loadNext(LoadingThreadData *loadingdata); int loadFirst(Node *song, AppState *state); void flushSeek(void); Node *findSelectedEntryById(PlayList *playlist, int id); void emitSeekedSignal(double newPositionSeconds); void rebuildNextSong(Node *song); void updateLibrary(char *path); void askIfCacheLibrary(UISettings *ui); void unloadSongA(AppState *state); void unloadSongB(AppState *state); void unloadPreviousSong(AppState *state); void createLibrary(AppSettings *settings, AppState *state); void resetClock(void); void loadNextSong(void); void setCurrentSongToNext(void); void finishLoading(void); void resetTimeCount(void); bool setPosition(gint64 newPosition); bool seekPosition(gint64 offset); void silentSwitchToNext(bool loadSong, AppState *state); void reshufflePlaylist(void); bool determineCurrentSongData(SongData **currentSongData); void updateLibraryIfChangedDetected(void); double getCurrentSongDuration(void); void updatePlaylistToPlayingSong(void); void playRadio(); void moveSongUp(); void moveSongDown(); void play(Node *song); void repeatList(); void skipToBegginningOfSong(void); void sortLibrary(void); #endif kew-3.2.0/src/playlist.c000066400000000000000000000650521500206121000150710ustar00rootroot00000000000000#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[] = "(m3u8?)$"; 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; } } void moveUpList(PlayList *list, Node *node) { if (node == list->head || node == NULL || node->prev == NULL) return; Node *prevNode = node->prev; Node *nextNode = node->next; if (prevNode->prev) prevNode->prev->next = node; else list->head = node; node->prev = prevNode->prev; node->next = prevNode; prevNode->prev = node; prevNode->next = nextNode; if (nextNode) nextNode->prev = prevNode; else list->tail = prevNode; } void moveDownList(PlayList *list, Node *node) { if (node == list->tail || node == NULL || node->next == NULL) return; Node *nextNode = node->next; Node *prevNode = node->prev; Node *nextNextNode = nextNode->next; if (prevNode) prevNode->next = nextNode; else list->head = nextNode; nextNode->prev = prevNode; nextNode->next = node; node->prev = nextNode; node->next = nextNextNode; if (nextNextNode) nextNextNode->prev = node; else list->tail = node; } 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 *tmp = nodes[j]; nodes[j] = nodes[k]; nodes[k] = tmp; } 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); } } 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."); free(song.filePath); 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[100]; struct dirent **entries; int numEntries = scandir(directoryPath, &entries, NULL, compareLibEntries); 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 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, int maxSize) { playlistName[0] = '\0'; snprintf(playlistName, maxSize, "%s", search); snprintf(playlistName + strnlen(playlistName, maxSize), maxSize - strnlen(playlistName, maxSize), ".m3u"); for (int i = 0; playlistName[i] != '\0'; i++) { if (playlistName[i] == ':') { playlistName[i] = '-'; } } } void readM3UFile(const char *filename, PlayList *playlist) { GError *error = NULL; gchar *contents; gchar **lines; if (!g_file_get_contents(filename, &contents, NULL, &error)) { g_clear_error(&error); return; } gchar *directory = g_path_get_dirname(filename); lines = g_strsplit(contents, "\n", -1); for (gint i = 0; lines[i] != NULL; i++) { gchar *line = lines[i]; line = g_strdelimit(line, "\r", '\0'); gchar *trimmed_line = g_strstrip(line); if (trimmed_line[0] != '#' && trimmed_line[0] != '\0') { gchar *songPath; if (g_path_is_absolute(trimmed_line)) { songPath = g_strdup(trimmed_line); } else { songPath = g_build_filename(directory, trimmed_line, NULL); } 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++; g_free(songPath); } } g_free(directory); g_strfreev(lines); g_free(contents); } 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++) { size_t len = strnlen(search, MAX_SEARCH_SIZE); snprintf(search + len, MAX_SEARCH_SIZE - len, " %s", argv[i]); } makePlaylistName(search, MAX_SEARCH_SIZE); 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, strnlen(token + 4, MAXPATHLEN) + 1); searchType = FileOnly; } trim(token, MAXPATHLEN); char *searching = g_utf8_casefold(token, -1); if (walker(path, searching, 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)); } } free(searching); token = strtok(NULL, delimiter); } } if (numDirs > 1) shuffle = true; if (shuffle) shufflePlaylist(&playlist); if (playlist.head == NULL) printf("Music not found\n"); return 0; } void generateM3UFilename(const char *basePath, const char *filePath, char *m3uFilename, size_t size) { const char *baseName = strrchr(filePath, '/'); if (baseName == NULL) { baseName = filePath; // No '/' found, use the entire filename } else { baseName++; // Skip the '/' character } const char *dot = strrchr(baseName, '.'); if (dot == NULL) { // No '.' found, copy the base name and append ".m3u" if (basePath[strnlen(basePath, MAXPATHLEN) - 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[strnlen(basePath, MAXPATHLEN) - 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]; if (directory[strnlen(directory, MAXPATHLEN) - 1] == '/') { snprintf(playlistPath, sizeof(playlistPath), "%s%s", directory, mainPlaylistName); } else { snprintf(playlistPath, sizeof(playlistPath), "%s/%s", directory, 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]; int length = snprintf(playlistPath, sizeof(playlistPath), "%s", directory); if (length < 0 || length >= (int)sizeof(playlistPath) || playlistPath[0] == '\0') { return; } if (playlistPath[length - 1] != '/') { snprintf(playlistPath + length, sizeof(playlistPath) - length, "/%s", mainPlaylistName); } else { snprintf(playlistPath + length, sizeof(playlistPath) - length, "%s", 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(const 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(const 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", ".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) { for (size_t i = 0; i < n - 1; i++) { size_t j = getRandomNumber(i, n - 1); // Swap entries at i and j FileSystemEntry *tmp = array[i]; array[i] = array[j]; array[j] = tmp; } } } 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); shuffleEntries(albums, albumCount); for (size_t i = 0; i < albumCount && list->count < playlistMax; i++) { addAlbumToPlayList(list, albums[i], playlistMax); } } kew-3.2.0/src/playlist.h000066400000000000000000000042131500206121000150660ustar00rootroot00000000000000#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef __USE_XOPEN_EXTENDED #define __USE_XOPEN_EXTENDED #endif #include #include #include #include #include #include #include "directorytree.h" #include "file.h" #include "utils.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(const char *path, PlayList *playlist); Node *findLastPathInPlaylist(const 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); void moveUpList(PlayList *list, Node *node); void moveDownList(PlayList *list, Node *node); kew-3.2.0/src/playlist_ui.c000066400000000000000000000134551500206121000155660ustar00rootroot00000000000000#include "playlist_ui.h" /* playlist_ui.c Playlist UI functions. */ int startIter = 0; int previousChosenSong = 0; 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) { if (node == NULL || buffer == NULL) { buffer[0] = '\0'; return; } char filePath[MAXPATHLEN]; c_strcpy(filePath, node->song.filePath, sizeof(filePath)); char *lastSlash = strrchr(filePath, '/'); size_t len = strnlen(filePath, sizeof(filePath)); if (lastSlash != NULL) { int nameLength = filePath + len - (lastSlash + 1); // Length of the filename nameLength = (nameLength < bufferSize - 1) ? nameLength : bufferSize - 1; c_strcpy(buffer, lastSlash + 1, nameLength + 1); buffer[nameLength] = '\0'; } else { buffer[0] = '\0'; } } int displayPlaylistItems(Node *startNode, int startIter, int maxListSize, int termWidth, int indent, int chosenSong, int *chosenNodeId, UISettings *ui) { int numPrintedRows = 0; Node *node = startNode; int bufferSize = termWidth - indent - 12; for (int i = startIter; node != NULL && i < startIter + maxListSize; i++) { char *buffer = (char *)malloc(MAXPATHLEN * sizeof(char)); char *filename = (char *)malloc(MAXPATHLEN * sizeof(char) + 1); if (!buffer || !filename) { return 0; } preparePlaylistString(node, buffer, MAXPATHLEN); if (buffer[0] != '\0') { if (ui->useConfigColors) setTextColor(ui->artistColor); else setColor(ui); printBlankSpaces(indent); printf(" %d. ", i + 1); setDefaultTextColor(); isSameNameAsLastTime = (previousChosenSong == chosenSong); if (!isSameNameAsLastTime) { resetNameScroll(); } filename[0] = '\0'; if (i == chosenSong) { previousChosenSong = chosenSong; *chosenNodeId = node->id; processNameScroll(buffer, filename, bufferSize, isSameNameAsLastTime); printf("\x1b[7m"); } else { processName(buffer, filename, bufferSize); } if (i + 1 < 10) printf(" "); if (currentSong != NULL && currentSong->id == node->id) { printf("\e[4m\e[1m"); } printf("%s\n", filename); numPrintedRows++; } free(buffer); free(filename); node = node->next; } return numPrintedRows; } int displayPlaylist(PlayList *list, int maxListSize, int indent, int *chosenSong, int *chosenNodeId, bool reset, AppState *state) { int termWidth, termHeight; getTermSize(&termWidth, &termHeight); UISettings *ui = &(state->uiSettings); 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, ui); while (printedRows < maxListSize) { printf("\n"); printedRows++; } return printedRows; } kew-3.2.0/src/playlist_ui.h000066400000000000000000000004431500206121000155640ustar00rootroot00000000000000#ifndef PLAYLIST_UI_H #define PLAYLIST_UI_H #include "common_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, AppState *state); #endif kew-3.2.0/src/search_ui.c000066400000000000000000000210751500206121000151670ustar00rootroot00000000000000#include "search_ui.h" /* search_ui.c Search UI functions. */ #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; int minSearchLetters = 1; FileSystemEntry *currentSearchEntry = NULL; char searchText[MAX_SEARCH_LEN * 4 + 1]; // Unicode can be 4 characters FileSystemEntry *getCurrentSearchEntry(void) { return currentSearchEntry; } int getSearchResultsCount(void) { 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(void) { if (results != NULL) { free(results); results = NULL; } if (currentSearchEntry != NULL) currentSearchEntry = NULL; resultsCapacity = 0; resultsCount = 0; } void fuzzySearch(FileSystemEntry *root, int threshold) { freeSearchResults(); if (numSearchLetters > minSearchLetters) { fuzzySearchRecursive(root, searchText, threshold, collectResult); } refresh = true; } int compareResults(const void *a, const void *b) { SearchResult *resultA = (SearchResult *)a; SearchResult *resultB = (SearchResult *)b; return resultA->distance - resultB->distance; } void sortResults(void) { qsort(results, resultsCount, sizeof(SearchResult), compareResults); } int displaySearchBox(int indent, UISettings *ui) { if (ui->useConfigColors) setTextColor(ui->mainColor); else setColor(ui); 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 = strnlen(str, MAX_SEARCH_LEN); // Check if the string can fit into the search text buffer if (numSearchLetters + len > 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] = '\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(void) { 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, UISettings *ui) { int term_w, term_h; getTermSize(&term_w, &term_h); int maxNameWidth = term_w - indent - 5; char name[maxNameWidth + 1]; int printedRows = 0; 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"); printedRows++; // Print the sorted results for (size_t i = startSearchIter; i < resultsCount; i++) { if (numSearchLetters < minSearchLetters) break; if ((int)i >= maxListSize + startSearchIter - 1) break; setDefaultTextColor(); printBlankSpaces(indent); if (*chosenRow == (int)i) { currentSearchEntry = results[i].entry; if (results[i].entry->isEnqueued) { if (ui->useConfigColors) setTextColor(ui->enqueuedColor); else setColor(ui); printf("\x1b[7m * "); } else { printf(" \x1b[7m "); } } else { if (results[i].entry->isEnqueued) { if (ui->useConfigColors) setTextColor(ui->enqueuedColor); else setColor(ui); printf(" * "); } else printf(" "); } name[0] = '\0'; if (results[i].entry->isDirectory) { if (results[i].entry->parent != NULL && strcmp(results[i].entry->parent->name, "root") != 0) snprintf(name, maxNameWidth + 1, "[%s] (%s)", results[i].entry->name, results[i].entry->parent->name); else snprintf(name, maxNameWidth + 1, "[%s]", results[i].entry->name); } else { if (results[i].entry->parent != NULL && strcmp(results[i].entry->parent->name, "root") != 0) snprintf(name, maxNameWidth + 1, "%s (%s)", results[i].entry->name, results[i].entry->parent->name); else snprintf(name, maxNameWidth + 1, "%s", results[i].entry->name); } printf("%s\n", name); printedRows++; } while (printedRows < maxListSize) { printf("\n"); printedRows++; } return 0; } int displaySearch(int maxListSize, int indent, int *chosenRow, int startSearchIter, UISettings *ui) { displaySearchBox(indent, ui); displaySearchResults(maxListSize, indent, chosenRow, startSearchIter, ui); return 0; } kew-3.2.0/src/search_ui.h000066400000000000000000000007571500206121000152000ustar00rootroot00000000000000#include #include #include "soundcommon.h" #include "directorytree.h" #include "term.h" #include "common_ui.h" #include "common.h" int displaySearch(int maxListSize, int indent, int *chosenRow, int startSearchIter, UISettings *ui); int addToSearchText(const char *str); int removeFromSearchText(void); int getSearchResultsCount(void); void fuzzySearch(FileSystemEntry *root, int threshold); void freeSearchResults(void); FileSystemEntry *getCurrentSearchEntry(void); kew-3.2.0/src/searchradio_ui.c000066400000000000000000000451631500206121000162120ustar00rootroot00000000000000#include "searchradio_ui.h" /* searchradio_ui.c Radio Search UI functions. */ #define MAX_SEARCH_LEN 32 #define MAX_LINE_LENGTH 2048 const char RADIOFAVORITES_FILE[] = "kewradiofavorites"; int numRadioSearchLetters = 0; int numRadioSearchBytes = 0; RadioSearchResult *radioSearchResults = NULL; RadioSearchResult *radioFavorites = NULL; size_t radioFavoritesCount = 0; size_t radioFavoritesCapacity = 0; size_t radioResultsCount = 0; size_t radioResultsCapacity = 0; int minRadioSearchLetters = 1; RadioSearchResult *currentRadioSearchEntry = NULL; char radioSearchText[MAX_SEARCH_LEN * 4 + 1]; // Unicode can be 4 characters RadioSearchResult *getCurrentRadioSearchEntry(void) { return currentRadioSearchEntry; } int getRadioSearchResultsCount(void) { return radioResultsCount; } void addRadioResult(RadioSearchResult **radioSearchResults, size_t *radioResultsCount, size_t *radioResultsCapacity, const char *name, const char *url_resolved, const char *country, const char *codec, int bitrate, int votes) { // Only accept MP3 codec if (strcmp(codec, "MP3") != 0) return; // Check for duplicate URL for (size_t i = 0; i < *radioResultsCount; i++) { if (strcmp((*radioSearchResults)[i].url_resolved, url_resolved) == 0) { return; // Duplicate found, do not add } } // Resize array if needed if (*radioResultsCount >= *radioResultsCapacity) { *radioResultsCapacity = (*radioResultsCapacity == 0) ? 10 : (*radioResultsCapacity * 2); RadioSearchResult *tmp = realloc(*radioSearchResults, *radioResultsCapacity * sizeof(RadioSearchResult)); if (!tmp) { return; // Memory allocation failed } *radioSearchResults = tmp; } // Add new radio station RadioSearchResult *newEntry = &(*radioSearchResults)[*radioResultsCount]; strncpy(newEntry->name, name, sizeof(newEntry->name) - 1); newEntry->name[sizeof(newEntry->name) - 1] = '\0'; strncpy(newEntry->url_resolved, url_resolved, sizeof(newEntry->url_resolved) - 1); newEntry->url_resolved[sizeof(newEntry->url_resolved) - 1] = '\0'; strncpy(newEntry->country, country, sizeof(newEntry->country) - 1); newEntry->country[sizeof(newEntry->country) - 1] = '\0'; strncpy(newEntry->codec, codec, sizeof(newEntry->codec) - 1); newEntry->codec[sizeof(newEntry->codec) - 1] = '\0'; // Copy bitrate and votes newEntry->bitrate = bitrate; newEntry->votes = votes; (*radioResultsCount)++; } void removeFromFavorites(RadioSearchResult *radioSearchResults, size_t *count, const RadioSearchResult *result) { if (radioSearchResults == NULL || result == NULL || *count == 0) { return; } size_t index = -1; // Find the index for (size_t i = 0; i < *count; i++) { if (strcmp(radioSearchResults[i].name, result->name) == 0 && strcmp(radioSearchResults[i].url_resolved, result->url_resolved) == 0 && strcmp(radioSearchResults[i].country, result->country) == 0 && strcmp(radioSearchResults[i].codec, result->codec) == 0 && radioSearchResults[i].bitrate == result->bitrate && radioSearchResults[i].votes == result->votes) { index = i; break; } } if (index == (size_t)-1) { return; } for (size_t i = index; i < *count - 1; i++) { radioSearchResults[i] = radioSearchResults[i + 1]; } (*count)--; } void removeFromRadioFavorites(RadioSearchResult *result) { removeFromFavorites(radioFavorites, &radioFavoritesCount, result); } void addToRadioFavorites(RadioSearchResult *result) { addRadioResult(&radioFavorites, &radioFavoritesCount, &radioFavoritesCapacity, result->name, result->url_resolved, result->country, result->codec, result->bitrate, result->votes); } // Callback function to collect results void collectRadioResult(const char *name, const char *url_resolved, const char *country, const char *codec, const int bitrate, const int votes) { addRadioResult(&radioSearchResults, &radioResultsCount, &radioResultsCapacity, name, url_resolved, country, codec, bitrate, votes); } // Free allocated memory from previous search void freeRadioSearchResults(void) { if (radioSearchResults != NULL) { free(radioSearchResults); radioSearchResults = NULL; } if (currentRadioSearchEntry != NULL) currentRadioSearchEntry = NULL; radioResultsCapacity = 0; radioResultsCount = 0; } void radioSearch() { freeRadioSearchResults(); if (numRadioSearchLetters > minRadioSearchLetters) { if (internetRadioSearch(radioSearchText, collectRadioResult) < 0) { setErrorMessage("Radio database unavailable."); } } refresh = true; } int compareRadioResults(const void *a, const void *b) { RadioSearchResult *resultA = (RadioSearchResult *)a; RadioSearchResult *resultB = (RadioSearchResult *)b; return resultB->votes - resultA->votes; } void sortRadioResults(RadioSearchResult *radioSearchResults, size_t radioResultsCount) { qsort(radioSearchResults, radioResultsCount, sizeof(RadioSearchResult), compareRadioResults); } int displayRadioSearchBox(int indent, UISettings *ui) { if (ui->useConfigColors) setTextColor(ui->mainColor); else setColor(ui); printBlankSpaces(indent); printf(" [Radio Search]: "); setDefaultTextColor(); // Save cursor position printf("%s", radioSearchText); printf("\033[s"); printf("█\n"); return 0; } int addToRadioSearchText(const char *str) { if (str == NULL) { return -1; } size_t len = strnlen(str, MAX_SEARCH_LEN); // Check if the string can fit into the search text buffer if (numRadioSearchLetters + len > 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++) { radioSearchText[numRadioSearchBytes++] = str[i]; } radioSearchText[numRadioSearchBytes] = '\0'; // Null-terminate the buffer numRadioSearchLetters++; return 0; } // Determine the number of bytes in the last UTF-8 character int getLastRadioCharBytes(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; } bool hasRadioSearchText() { return (numRadioSearchLetters != 0); } // Remove the preceding character from the search text int removeFromRadioSearchText(void) { if (numRadioSearchLetters == 0) return 0; // Determine the number of bytes to remove for the last character int lastCharBytes = getLastRadioCharBytes(radioSearchText, numRadioSearchBytes); 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 numRadioSearchBytes -= lastCharBytes; radioSearchText[numRadioSearchBytes] = '\0'; numRadioSearchLetters--; return 0; } int displayRadioSearchResults(RadioSearchResult *radioSearchResults, size_t radioResultsCount, int maxListSize, int indent, int *chosenRow, int startSearchIter, UISettings *ui) { int term_w, term_h; getTermSize(&term_w, &term_h); int maxNameWidth = term_w - indent - 5; char name[maxNameWidth + 1]; int printedRows = 0; RadioSearchResult *currentlyPlayingStation = getCurrentPlayingRadioStation(); sortRadioResults(radioSearchResults, radioResultsCount); currentRadioSearchEntry = &radioSearchResults[0]; if (*chosenRow >= (int)radioResultsCount - 1) { *chosenRow = radioResultsCount - 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"); printedRows++; bool isFavorite = false; // Print the sorted results for (size_t i = startSearchIter; i < radioResultsCount; i++) { if ((int)i >= maxListSize + startSearchIter - 1) break; setDefaultTextColor(); printBlankSpaces(indent); if (radioSearchResults != radioFavorites) { for (size_t j = 0; j < radioFavoritesCount; j++) { isFavorite = strcmp(radioSearchResults[i].url_resolved, radioFavorites[j].url_resolved) == 0; if (isFavorite) break; } } if (*chosenRow == (int)i) { currentRadioSearchEntry = &radioSearchResults[i]; if (currentlyPlayingStation != NULL && strcmp(radioSearchResults[i].url_resolved, currentlyPlayingStation->url_resolved) == 0) { setTextColor(ui->enqueuedColor); printf("\x1b[7m * "); } else if (isFavorite) { setTextColor(ui->enqueuedColor); printf(" \x1b[7m "); } else { printf(" \x1b[7m "); } } else { if (currentlyPlayingStation != NULL && strcmp(radioSearchResults[i].url_resolved, currentlyPlayingStation->url_resolved) == 0) { setTextColor(ui->enqueuedColor); printf(" * "); } else if (isFavorite) { setTextColor(ui->enqueuedColor); printf(" "); } else printf(" "); } name[0] = '\0'; char buffer[20]; snprintf(name, maxNameWidth + 1, "%d. %s %s %s", (int)i + 1, radioSearchResults[i].name, radioSearchResults[i].bitrate == 0 ? "" : snprintf(buffer, sizeof(buffer), "[%d]", radioSearchResults[i].bitrate) > 0 ? buffer : "", radioSearchResults[i].country); printf("%s\n", name); printedRows++; } while (printedRows < maxListSize) { printf("\n"); printedRows++; } return 0; } int displayRadioSearch(int maxListSize, int indent, int *chosenRow, int startSearchIter, UISettings *ui) { displayRadioSearchBox(indent, ui); if (numRadioSearchLetters > 0) displayRadioSearchResults(radioSearchResults, radioResultsCount, maxListSize, indent, chosenRow, startSearchIter, ui); else displayRadioSearchResults(radioFavorites, radioFavoritesCount, maxListSize, indent, chosenRow, startSearchIter, ui); return 0; } void stripTripleColon(char *str) { char *src = str; char *dst = str; while (*src) { // Look for triple colons ":::" if (src[0] == ':' && src[1] == ':' && src[2] == ':') { src += 3; } else { *dst++ = *src++; } } *dst = '\0'; } void writeRadioResultsToFile(RadioSearchResult *radioFavorites, size_t count, FILE *file) { if (radioFavorites == NULL || file == NULL || count == 0) { return; } for (size_t i = 0; i < count; i++) { stripTripleColon(radioFavorites[i].name); stripTripleColon(radioFavorites[i].url_resolved); stripTripleColon(radioFavorites[i].country); stripTripleColon(radioFavorites[i].codec); char line[MAX_LINE_LENGTH]; int written = snprintf(line, sizeof(line), "%s:::%s:::%s:::%s:::%d:::%d\n", radioFavorites[i].name, radioFavorites[i].url_resolved, radioFavorites[i].country, radioFavorites[i].codec, radioFavorites[i].bitrate, radioFavorites[i].votes); // If the line is too long, skip it if (written >= MAX_LINE_LENGTH) { continue; } // Write the formatted line to the file fprintf(file, "%s", line); } } void writeRadioFavorites(RadioSearchResult *favorites, const char *filename, size_t count) { FILE *file = fopen(filename, "w"); if (!file) { return; } writeRadioResultsToFile(favorites, count, file); fclose(file); } char *getRadioFavoritesFilePath(void) { return getFilePath(RADIOFAVORITES_FILE); } void freeAndwriteRadioFavorites(void) { if (radioFavorites == NULL) return; char *filepath = getRadioFavoritesFilePath(); writeRadioFavorites(radioFavorites, filepath, radioFavoritesCount); if (radioFavorites != NULL) { free(radioFavorites); radioFavorites = NULL; } free(filepath); } void splitLineByTripleColon(char *line, char **tokens) { size_t tokenIndex = 0; char *start = line; size_t len = strlen(line); for (size_t i = 0; i < len; i++) { if (i + 2 < len && line[i] == ':' && line[i + 1] == ':' && line[i + 2] == ':') { line[i] = '\0'; tokens[tokenIndex++] = start; i += 2; start = &line[i + 1]; } } tokens[tokenIndex] = start; } RadioSearchResult *reconstructRadioFavoritesFromFile(const char *filename, size_t *count, size_t *capacity) { FILE *file = fopen(filename, "r"); if (file == NULL || count == NULL || capacity == NULL) { return NULL; } *capacity = 10; // Allocate initial memory RadioSearchResult *radioFavorites = malloc(*capacity * sizeof(RadioSearchResult)); if (radioFavorites == NULL) { return NULL; } char line[MAX_LINE_LENGTH]; while (fgets(line, sizeof(line), file)) { line[strcspn(line, "\n")] = 0; char *tokens[6] = {0}; splitLineByTripleColon(line, tokens); if (tokens[0] == NULL || tokens[1] == NULL || tokens[2] == NULL || tokens[3] == NULL || tokens[4] == NULL || tokens[5] == NULL) { continue; } strncpy(radioFavorites[*count].name, tokens[0], sizeof(radioFavorites[*count].name) - 1); strncpy(radioFavorites[*count].url_resolved, tokens[1], sizeof(radioFavorites[*count].url_resolved) - 1); strncpy(radioFavorites[*count].country, tokens[2], sizeof(radioFavorites[*count].country) - 1); strncpy(radioFavorites[*count].codec, tokens[3], sizeof(radioFavorites[*count].codec) - 1); radioFavorites[*count].bitrate = atoi(tokens[4]); radioFavorites[*count].votes = atoi(tokens[5]); (*count)++; // Resize the array if needed if (*count >= *capacity) { *capacity *= 2; RadioSearchResult *tmp = realloc(radioFavorites, *capacity * sizeof(RadioSearchResult)); if (!tmp) { free(radioFavorites); fclose(file); return NULL; } radioFavorites = tmp; } } fclose(file); return radioFavorites; } void createRadioFavorites(void) { char *favoritesFilepath = getRadioFavoritesFilePath(); radioFavorites = reconstructRadioFavoritesFromFile(favoritesFilepath, &radioFavoritesCount, &radioFavoritesCapacity); free(favoritesFilepath); } kew-3.2.0/src/searchradio_ui.h000066400000000000000000000014021500206121000162030ustar00rootroot00000000000000#include #include #include "soundcommon.h" #include "directorytree.h" #include "term.h" #include "common_ui.h" #include "common.h" #include "soundradio.h" extern bool newUndisplayedRadioSearch; int displayRadioSearch(int maxListSize, int indent, int *chosenRow, int startSearchIter, UISettings *ui); int addToRadioSearchText(const char *str); int removeFromRadioSearchText(void); int getRadioSearchResultsCount(void); bool hasRadioSearchText(); void radioSearch(); void freeRadioSearchResults(void); void freeAndwriteRadioFavorites(void); void createRadioFavorites(void); void addToRadioFavorites(RadioSearchResult *result); void removeFromRadioFavorites(RadioSearchResult *result); RadioSearchResult *getCurrentRadioSearchEntry(void); kew-3.2.0/src/settings.c000066400000000000000000001441421500206121000150660ustar00rootroot00000000000000#include "settings.h" /* settings.c Functions related to the config file. */ #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif const char SETTINGS_FILE[] = "kewrc"; time_t lastTimeAppRan; AppSettings settings; 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)); c_strcpy(settings.coverEnabled, "1", sizeof(settings.coverEnabled)); c_strcpy(settings.allowNotifications, "1", sizeof(settings.allowNotifications)); c_strcpy(settings.coverAnsi, "0", sizeof(settings.coverAnsi)); c_strcpy(settings.quitAfterStopping, "0", sizeof(settings.quitAfterStopping)); c_strcpy(settings.hideGlimmeringText, "0", sizeof(settings.hideGlimmeringText)); c_strcpy(settings.mouseEnabled, "1", sizeof(settings.mouseEnabled)); c_strcpy(settings.visualizerBrailleMode, "0", sizeof(settings.visualizerBrailleMode)); c_strcpy(settings.tweenFactor, "0.23", sizeof(settings.tweenFactor)); c_strcpy(settings.tweenFactorFall, "0.13", sizeof(settings.tweenFactor)); c_strcpy(settings.progressBarType, "0", sizeof(settings.progressBarType)); #ifdef __APPLE__ c_strcpy(settings.visualizerEnabled, "0", sizeof(settings.visualizerEnabled)); // Visualizer looks wonky in default terminal c_strcpy(settings.useConfigColors, "1", sizeof(settings.useConfigColors)); // Colors from album look wrong in default terminal #else c_strcpy(settings.visualizerEnabled, "1", sizeof(settings.visualizerEnabled)); c_strcpy(settings.useConfigColors, "0", sizeof(settings.useConfigColors)); #endif c_strcpy(settings.hideLogo, "0", sizeof(settings.hideLogo)); c_strcpy(settings.hideHelp, "0", sizeof(settings.hideHelp)); c_strcpy(settings.cacheLibrary, "-1", sizeof(settings.cacheLibrary)); c_strcpy(settings.visualizerHeight, "5", sizeof(settings.visualizerHeight)); c_strcpy(settings.visualizerColorType, "0", sizeof(settings.visualizerColorType)); c_strcpy(settings.titleDelay, "9", sizeof(settings.titleDelay)); c_strcpy(settings.nextView, "\t", sizeof(settings.nextView)); c_strcpy(settings.prevView, "[Z", sizeof(settings.prevView)); c_strcpy(settings.volumeUp, "+", sizeof(settings.volumeUp)); c_strcpy(settings.volumeUpAlt, "=", sizeof(settings.volumeUpAlt)); c_strcpy(settings.volumeDown, "-", sizeof(settings.volumeDown)); c_strcpy(settings.previousTrackAlt, "h", sizeof(settings.previousTrackAlt)); c_strcpy(settings.nextTrackAlt, "l", sizeof(settings.nextTrackAlt)); c_strcpy(settings.scrollUpAlt, "k", sizeof(settings.scrollUpAlt)); c_strcpy(settings.scrollDownAlt, "j", sizeof(settings.scrollDownAlt)); c_strcpy(settings.toggleColorsDerivedFrom, "i", sizeof(settings.toggleColorsDerivedFrom)); c_strcpy(settings.toggleVisualizer, "v", sizeof(settings.toggleVisualizer)); c_strcpy(settings.toggleAscii, "b", sizeof(settings.toggleAscii)); c_strcpy(settings.toggleRepeat, "r", sizeof(settings.toggleRepeat)); c_strcpy(settings.toggleShuffle, "s", sizeof(settings.toggleShuffle)); c_strcpy(settings.togglePause, "p", sizeof(settings.togglePause)); c_strcpy(settings.seekBackward, "a", sizeof(settings.seekBackward)); c_strcpy(settings.seekForward, "d", sizeof(settings.seekForward)); c_strcpy(settings.savePlaylist, "x", sizeof(settings.savePlaylist)); c_strcpy(settings.updateLibrary, "u", sizeof(settings.updateLibrary)); c_strcpy(settings.addToMainPlaylist, ".", sizeof(settings.addToMainPlaylist)); c_strcpy(settings.hardPlayPause, " ", sizeof(settings.hardPlayPause)); c_strcpy(settings.hardSwitchNumberedSong, "\n", sizeof(settings.hardSwitchNumberedSong)); c_strcpy(settings.hardPrev, "[D", sizeof(settings.hardPrev)); c_strcpy(settings.hardNext, "[C", sizeof(settings.hardNext)); c_strcpy(settings.hardScrollUp, "[A", sizeof(settings.hardScrollUp)); c_strcpy(settings.hardScrollDown, "[B", sizeof(settings.hardScrollDown)); c_strcpy(settings.hardShowPlaylist, "OQ", sizeof(settings.hardShowPlaylist)); c_strcpy(settings.hardShowPlaylistAlt, "[[B", sizeof(settings.hardShowPlaylistAlt)); c_strcpy(settings.showPlaylistAlt, "Z", sizeof(settings.showPlaylistAlt)); c_strcpy(settings.hardShowKeys, "[18~", sizeof(settings.hardShowKeys)); c_strcpy(settings.hardShowKeysAlt, "[18~", sizeof(settings.hardShowKeysAlt)); c_strcpy(settings.showKeysAlt, "N", sizeof(settings.showKeysAlt)); c_strcpy(settings.hardShowTrack, "OS", sizeof(settings.hardShowTrack)); c_strcpy(settings.hardShowTrackAlt, "[[D", sizeof(settings.hardShowTrackAlt)); c_strcpy(settings.showTrackAlt, "C", sizeof(settings.showTrackAlt)); c_strcpy(settings.hardShowLibrary, "OR", sizeof(settings.hardShowLibrary)); c_strcpy(settings.hardShowLibraryAlt, "[[C", sizeof(settings.hardShowLibraryAlt)); c_strcpy(settings.showLibraryAlt, "X", sizeof(settings.showLibraryAlt)); c_strcpy(settings.hardShowSearch, "[15~", sizeof(settings.hardShowSearch)); c_strcpy(settings.hardShowSearchAlt, "[[E", sizeof(settings.hardShowSearchAlt)); c_strcpy(settings.hardShowRadioSearch, "[17~", sizeof(settings.hardShowSearch)); c_strcpy(settings.hardShowRadioSearchAlt, "B", sizeof(settings.hardShowSearchAlt)); c_strcpy(settings.showSearchAlt, "V", sizeof(settings.showSearchAlt)); c_strcpy(settings.showRadioSearchAlt, "B", sizeof(settings.showSearchAlt)); c_strcpy(settings.hardNextPage, "[6~", sizeof(settings.hardNextPage)); c_strcpy(settings.hardPrevPage, "[5~", sizeof(settings.hardPrevPage)); c_strcpy(settings.hardRemove, "[3~", sizeof(settings.hardRemove)); c_strcpy(settings.hardRemove2, "[P", sizeof(settings.hardRemove2)); c_strcpy(settings.mouseLeftClick, "[M ", sizeof(settings.mouseLeftClick)); c_strcpy(settings.mouseMiddleClick, "[M!", sizeof(settings.mouseMiddleClick)); c_strcpy(settings.mouseRightClick, "[M\"", sizeof(settings.mouseRightClick)); c_strcpy(settings.mouseScrollUp, "[M`", sizeof(settings.mouseScrollUp)); c_strcpy(settings.mouseScrollDown, "[Ma", sizeof(settings.mouseScrollDown)); c_strcpy(settings.mouseAltScrollUp, "[Mh", sizeof(settings.mouseAltScrollUp)); c_strcpy(settings.mouseAltScrollDown, "[Mi", sizeof(settings.mouseAltScrollDown)); c_strcpy(settings.lastVolume, "100", sizeof(settings.lastVolume)); c_strcpy(settings.color, "6", sizeof(settings.color)); c_strcpy(settings.artistColor, "6", sizeof(settings.artistColor)); c_strcpy(settings.titleColor, "6", sizeof(settings.titleColor)); c_strcpy(settings.enqueuedColor, "6", sizeof(settings.enqueuedColor)); c_strcpy(settings.mouseLeftClickAction, "0", sizeof(settings.mouseLeftClickAction)); c_strcpy(settings.mouseMiddleClickAction, "1", sizeof(settings.mouseMiddleClickAction)); c_strcpy(settings.mouseRightClickAction, "2", sizeof(settings.mouseRightClickAction)); c_strcpy(settings.mouseScrollUpAction, "3", sizeof(settings.mouseScrollUpAction)); c_strcpy(settings.mouseScrollDownAction, "4", sizeof(settings.mouseScrollDownAction)); c_strcpy(settings.mouseAltScrollUpAction, "7", sizeof(settings.mouseAltScrollUpAction)); c_strcpy(settings.mouseAltScrollDownAction, "8", sizeof(settings.mouseAltScrollDownAction)); c_strcpy(settings.moveSongUp, "t", sizeof(settings.moveSongUp)); c_strcpy(settings.moveSongDown, "g", sizeof(settings.moveSongDown)); c_strcpy(settings.enqueueAndPlay, "^M", sizeof(settings.enqueueAndPlay)); c_strcpy(settings.hardAddToRadioFavorites, "F", sizeof(settings.hardAddToRadioFavorites)); c_strcpy(settings.hardStop, "S", sizeof(settings.hardStop)); c_strcpy(settings.sortLibrary, "o", sizeof(settings.sortLibrary)); c_strcpy(settings.quit, "q", sizeof(settings.quit)); c_strcpy(settings.hardQuit, "\x1B", sizeof(settings.hardQuit)); c_strcpy(settings.hardClearPlaylist, "\b", sizeof(settings.hardClearPlaylist)); if (pairs == NULL) { return settings; } for (int i = 0; i < count; i++) { KeyValuePair *pair = &pairs[i]; char *lowercaseKey = stringToLower(pair->key); if (strcmp(lowercaseKey, "path") == 0) { snprintf(settings.path, sizeof(settings.path), "%s", pair->value); } else if (strcmp(lowercaseKey, "coverenabled") == 0) { snprintf(settings.coverEnabled, sizeof(settings.coverEnabled), "%s", pair->value); } else if (strcmp(lowercaseKey, "coveransi") == 0) { snprintf(settings.coverAnsi, sizeof(settings.coverAnsi), "%s", pair->value); } else if (strcmp(lowercaseKey, "visualizerenabled") == 0) { snprintf(settings.visualizerEnabled, sizeof(settings.visualizerEnabled), "%s", pair->value); } else if (strcmp(lowercaseKey, "useconfigcolors") == 0) { snprintf(settings.useConfigColors, sizeof(settings.useConfigColors), "%s", pair->value); } else if (strcmp(lowercaseKey, "visualizerheight") == 0) { snprintf(settings.visualizerHeight, sizeof(settings.visualizerHeight), "%s", pair->value); } else if (strcmp(lowercaseKey, "visualizercolortype") == 0) { snprintf(settings.visualizerColorType, sizeof(settings.visualizerColorType), "%s", pair->value); } else if (strcmp(lowercaseKey, "titledelay") == 0) { snprintf(settings.titleDelay, sizeof(settings.titleDelay), "%s", pair->value); } else if (strcmp(lowercaseKey, "volumeup") == 0) { snprintf(settings.volumeUp, sizeof(settings.volumeUp), "%s", pair->value); } else if (strcmp(lowercaseKey, "volumeupalt") == 0) { snprintf(settings.volumeUpAlt, sizeof(settings.volumeUpAlt), "%s", pair->value); } else if (strcmp(lowercaseKey, "volumedown") == 0) { snprintf(settings.volumeDown, sizeof(settings.volumeDown), "%s", pair->value); } else if (strcmp(lowercaseKey, "previoustrackalt") == 0) { snprintf(settings.previousTrackAlt, sizeof(settings.previousTrackAlt), "%s", pair->value); } else if (strcmp(lowercaseKey, "nexttrackalt") == 0) { snprintf(settings.nextTrackAlt, sizeof(settings.nextTrackAlt), "%s", pair->value); } else if (strcmp(lowercaseKey, "scrollupalt") == 0) { snprintf(settings.scrollUpAlt, sizeof(settings.scrollUpAlt), "%s", pair->value); } else if (strcmp(lowercaseKey, "scrolldownalt") == 0) { snprintf(settings.scrollDownAlt, sizeof(settings.scrollDownAlt), "%s", pair->value); } else if (strcmp(lowercaseKey, "switchnumberedsong") == 0) { snprintf(settings.switchNumberedSong, sizeof(settings.switchNumberedSong), "%s", pair->value); } else if (strcmp(lowercaseKey, "togglepause") == 0) { snprintf(settings.togglePause, sizeof(settings.togglePause), "%s", pair->value); } else if (strcmp(lowercaseKey, "togglecolorsderivedfrom") == 0) { snprintf(settings.toggleColorsDerivedFrom, sizeof(settings.toggleColorsDerivedFrom), "%s", pair->value); } else if (strcmp(lowercaseKey, "togglevisualizer") == 0) { snprintf(settings.toggleVisualizer, sizeof(settings.toggleVisualizer), "%s", pair->value); } else if (strcmp(lowercaseKey, "toggleascii") == 0) { snprintf(settings.toggleAscii, sizeof(settings.toggleAscii), "%s", pair->value); } else if (strcmp(lowercaseKey, "togglerepeat") == 0) { snprintf(settings.toggleRepeat, sizeof(settings.toggleRepeat), "%s", pair->value); } else if (strcmp(lowercaseKey, "toggleshuffle") == 0) { snprintf(settings.toggleShuffle, sizeof(settings.toggleShuffle), "%s", pair->value); } else if (strcmp(lowercaseKey, "seekbackward") == 0) { snprintf(settings.seekBackward, sizeof(settings.seekBackward), "%s", pair->value); } else if (strcmp(lowercaseKey, "seekforward") == 0) { snprintf(settings.seekForward, sizeof(settings.seekForward), "%s", pair->value); } else if (strcmp(lowercaseKey, "saveplaylist") == 0) { snprintf(settings.savePlaylist, sizeof(settings.savePlaylist), "%s", pair->value); } else if (strcmp(lowercaseKey, "addtomainplaylist") == 0) { snprintf(settings.quit, sizeof(settings.quit), "%s", pair->value); } else if (strcmp(lowercaseKey, "lastvolume") == 0) { snprintf(settings.lastVolume, sizeof(settings.lastVolume), "%s", pair->value); } else if (strcmp(lowercaseKey, "allownotifications") == 0) { snprintf(settings.allowNotifications, sizeof(settings.allowNotifications), "%s", pair->value); } else if (strcmp(lowercaseKey, "color") == 0) { snprintf(settings.color, sizeof(settings.color), "%s", pair->value); } else if (strcmp(lowercaseKey, "artistcolor") == 0) { snprintf(settings.artistColor, sizeof(settings.artistColor), "%s", pair->value); } else if (strcmp(lowercaseKey, "enqueuedcolor") == 0) { snprintf(settings.enqueuedColor, sizeof(settings.enqueuedColor), "%s", pair->value); } else if (strcmp(lowercaseKey, "titlecolor") == 0) { snprintf(settings.titleColor, sizeof(settings.titleColor), "%s", pair->value); } else if (strcmp(lowercaseKey, "mouseenabled") == 0) { snprintf(settings.mouseEnabled, sizeof(settings.mouseEnabled), "%s", pair->value); } else if (strcmp(lowercaseKey, "visualizerbraillemode") == 0) { snprintf(settings.visualizerBrailleMode, sizeof(settings.visualizerBrailleMode), "%s", pair->value); } else if (strcmp(lowercaseKey, "mouseleftclickaction") == 0) { snprintf(settings.mouseLeftClickAction, sizeof(settings.mouseLeftClickAction), "%s", pair->value); } else if (strcmp(lowercaseKey, "mousemiddleclickaction") == 0) { snprintf(settings.mouseMiddleClickAction, sizeof(settings.mouseMiddleClickAction), "%s", pair->value); } else if (strcmp(lowercaseKey, "mouserightclickaction") == 0) { snprintf(settings.mouseRightClickAction, sizeof(settings.mouseRightClickAction), "%s", pair->value); } else if (strcmp(lowercaseKey, "mousescrollupaction") == 0) { snprintf(settings.mouseScrollUpAction, sizeof(settings.mouseScrollUpAction), "%s", pair->value); } else if (strcmp(lowercaseKey, "mousescrolldownaction") == 0) { snprintf(settings.mouseScrollDownAction, sizeof(settings.mouseScrollDownAction), "%s", pair->value); } else if (strcmp(lowercaseKey, "mousealtscrollupaction") == 0) { snprintf(settings.mouseAltScrollUpAction, sizeof(settings.mouseAltScrollUpAction), "%s", pair->value); } else if (strcmp(lowercaseKey, "mousealtscrolldownaction") == 0) { snprintf(settings.mouseAltScrollDownAction, sizeof(settings.mouseAltScrollDownAction), "%s", pair->value); } else if (strcmp(lowercaseKey, "hidelogo") == 0) { snprintf(settings.hideLogo, sizeof(settings.hideLogo), "%s", pair->value); } else if (strcmp(lowercaseKey, "hidehelp") == 0) { snprintf(settings.hideHelp, sizeof(settings.hideHelp), "%s", pair->value); } else if (strcmp(lowercaseKey, "cachelibrary") == 0) { snprintf(settings.cacheLibrary, sizeof(settings.cacheLibrary), "%s", pair->value); } else if (strcmp(lowercaseKey, "quitonstop") == 0) { snprintf(settings.quitAfterStopping, sizeof(settings.quitAfterStopping), "%s", pair->value); } else if (strcmp(lowercaseKey, "hideglimmeringtext") == 0) { snprintf(settings.hideGlimmeringText, sizeof(settings.hideGlimmeringText), "%s", pair->value); } else if (strcmp(lowercaseKey, "quit") == 0) { snprintf(settings.quit, sizeof(settings.quit), "%s", pair->value); } else if (strcmp(lowercaseKey, "updatelibrary") == 0) { snprintf(settings.updateLibrary, sizeof(settings.updateLibrary), "%s", pair->value); } else if (strcmp(lowercaseKey, "showplaylistalt") == 0) { if (strcmp(pair->value, "") != 0) // Don't set these to nothing snprintf(settings.showPlaylistAlt, sizeof(settings.showPlaylistAlt), "%s", pair->value); } else if (strcmp(lowercaseKey, "showlibraryalt") == 0) { if (strcmp(pair->value, "") != 0) snprintf(settings.showLibraryAlt, sizeof(settings.showLibraryAlt), "%s", pair->value); } else if (strcmp(lowercaseKey, "showtrackalt") == 0) { if (strcmp(pair->value, "") != 0) snprintf(settings.showTrackAlt, sizeof(settings.showTrackAlt), "%s", pair->value); } else if (strcmp(lowercaseKey, "showsearchalt") == 0) { if (strcmp(pair->value, "") != 0) snprintf(settings.showSearchAlt, sizeof(settings.showSearchAlt), "%s", pair->value); } else if (strcmp(lowercaseKey, "movesongup") == 0) { if (strcmp(pair->value, "") != 0) snprintf(settings.moveSongUp, sizeof(settings.moveSongUp), "%s", pair->value); } else if (strcmp(lowercaseKey, "movesongdown") == 0) { if (strcmp(pair->value, "") != 0) snprintf(settings.moveSongDown, sizeof(settings.moveSongDown), "%s", pair->value); } else if (strcmp(lowercaseKey, "enqueueandplay") == 0) { if (strcmp(pair->value, "") != 0) snprintf(settings.enqueueAndPlay, sizeof(settings.enqueueAndPlay), "%s", pair->value); } else if (strcmp(lowercaseKey, "sort") == 0) { if (strcmp(pair->value, "") != 0) snprintf(settings.sortLibrary, sizeof(settings.sortLibrary), "%s", pair->value); } else if (strcmp(lowercaseKey, "tweenfactor") == 0) { if (strcmp(pair->value, "") != 0) snprintf(settings.tweenFactor, sizeof(settings.tweenFactor), "%s", pair->value); } else if (strcmp(lowercaseKey, "tweenfactorfall") == 0) { if (strcmp(pair->value, "") != 0) snprintf(settings.tweenFactorFall, sizeof(settings.tweenFactorFall), "%s", pair->value); } else if (strcmp(lowercaseKey, "progressbartype") == 0) { if (strcmp(pair->value, "") != 0) snprintf(settings.progressBarType, sizeof(settings.progressBarType), "%s", pair->value); } else if (strcmp(lowercaseKey, "showkeysalt") == 0 && strcmp(pair->value, "B") != 0) { // We need to prevent the previous key B or else config files wont get updated // to the new key N and B for radio search on macOS if (strcmp(pair->value, "") != 0) snprintf(settings.showKeysAlt, sizeof(settings.showKeysAlt), "%s", pair->value); } free(lowercaseKey); } freeKeyValuePairs(pairs, count); return settings; } KeyValuePair *readKeyValuePairs(const char *file_path, int *count, time_t *lastTimeAppRan) { FILE *file = fopen(file_path, "r"); if (file == NULL) { return NULL; } struct stat file_stat; if (stat(file_path, &file_stat) == -1) { perror("stat"); return NULL; } // Save the modification time (mtime) of the file #ifdef __APPLE__ *lastTimeAppRan = (file_stat.st_mtime > 0) ? file_stat.st_mtime : file_stat.st_mtimespec.tv_sec; #else *lastTimeAppRan = (file_stat.st_mtime > 0) ? file_stat.st_mtime : file_stat.st_mtim.tv_sec; #endif 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(void) { 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) { c_strcpy(path, expandedPath, sizeof(expandedPath)); } } return 0; } void mapSettingsToKeys(AppSettings *settings, UISettings *ui, EventMapping *mappings) { mappings[0] = (EventMapping){settings->scrollUpAlt, EVENT_SCROLLPREV}; mappings[1] = (EventMapping){settings->scrollDownAlt, EVENT_SCROLLNEXT}; mappings[2] = (EventMapping){settings->nextTrackAlt, EVENT_NEXT}; mappings[3] = (EventMapping){settings->previousTrackAlt, EVENT_PREV}; mappings[4] = (EventMapping){settings->volumeUp, EVENT_VOLUME_UP}; mappings[5] = (EventMapping){settings->volumeUpAlt, EVENT_VOLUME_UP}; mappings[6] = (EventMapping){settings->volumeDown, EVENT_VOLUME_DOWN}; mappings[7] = (EventMapping){settings->togglePause, EVENT_PLAY_PAUSE}; mappings[8] = (EventMapping){settings->quit, EVENT_QUIT}; mappings[9] = (EventMapping){settings->hardQuit, EVENT_QUIT}; mappings[10] = (EventMapping){settings->toggleShuffle, EVENT_SHUFFLE}; mappings[11] = (EventMapping){settings->toggleVisualizer, EVENT_TOGGLEVISUALIZER}; mappings[12] = (EventMapping){settings->toggleAscii, EVENT_TOGGLEASCII}; mappings[13] = (EventMapping){settings->switchNumberedSong, EVENT_GOTOSONG}; mappings[14] = (EventMapping){settings->seekBackward, EVENT_SEEKBACK}; mappings[15] = (EventMapping){settings->seekForward, EVENT_SEEKFORWARD}; mappings[16] = (EventMapping){settings->toggleRepeat, EVENT_TOGGLEREPEAT}; mappings[17] = (EventMapping){settings->savePlaylist, EVENT_EXPORTPLAYLIST}; mappings[18] = (EventMapping){settings->toggleColorsDerivedFrom, EVENT_TOGGLEPROFILECOLORS}; mappings[19] = (EventMapping){settings->addToMainPlaylist, EVENT_ADDTOMAINPLAYLIST}; mappings[20] = (EventMapping){settings->updateLibrary, EVENT_UPDATELIBRARY}; mappings[21] = (EventMapping){settings->hardPlayPause, EVENT_PLAY_PAUSE}; mappings[22] = (EventMapping){settings->hardPrev, EVENT_PREV}; mappings[23] = (EventMapping){settings->hardNext, EVENT_NEXT}; mappings[24] = (EventMapping){settings->hardSwitchNumberedSong, EVENT_GOTOSONG}; mappings[25] = (EventMapping){settings->hardScrollUp, EVENT_SCROLLPREV}; mappings[26] = (EventMapping){settings->hardScrollDown, EVENT_SCROLLNEXT}; mappings[27] = (EventMapping){settings->hardShowPlaylist, EVENT_SHOWPLAYLIST}; mappings[28] = (EventMapping){settings->hardShowPlaylistAlt, EVENT_SHOWPLAYLIST}; mappings[29] = (EventMapping){settings->showPlaylistAlt, EVENT_SHOWPLAYLIST}; mappings[30] = (EventMapping){settings->hardShowKeys, EVENT_SHOWKEYBINDINGS}; mappings[31] = (EventMapping){settings->hardShowKeysAlt, EVENT_SHOWKEYBINDINGS}; mappings[32] = (EventMapping){settings->showKeysAlt, EVENT_SHOWKEYBINDINGS}; mappings[33] = (EventMapping){settings->hardShowTrack, EVENT_SHOWTRACK}; mappings[34] = (EventMapping){settings->hardShowTrackAlt, EVENT_SHOWTRACK}; mappings[35] = (EventMapping){settings->showTrackAlt, EVENT_SHOWTRACK}; mappings[36] = (EventMapping){settings->hardShowLibrary, EVENT_SHOWLIBRARY}; mappings[37] = (EventMapping){settings->hardShowLibraryAlt, EVENT_SHOWLIBRARY}; mappings[38] = (EventMapping){settings->showLibraryAlt, EVENT_SHOWLIBRARY}; mappings[39] = (EventMapping){settings->hardShowSearch, EVENT_SHOWSEARCH}; mappings[40] = (EventMapping){settings->hardShowSearchAlt, EVENT_SHOWSEARCH}; mappings[41] = (EventMapping){settings->showSearchAlt, EVENT_SHOWSEARCH}; mappings[42] = (EventMapping){settings->hardNextPage, EVENT_NEXTPAGE}; mappings[43] = (EventMapping){settings->hardPrevPage, EVENT_PREVPAGE}; mappings[44] = (EventMapping){settings->hardRemove, EVENT_REMOVE}; mappings[45] = (EventMapping){settings->hardRemove2, EVENT_REMOVE}; mappings[46] = (EventMapping){settings->nextView, EVENT_NEXTVIEW}; mappings[47] = (EventMapping){settings->prevView, EVENT_PREVVIEW}; mappings[48] = (EventMapping){settings->mouseLeftClick, ui->mouseLeftClickAction}; mappings[49] = (EventMapping){settings->mouseMiddleClick, ui->mouseMiddleClickAction}; mappings[50] = (EventMapping){settings->mouseRightClick, ui->mouseRightClickAction}; mappings[51] = (EventMapping){settings->mouseScrollUp, ui->mouseScrollUpAction}; mappings[52] = (EventMapping){settings->mouseScrollDown, ui->mouseScrollDownAction}; mappings[53] = (EventMapping){settings->mouseAltScrollUp, ui->mouseAltScrollUpAction}; mappings[54] = (EventMapping){settings->mouseAltScrollDown, ui->mouseAltScrollDownAction}; mappings[55] = (EventMapping){settings->hardClearPlaylist, EVENT_CLEARPLAYLIST}; mappings[56] = (EventMapping){settings->showRadioSearchAlt, EVENT_SHOWRADIOSEARCH}; mappings[57] = (EventMapping){settings->hardShowRadioSearch, EVENT_SHOWRADIOSEARCH}; mappings[58] = (EventMapping){settings->hardShowRadioSearchAlt, EVENT_SHOWRADIOSEARCH}; mappings[59] = (EventMapping){settings->moveSongUp, EVENT_MOVESONGUP}; mappings[60] = (EventMapping){settings->moveSongDown, EVENT_MOVESONGDOWN}; mappings[61] = (EventMapping){settings->enqueueAndPlay, EVENT_ENQUEUEANDPLAY}; mappings[62] = (EventMapping){settings->hardStop, EVENT_STOP}; mappings[63] = (EventMapping){settings->hardAddToRadioFavorites, EVENT_ADDTORADIOFAVORITES}; mappings[64] = (EventMapping){settings->sortLibrary, EVENT_SORTLIBRARY}; } char *getConfigFilePath(char *configdir) { size_t configdir_length = strnlen(configdir, MAXPATHLEN - 1); size_t settings_file_length = strnlen(SETTINGS_FILE, sizeof(SETTINGS_FILE) - 1); if (configdir_length + 1 + settings_file_length + 1 > MAXPATHLEN) { fprintf(stderr, "Error: File path exceeds maximum length.\n"); exit(1); } char *filepath = (char *)malloc(MAXPATHLEN); if (filepath == NULL) { perror("malloc"); exit(1); } int written = snprintf(filepath, MAXPATHLEN, "%s/%s", configdir, SETTINGS_FILE); if (written < 0 || written >= MAXPATHLEN) { fprintf(stderr, "Error: snprintf failed or filepath truncated.\n"); free(filepath); exit(1); } return filepath; } enum EventType getMouseAction(int num) { enum EventType value = EVENT_NONE; switch (num) { case 0: value = EVENT_NONE; break; case 1: value = EVENT_GOTOSONG; break; case 2: value = EVENT_PLAY_PAUSE; break; case 3: value = EVENT_SCROLLPREV; break; case 4: value = EVENT_SCROLLNEXT; break; case 5: value = EVENT_SEEKFORWARD; break; case 6: value = EVENT_SEEKBACK; break; case 7: value = EVENT_VOLUME_UP; break; case 8: value = EVENT_VOLUME_DOWN; break; case 9: value = EVENT_NEXTVIEW; break; case 10: value = EVENT_PREVVIEW; break; default: value = EVENT_NONE; break; } return value; } int mkdir_p(const char *path, mode_t mode) { if (path == NULL) return -1; if (path[0] == '~') { // Just try a plain mkdir if there's a tilde if (mkdir(path, mode) == -1) { if (errno != EEXIST) return -1; } return 0; } char tmp[PATH_MAX]; char *p = NULL; size_t len; snprintf(tmp, sizeof(tmp), "%s", path); len = strlen(tmp); if (len > 0 && tmp[len - 1] == '/') tmp[len - 1] = 0; for (p = tmp + 1; *p; p++) { if (*p == '/') { *p = 0; if (mkdir(tmp, mode) == -1) { if (errno != EEXIST) return -1; } *p = '/'; } } if (mkdir(tmp, mode) == -1) { if (errno != EEXIST) return -1; } return 0; } void getConfig(AppSettings *settings, UISettings *ui) { int pair_count; char *configdir = getConfigPath(); // Create the directory if it doesn't exist struct stat st = {0}; if (stat(configdir, &st) == -1) { if (mkdir_p(configdir, 0700) != 0) { perror("mkdir"); exit(1); } } char *filepath = getConfigFilePath(configdir); KeyValuePair *pairs = readKeyValuePairs(filepath, &pair_count, &(ui->lastTimeAppRan)); free(filepath); *settings = constructAppSettings(pairs, pair_count); ui->allowNotifications = (settings->allowNotifications[0] == '1'); ui->coverEnabled = (settings->coverEnabled[0] == '1'); ui->coverAnsi = (settings->coverAnsi[0] == '1'); ui->visualizerEnabled = (settings->visualizerEnabled[0] == '1'); ui->useConfigColors = (settings->useConfigColors[0] == '1'); ui->quitAfterStopping = (settings->quitAfterStopping[0] == '1'); ui->hideGlimmeringText = (settings->hideGlimmeringText[0] == '1'); ui->mouseEnabled = (settings->mouseEnabled[0] == '1'); ui->visualizerBrailleMode = (settings->visualizerBrailleMode[0] == '1'); ui->hideLogo = (settings->hideLogo[0] == '1'); ui->hideHelp = (settings->hideHelp[0] == '1'); int tmp = getNumber(settings->color); if (tmp >= 0) ui->mainColor = tmp; tmp = getNumber(settings->artistColor); if (tmp >= 0) ui->artistColor = tmp; tmp = getNumber(settings->enqueuedColor); if (tmp >= 0) ui->enqueuedColor = tmp; tmp = getNumber(settings->titleColor); if (tmp >= 0) ui->titleColor = tmp; tmp = getNumber(settings->mouseLeftClickAction); enum EventType tmpEvent = getMouseAction(tmp); if (tmp >= 0) ui->mouseLeftClickAction = tmpEvent; tmp = getNumber(settings->mouseMiddleClickAction); tmpEvent = getMouseAction(tmp); if (tmp >= 0) ui->mouseMiddleClickAction = tmpEvent; tmp = getNumber(settings->mouseRightClickAction); tmpEvent = getMouseAction(tmp); if (tmp >= 0) ui->mouseRightClickAction = tmpEvent; tmp = getNumber(settings->mouseScrollUpAction); tmpEvent = getMouseAction(tmp); if (tmp >= 0) ui->mouseScrollUpAction = tmpEvent; tmp = getNumber(settings->mouseScrollDownAction); tmpEvent = getMouseAction(tmp); if (tmp >= 0) ui->mouseScrollDownAction = tmpEvent; tmp = getNumber(settings->mouseAltScrollUpAction); tmpEvent = getMouseAction(tmp); if (tmp >= 0) ui->mouseAltScrollUpAction = tmpEvent; tmp = getNumber(settings->mouseAltScrollDownAction); tmpEvent = getMouseAction(tmp); if (tmp >= 0) ui->mouseAltScrollDownAction = tmpEvent; tmp = getNumber(settings->visualizerHeight); if (tmp > 0) ui->visualizerHeight = tmp; tmp = getNumber(settings->visualizerColorType); if (tmp >= 0) ui->visualizerColorType = tmp; tmp = getNumber(settings->progressBarType); if (tmp >= 0) ui->progressBarType = tmp; tmp = getNumber(settings->titleDelay); if (tmp >= 0) ui->titleDelay = tmp; tmp = getNumber(settings->lastVolume); if (tmp >= 0) setVolume(tmp); tmp = getNumber(settings->cacheLibrary); if (tmp >= 0) ui->cacheLibrary = tmp; float tmpFloat = getFloat(settings->tweenFactor); if (tmpFloat >= 0.0f) ui->tweenFactor = tmpFloat; tmpFloat = getFloat(settings->tweenFactorFall); if (tmpFloat >= 0.0f) ui->tweenFactorFall = tmpFloat; getMusicLibraryPath(settings->path); free(configdir); } void setConfig(AppSettings *settings, UISettings *ui) { // Create the file path char *configdir = getConfigPath(); char *filepath = getConfigFilePath(configdir); FILE *file = fopen(filepath, "w"); if (file == NULL) { fprintf(stderr, "Error opening file: %s\n", filepath); free(filepath); free(configdir); return; } // Make sure strings are valid before writing settings to the file if (settings->allowNotifications[0] == '\0') ui->allowNotifications ? c_strcpy(settings->allowNotifications, "1", sizeof(settings->allowNotifications)) : c_strcpy(settings->allowNotifications, "0", sizeof(settings->allowNotifications)); if (settings->coverEnabled[0] == '\0') ui->coverEnabled ? c_strcpy(settings->coverEnabled, "1", sizeof(settings->coverEnabled)) : c_strcpy(settings->coverEnabled, "0", sizeof(settings->coverEnabled)); if (settings->coverAnsi[0] == '\0') ui->coverAnsi ? c_strcpy(settings->coverAnsi, "1", sizeof(settings->coverAnsi)) : c_strcpy(settings->coverAnsi, "0", sizeof(settings->coverAnsi)); if (settings->visualizerEnabled[0] == '\0') ui->visualizerEnabled ? c_strcpy(settings->visualizerEnabled, "1", sizeof(settings->visualizerEnabled)) : c_strcpy(settings->visualizerEnabled, "0", sizeof(settings->visualizerEnabled)); if (settings->useConfigColors[0] == '\0') ui->useConfigColors ? c_strcpy(settings->useConfigColors, "1", sizeof(settings->useConfigColors)) : c_strcpy(settings->useConfigColors, "0", sizeof(settings->useConfigColors)); if (settings->quitAfterStopping[0] == '\0') ui->quitAfterStopping ? c_strcpy(settings->quitAfterStopping, "1", sizeof(settings->quitAfterStopping)) : c_strcpy(settings->quitAfterStopping, "0", sizeof(settings->quitAfterStopping)); if (settings->hideGlimmeringText[0] == '\0') ui->hideGlimmeringText ? c_strcpy(settings->hideGlimmeringText, "1", sizeof(settings->hideGlimmeringText)) : c_strcpy(settings->hideGlimmeringText, "0", sizeof(settings->hideGlimmeringText)); if (settings->mouseEnabled[0] == '\0') ui->mouseEnabled ? c_strcpy(settings->mouseEnabled, "1", sizeof(settings->mouseEnabled)) : c_strcpy(settings->mouseEnabled, "0", sizeof(settings->mouseEnabled)); if (settings->visualizerBrailleMode[0] == '\0') ui->visualizerBrailleMode ? c_strcpy(settings->visualizerBrailleMode, "1", sizeof(settings->visualizerBrailleMode)) : c_strcpy(settings->visualizerBrailleMode, "0", sizeof(settings->visualizerBrailleMode)); if (settings->hideLogo[0] == '\0') ui->hideLogo ? c_strcpy(settings->hideLogo, "1", sizeof(settings->hideLogo)) : c_strcpy(settings->hideLogo, "0", sizeof(settings->hideLogo)); if (settings->hideHelp[0] == '\0') ui->hideHelp ? c_strcpy(settings->hideHelp, "1", sizeof(settings->hideHelp)) : c_strcpy(settings->hideHelp, "0", sizeof(settings->hideHelp)); if (settings->visualizerHeight[0] == '\0') snprintf(settings->visualizerHeight, sizeof(settings->visualizerHeight), "%d", ui->visualizerHeight); if (settings->tweenFactor[0] == '\0') snprintf(settings->tweenFactor, sizeof(settings->tweenFactor), "%.2f", ui->tweenFactor); if (settings->tweenFactorFall[0] == '\0') snprintf(settings->tweenFactorFall, sizeof(settings->tweenFactorFall), "%.2f", ui->tweenFactorFall); if (settings->visualizerColorType[0] == '\0') snprintf(settings->visualizerColorType, sizeof(settings->visualizerColorType), "%d", ui->visualizerColorType); if (settings->progressBarType[0] == '\0') snprintf(settings->progressBarType, sizeof(settings->progressBarType), "%d", ui->progressBarType); if (settings->titleDelay[0] == '\0') snprintf(settings->titleDelay, sizeof(settings->titleDelay), "%d", ui->titleDelay); if (settings->cacheLibrary[0] == '\0') snprintf(settings->cacheLibrary, sizeof(settings->cacheLibrary), "%d", ui->cacheLibrary); int currentVolume = getCurrentVolume(); currentVolume = (currentVolume <= 0) ? 10 : currentVolume; snprintf(settings->lastVolume, sizeof(settings->lastVolume), "%d", currentVolume); if (settings->color[0] == '\0') snprintf(settings->color, sizeof(settings->color), "%d", ui->mainColor); if (settings->artistColor[0] == '\0') snprintf(settings->artistColor, sizeof(settings->artistColor), "%d", ui->artistColor); if (settings->titleColor[0] == '\0') snprintf(settings->titleColor, sizeof(settings->titleColor), "%d", ui->titleColor); if (settings->enqueuedColor[0] == '\0') snprintf(settings->enqueuedColor, sizeof(settings->enqueuedColor), "%d", ui->enqueuedColor); if (settings->mouseLeftClickAction[0] == '\0') snprintf(settings->mouseLeftClickAction, sizeof(settings->mouseLeftClickAction), "%d", ui->mouseLeftClickAction); if (settings->mouseMiddleClickAction[0] == '\0') snprintf(settings->mouseMiddleClickAction, sizeof(settings->mouseMiddleClickAction), "%d", ui->mouseMiddleClickAction); if (settings->mouseRightClickAction[0] == '\0') snprintf(settings->mouseRightClickAction, sizeof(settings->mouseRightClickAction), "%d", ui->mouseRightClickAction); if (settings->mouseScrollUpAction[0] == '\0') snprintf(settings->mouseScrollUpAction, sizeof(settings->mouseScrollUpAction), "%d", ui->mouseScrollUpAction); if (settings->mouseScrollDownAction[0] == '\0') snprintf(settings->mouseScrollDownAction, sizeof(settings->mouseScrollDownAction), "%d", ui->mouseScrollDownAction); if (settings->mouseAltScrollUpAction[0] == '\0') snprintf(settings->mouseAltScrollUpAction, sizeof(settings->mouseAltScrollUpAction), "%d", ui->mouseAltScrollUpAction); if (settings->mouseAltScrollDownAction[0] == '\0') snprintf(settings->mouseAltScrollDownAction, sizeof(settings->mouseAltScrollDownAction), "%d", ui->mouseAltScrollDownAction); // 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, "[miscellaneous]\n"); fprintf(file, "path=%s\n", settings->path); fprintf(file, "version=%s\n", VERSION); 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\n", settings->lastVolume); fprintf(file, "# Cache: Set to 1 to use cache of the music library directory tree for faster startup times.\n"); fprintf(file, "cacheLibrary=%s\n\n", settings->cacheLibrary); fprintf(file, "# Delay when drawing title in track view, set to 0 to have no delay.\n"); fprintf(file, "titleDelay=%s\n\n", settings->titleDelay); fprintf(file, "# Same as '--quitonstop' flag, exits after playing the whole playlist.\n"); fprintf(file, "quitOnStop=%s\n\n", settings->quitAfterStopping); fprintf(file, "# Glimmering text on the bottom row.\n"); fprintf(file, "hideGlimmeringText=%s\n\n", settings->hideGlimmeringText); fprintf(file, "[visualizer]\n"); fprintf(file, "visualizerEnabled=%s\n", settings->visualizerEnabled); fprintf(file, "visualizerHeight=%s\n", settings->visualizerHeight); fprintf(file, "visualizerBrailleMode=%s\n\n", settings->visualizerBrailleMode); fprintf(file, "# How colors are laid out in the spectrum visualizer. 0=default, 1=brightness depending on bar height, 2=reversed.\n"); fprintf(file, "visualizerColorType=%s\n\n", settings->visualizerColorType); fprintf(file, "# How fast the visualizer moves (higher values = faster) Normal values: 0.23 and 0.13.\n"); fprintf(file, "tweenFactor=%s\n", settings->tweenFactor); fprintf(file, "tweenFactorFall=%s\n\n", settings->tweenFactorFall); fprintf(file, "# How the progress bar looks. 0=Dots, 1=Line\n"); fprintf(file, "progressBarType=%s\n\n", settings->progressBarType); fprintf(file, "[colors]\n\n"); fprintf(file, "# Use the configuration file colors below\n"); fprintf(file, "useConfigColors=%s\n\n", settings->useConfigColors); fprintf(file, "# 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\n", settings->color); fprintf(file, "# Header color in library view:\n"); fprintf(file, "artistColor=%s\n\n", settings->artistColor); fprintf(file, "# Now playing song text in library view:\n"); fprintf(file, "titleColor=%s\n\n", settings->titleColor); fprintf(file, "# Color of enqueued songs in library view:\n"); fprintf(file, "enqueuedColor=%s\n\n", settings->enqueuedColor); fprintf(file, "[track cover]\n"); fprintf(file, "coverEnabled=%s\n", settings->coverEnabled); fprintf(file, "coverAnsi=%s\n\n", settings->coverAnsi); fprintf(file, "[mouse]\n"); fprintf(file, "mouseEnabled=%s\n\n", settings->mouseEnabled); fprintf(file, "# Mouse actions are 0=none, 1=select song, 2=toggle pause, 3=scroll up, 4=scroll down, 5=seek forward, 6=seek backward, 7=volume up, 8=volume down, 9=switch to next view, 10=switch to previous view\n"); fprintf(file, "mouseLeftClickAction=%s\n", settings->mouseLeftClickAction); fprintf(file, "mouseMiddleClickAction=%s\n", settings->mouseMiddleClickAction); fprintf(file, "mouseRightClickAction=%s\n", settings->mouseRightClickAction); fprintf(file, "mouseScrollUpAction=%s\n", settings->mouseScrollUpAction); fprintf(file, "mouseScrollDownAction=%s\n\n", settings->mouseScrollDownAction); fprintf(file, "# Mouse action when using mouse scroll + alt\n"); fprintf(file, "mouseAltScrollUpAction=%s\n", settings->mouseAltScrollUpAction); fprintf(file, "mouseAltScrollDownAction=%s\n\n", settings->mouseAltScrollDownAction); fprintf(file, "[key bindings]\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, "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, "moveSongUp=%s\n", settings->moveSongUp); fprintf(file, "moveSongDown=%s\n", settings->moveSongDown); fprintf(file, "enqueueAndPlay=%s\n", settings->enqueueAndPlay); fprintf(file, "sortLibrary=%s\n", settings->sortLibrary); fprintf(file, "quit=%s\n\n", settings->quit); fprintf(file, "# Alt keys for the different main views, normally F2-F7:\n"); fprintf(file, "showPlaylistAlt=%s\n", settings->showPlaylistAlt); fprintf(file, "showLibraryAlt=%s\n", settings->showLibraryAlt); fprintf(file, "showTrackAlt=%s\n", settings->showTrackAlt); fprintf(file, "showSearchAlt=%s\n", settings->showSearchAlt); fprintf(file, "showRadioSearchAlt=%s\n", settings->showRadioSearchAlt); fprintf(file, "showKeysAlt=%s\n\n", settings->showKeysAlt); 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"); fclose(file); free(filepath); free(configdir); } kew-3.2.0/src/settings.h000066400000000000000000000012151500206121000150640ustar00rootroot00000000000000#ifndef SETTINGS_H #define SETTINGS_H #include #include #include #include #include #include #include #include "appstate.h" #include "events.h" #include "file.h" #include "soundcommon.h" #include "player.h" #include "utils.h" #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef NUM_KEY_MAPPINGS #define NUM_KEY_MAPPINGS 65 #endif extern AppSettings settings; void getConfig(AppSettings *settings, UISettings *ui); void setConfig(AppSettings *settings, UISettings *ui); void mapSettingsToKeys(AppSettings *settings, UISettings *ui, EventMapping *mappings); #endif kew-3.2.0/src/songloader.c000066400000000000000000000127361500206121000153660ustar00rootroot00000000000000#include "songloader.h" #include "stb_image.h" /* songloader.c This file should contain only functions related to loading song data. */ #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif static guint track_counter = 0; 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]; if (directoryPath[strnlen(directoryPath, MAXPATHLEN) - 1] == '/') { snprintf(filePath, sizeof(filePath), "%s%s", directoryPath, entry->d_name); } else { 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; } // Generate a new track ID gchar *generateTrackId(void) { gchar *trackId = g_strdup_printf("/org/kew/tracklist/track%d", track_counter); track_counter++; return trackId; } void loadColor(SongData *songdata) { getCoverColor(songdata->cover, songdata->coverWidth, songdata->coverHeight, &(songdata->red), &(songdata->green), &(songdata->blue)); } void loadMetaData(SongData *songdata, AppState *state) { char path[MAXPATHLEN]; songdata->metadata = malloc(sizeof(TagSettings)); songdata->metadata->replaygainTrack = 0.0; songdata->metadata->replaygainAlbum = 0.0; 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, tmp, sizeof(songdata->coverArtPath)); free(tmp); tmp = NULL; } else c_strcpy(songdata->coverArtPath, "", sizeof(songdata->coverArtPath)); } else { addToCache(state->tmpCache, songdata->coverArtPath); } songdata->cover = getBitmap(songdata->coverArtPath, &(songdata->coverWidth), &(songdata->coverHeight)); } SongData *loadSongData(char *filePath, AppState *state) { 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 = defaultColor; songdata->green = defaultColor; songdata->blue = defaultColor; songdata->metadata = NULL; songdata->cover = NULL; songdata->duration = 0.0; songdata->avgBitRate = 0; c_strcpy(songdata->filePath, filePath, sizeof(songdata->filePath)); loadMetaData(songdata, state); loadColor(songdata); return songdata; } void unloadSongData(SongData **songdata, AppState *state) { if (*songdata == NULL) return; SongData *data = *songdata; if (data->cover != NULL) { stbi_image_free(data->cover); data->cover = NULL; } if (existsInCache(state->tmpCache, 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-3.2.0/src/songloader.h000066400000000000000000000027301500206121000153640ustar00rootroot00000000000000#include #include #include #include #include "appstate.h" #include "tagLibWrapper.h" #include "cache.h" #include "imgfunc.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 #define METADATA_MAX_LENGTH 256 typedef struct { char title[METADATA_MAX_LENGTH]; char artist[METADATA_MAX_LENGTH]; char album_artist[METADATA_MAX_LENGTH]; char album[METADATA_MAX_LENGTH]; char date[METADATA_MAX_LENGTH]; double replaygainTrack; double replaygainAlbum; } 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; unsigned char *cover; int avgBitRate; int coverWidth; int coverHeight; double duration; bool hasErrors; } SongData; #endif SongData *loadSongData(char *filePath, AppState *state); void unloadSongData(SongData **songdata, AppState *state); kew-3.2.0/src/sound.c000066400000000000000000000517271500206121000143640ustar00rootroot00000000000000#define MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING #define MA_NO_ENGINE #define MINIAUDIO_IMPLEMENTATION #include #include "sound.h" /* sound.c Functions related to miniaudio implementation */ ma_context context; bool isContextInitialized = false; bool tryAgain = false; UserData userData; ma_result initFirstDatasource(AudioData *pAudioData, UserData *pUserData) { char *filePath = NULL; SongData *songData = (pAudioData->currentFileIndex == 0) ? pUserData->songdataA : pUserData->songdataB; if (songData == NULL) { return MA_ERROR; } filePath = songData->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 (pathEndsWith(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 (pathEndsWith(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 (pathEndsWith(filePath, "m4a") || pathEndsWith(filePath, "aac")) { #ifdef USE_FAAD int result = prepareNextM4aDecoder(songData); 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; #endif } else { return MA_ERROR; } return MA_SUCCESS; } int 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 -1; 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 -1; setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) return -1; appState.uiState.doNotifyMPRISPlaying = true; return 0; } int builtin_createAudioDevice(UserData *userData, ma_device *device, ma_context *context, ma_data_source_vtable *vtable) { return createDevice(userData, device, context, vtable, builtin_on_audio_frames); } int vorbis_createAudioDevice(UserData *userData, ma_device *device, ma_context *context) { ma_result result; result = initFirstDatasource(&audioData, userData); if (result != MA_SUCCESS) { printf("\n\nFailed to initialize ogg vorbis file.\n"); return -1; } 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) { setErrorMessage("Failed to initialize miniaudio device."); return -1; } setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) { setErrorMessage("Failed to start miniaudio device."); return -1; } appState.uiState.doNotifyMPRISPlaying = true; return 0; } #ifdef USE_FAAD int m4a_createAudioDevice(UserData *userData, ma_device *device, ma_context *context) { ma_result result; result = initFirstDatasource(&audioData, userData); if (result != MA_SUCCESS) { setErrorMessage("M4a type not supported."); return -1; } 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) { setErrorMessage("Failed to initialize miniaudio device."); return -1; } setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) { setErrorMessage("Failed to start miniaudio device."); return -1; } appState.uiState.doNotifyMPRISPlaying = true; return 0; } #endif int opus_createAudioDevice(UserData *userData, ma_device *device, ma_context *context) { ma_result result; result = initFirstDatasource(&audioData, userData); if (result != MA_SUCCESS) { printf("\n\nFailed to initialize opus file.\n"); return -1; } 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) { setErrorMessage("Failed to initialize miniaudio device."); return -1; } setVolume(getCurrentVolume()); result = ma_device_start(device); if (result != MA_SUCCESS) { setErrorMessage("Failed to start miniaudio device."); return -1; } appState.uiState.doNotifyMPRISPlaying = true; return 0; } bool validFilePath(char *filePath) { if (filePath == NULL || filePath[0] == '\0' || filePath[0] == '\r') return false; if (existsFile(filePath) < 0) return false; return true; } long long getFileSize(const char *filename) { struct stat st; if (stat(filename, &st) == 0) { return (long long)st.st_size; } else { return -1; } } int calcAvgBitRate(double duration, const char *filePath) { long long fileSize = getFileSize(filePath); // in bytes int avgBitRate = 0; if (duration > 0.0) avgBitRate = (int)((fileSize * 8.0) / duration / 1000.0); // use 1000 for kbps return avgBitRate; } int switchAudioImplementation(void) { if (isRadioPlaying()) { stopRadio(); } if (audioData.endOfListReached) { setEOFNotReached(); setCurrentImplementationType(NONE); return 0; } enum AudioImplementation currentImplementation = getCurrentImplementationType(); userData.currentSongData = (audioData.currentFileIndex == 0) ? userData.songdataA : userData.songdataB; char *filePath = NULL; if (userData.currentSongData == NULL) { setEOFNotReached(); return 0; } else { if (!validFilePath(userData.currentSongData->filePath)) { if (!tryAgain) { setCurrentFileIndex(&audioData, 1 - audioData.currentFileIndex); tryAgain = true; switchAudioImplementation(); return 0; } else { setEOFReached(); return -1; } } filePath = strdup(userData.currentSongData->filePath); } tryAgain = false; 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 (pathEndsWith(filePath, ".mp3") && userData.currentSongData) { int avgBitRate = calcAvgBitRate(userData.currentSongData->duration, filePath); if (avgBitRate > 320) avgBitRate = 320; userData.currentSongData->avgBitRate = audioData.avgBitRate = avgBitRate; } else audioData.avgBitRate = 0; if (isRepeatEnabled() || !(sameFormat && currentImplementation == BUILTIN)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(BUILTIN); cleanupPlaybackDevice(); resetAllDecoders(); resetAudioBuffer(); audioData.sampleRate = sampleRate; int result = builtin_createAudioDevice(&userData, getDevice(), &context, &builtin_file_data_source_vtable); if (result < 0) { setCurrentImplementationType(NONE); setImplSwitchNotReached(); setEOFReached(); free(filePath); pthread_mutex_unlock(&dataSourceMutex); return -1; } pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } } else if (pathEndsWith(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(); resetAllDecoders(); resetAudioBuffer(); audioData.sampleRate = sampleRate; audioData.avgBitRate = 0; int result = opus_createAudioDevice(&userData, getDevice(), &context); if (result < 0) { setCurrentImplementationType(NONE); setImplSwitchNotReached(); setEOFReached(); free(filePath); pthread_mutex_unlock(&dataSourceMutex); return -1; } pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } } else if (pathEndsWith(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 (userData.currentSongData) userData.currentSongData->avgBitRate = audioData.avgBitRate = calcAvgBitRate(userData.currentSongData->duration, filePath); else audioData.avgBitRate = 0; if (isRepeatEnabled() || !(sameFormat && currentImplementation == VORBIS)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(VORBIS); cleanupPlaybackDevice(); resetAllDecoders(); resetAudioBuffer(); audioData.sampleRate = sampleRate; int result = vorbis_createAudioDevice(&userData, getDevice(), &context); if (result < 0) { setCurrentImplementationType(NONE); setImplSwitchNotReached(); setEOFReached(); free(filePath); pthread_mutex_unlock(&dataSourceMutex); return -1; } pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } } else if (pathEndsWith(filePath, "m4a") || pathEndsWith(filePath, "aac")) { #ifdef USE_FAAD ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_uint32 nSampleRate; ma_uint32 nChannels; ma_format nFormat; int avgBitRate; ma_channel nChannelMap[MA_MAX_CHANNELS]; m4a_decoder *decoder = getCurrentM4aDecoder(); k_m4adec_filetype fileType = k_unknown; getM4aFileInfo(filePath, &format, &channels, &sampleRate, channelMap, &avgBitRate, &fileType); 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 && decoder->fileType == fileType && decoder->fileType != k_rawAAC)); if (userData.currentSongData) userData.currentSongData->avgBitRate = audioData.avgBitRate = avgBitRate; if (isRepeatEnabled() || !(sameFormat && currentImplementation == M4A)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(M4A); cleanupPlaybackDevice(); resetAllDecoders(); resetAudioBuffer(); audioData.sampleRate = sampleRate; int result = m4a_createAudioDevice(&userData, getDevice(), &context); if (result < 0) { setCurrentImplementationType(NONE); setImplSwitchNotReached(); setEOFReached(); free(filePath); pthread_mutex_unlock(&dataSourceMutex); return -1; } pthread_mutex_unlock(&dataSourceMutex); setImplSwitchNotReached(); } #else setCurrentImplementationType(NONE); setImplSwitchNotReached(); setEOFReached(); free(filePath); pthread_mutex_unlock(&dataSourceMutex); return -1; #endif } else { free(filePath); return -1; } free(filePath); setEOFNotReached(); return 0; } void cleanupAudioContext(void) { ma_context_uninit(&context); isContextInitialized = false; } int createAudioDevice() { if (isContextInitialized) { ma_context_uninit(&context); isContextInitialized = false; } ma_context_init(NULL, 0, NULL, &context); isContextInitialized = true; if (switchAudioImplementation() >= 0) { appState.uiState.doNotifyMPRISSwitched = true; } else { return -1; } return 0; } kew-3.2.0/src/sound.h000066400000000000000000000024051500206121000143560ustar00rootroot00000000000000#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" #include "soundradio.h" #include "common.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; ma_uint32 avgBitRate; bool switchFiles; int currentFileIndex; ma_uint64 totalFrames; bool endOfListReached; bool restart; } AudioData; #endif extern UserData userData; extern bool isContextInitialized; int createAudioDevice(); int switchAudioImplementation(void); void cleanupAudioContext(void); #endif kew-3.2.0/src/soundbuiltin.c000066400000000000000000000267001500206121000157440ustar00rootroot00000000000000#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 }; double dbToLinear(double db) { return pow(10.0, db / 20.0); } 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; // Convert ReplayGain dB values to linear gain factors double gainDb = 0.0; // Default to 0 dB (no gain) bool gainAvailable = false; if ((!audioData->pUserData->songdataADeleted && audioData->pUserData->currentSongData == audioData->pUserData->songdataA) || (!audioData->pUserData->songdataBDeleted && audioData->pUserData->currentSongData == audioData->pUserData->songdataB)) { if (audioData->pUserData->currentSongData->metadata->replaygainTrack > -50.0) { gainDb = audioData->pUserData->currentSongData->metadata->replaygainTrack; gainAvailable = true; } else if (audioData->pUserData->currentSongData->metadata->replaygainAlbum > -50.0) { gainDb = audioData->pUserData->currentSongData->metadata->replaygainAlbum; gainAvailable = true; } } double totalGainFactor = 1.0; if (gainAvailable) { totalGainFactor = dbToLinear(gainDb); } 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 = (ma_uint64)((totalFrames - 1) * seekPercent / 100.0); if (targetFrame >= totalFrames) targetFrame = totalFrames - 1; 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; } if (isEOFReached()) { 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); float *frames = (float *)pFramesOut + framesRead * audioData->channels; // Apply replay gain if (totalGainFactor != 1.0) { switch (audioData->format) { case ma_format_f32: for (ma_uint64 i = 0; i < framesToRead; ++i) { for (int ch = 0; ch < (int)audioData->channels; ++ch) { ma_uint64 frameIndex = i * audioData->channels + ch; float originalSample = frames[frameIndex]; double sample = (double)originalSample; sample *= totalGainFactor; frames[frameIndex] = (float)sample; } } break; case ma_format_s16: for (ma_uint64 i = 0; i < framesToRead; ++i) { for (int ch = 0; ch < (int)audioData->channels; ++ch) { ma_uint64 frameIndex = i * audioData->channels + ch; ma_int16 originalSample = frames[frameIndex]; double sample = (double)originalSample; sample *= totalGainFactor; if (sample > 32767.0) sample = 32767.0; else if (sample < -32768.0) sample = -32768.0; frames[frameIndex] = (ma_int16)sample; } } break; case ma_format_s32: for (ma_uint64 i = 0; i < framesToRead; ++i) { for (int ch = 0; ch < (int)audioData->channels; ++ch) { ma_uint64 frameIndex = i * audioData->channels + ch; ma_int32 originalSample = frames[frameIndex]; double sample = (double)originalSample; sample *= totalGainFactor; // Clamp if (sample > 2147483647.0) sample = 2147483647.0; else if (sample < -2147483648.0) sample = -2147483648.0; frames[frameIndex] = (ma_int32)sample; } } break; default: break; } } 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); } setAudioBuffer(pFramesOut, framesRead); 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-3.2.0/src/soundbuiltin.h000066400000000000000000000006711500206121000157500ustar00rootroot00000000000000#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-3.2.0/src/soundcommon.c000066400000000000000000001262721500206121000155730ustar00rootroot00000000000000#include "soundcommon.h" #include "playerops.h" /* soundcommon.c Related to common functions for decoders / miniaudio implementations. */ #define MAX_DECODERS 2 #ifndef PATH_MAX #define PATH_MAX 4096 #endif bool repeatEnabled = false; bool repeatListEnabled = false; bool shuffleEnabled = false; bool skipToNext = false; bool seekRequested = false; bool paused = false; bool stopped = true; bool hasSilentlySwitched; 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}; static ma_int32 audioBuffer[MAX_BUFFER_SIZE]; static int writeHead = 0; bool bufferReady = false; AudioData audioData; int bufSize; ma_event switchAudioImpl; enum AudioImplementation currentImplementation = NONE; AppState appState; 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; #ifdef USE_FAAD m4a_decoder *m4aDecoders[MAX_DECODERS]; m4a_decoder *firstM4aDecoder; #endif int decoderIndex = -1; int m4aDecoderIndex = -1; int opusDecoderIndex = -1; int vorbisDecoderIndex = -1; void uninitMaDecoder(void *decoder) { ma_decoder_uninit((ma_decoder *)decoder); } void uninitOpusDecoder(void *decoder) { ma_libopus_uninit((ma_libopus *)decoder, NULL); } void uninitVorbisDecoder(void *decoder) { ma_libvorbis_uninit((ma_libvorbis *)decoder, NULL); } #ifdef USE_FAAD void uninitM4aDecoder(void *decoder) { m4a_decoder_uninit((m4a_decoder *)decoder, NULL); } #endif void uninitPreviousDecoder(void **decoderArray, int index, uninit_func uninit) { if (index == -1) { return; } void *toUninit = decoderArray[1 - index]; if (toUninit != NULL) { uninit(toUninit); free(toUninit); decoderArray[1 - index] = NULL; } } void resetDecoders(void **decoderArray, void **firstDecoder, int arraySize, int *decoderIndex, uninit_func uninit) { *decoderIndex = -1; if (*firstDecoder != NULL) { uninit(*firstDecoder); free(*firstDecoder); *firstDecoder = NULL; } for (int i = 0; i < arraySize; i++) { if (decoderArray[i] != NULL) { uninit(decoderArray[i]); free(decoderArray[i]); decoderArray[i] = NULL; } } } void resetAllDecoders() { resetDecoders((void **)decoders, (void **)&firstDecoder, MAX_DECODERS, &decoderIndex, uninitMaDecoder); resetDecoders((void **)vorbisDecoders, (void **)&firstVorbisDecoder, MAX_DECODERS, &vorbisDecoderIndex, uninitVorbisDecoder); resetDecoders((void **)opusDecoders, (void **)&firstOpusDecoder, MAX_DECODERS, &opusDecoderIndex, uninitOpusDecoder); #ifdef USE_FAAD resetDecoders((void **)m4aDecoders, (void **)&firstM4aDecoder, MAX_DECODERS, &m4aDecoderIndex, uninitM4aDecoder); #endif } void setNextDecoder(void **decoderArray, void **decoder, void **firstDecoder, int *decoderIndex, uninit_func uninit) { if (*decoderIndex == -1 && *firstDecoder == NULL) { *firstDecoder = *decoder; } else if (*decoderIndex == -1) // Array hasn't been used yet { if (decoderArray[0] != NULL) { uninit(decoderArray[0]); free(decoderArray[0]); decoderArray[0] = NULL; } decoderArray[0] = *decoder; } else { int nextIndex = 1 - *decoderIndex; if (decoderArray[nextIndex] != NULL) { uninit(decoderArray[nextIndex]); free(decoderArray[nextIndex]); decoderArray[nextIndex] = NULL; } decoderArray[nextIndex] = *decoder; } } 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(void) { return currentImplementation; } void setCurrentImplementationType(enum AudioImplementation value) { currentImplementation = value; } ma_decoder *getFirstDecoder(void) { return firstDecoder; } ma_decoder *getCurrentBuiltinDecoder(void) { if (decoderIndex == -1) return getFirstDecoder(); else return decoders[decoderIndex]; } void switchDecoder(int *decoderIndex) { if (*decoderIndex == -1) *decoderIndex = 0; else *decoderIndex = 1 - *decoderIndex; } #ifdef USE_FAAD m4a_decoder *getFirstM4aDecoder(void) { return firstM4aDecoder; } m4a_decoder *getCurrentM4aDecoder(void) { if (m4aDecoderIndex == -1) return getFirstM4aDecoder(); else return m4aDecoders[m4aDecoderIndex]; } void getM4aFileInfo(const char *filename, ma_format *format, ma_uint32 *channels, ma_uint32 *sampleRate, ma_channel *channelMap, int *avgBitRate, k_m4adec_filetype *fileType) { 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); *avgBitRate = decoder.avgBitRate / 1000; *fileType = decoder.fileType; m4a_decoder_uninit(&decoder, NULL); } } 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); } int prepareNextM4aDecoder(SongData *songData) { m4a_decoder *currentDecoder; if (songData == NULL) return -1; char *filepath = songData->filePath; 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); uninitPreviousDecoder((void **)m4aDecoders, m4aDecoderIndex, (uninit_func)uninitM4aDecoder); 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 && currentDecoder->fileType == decoder->fileType && currentDecoder->fileType != k_rawAAC)); 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; setNextDecoder((void **)m4aDecoders, (void **)&decoder, (void **)&firstM4aDecoder, &m4aDecoderIndex, (uninit_func)uninitM4aDecoder); if (songData != NULL) { if (decoder != NULL && decoder->fileType == k_rawAAC) { songData->duration = decoder->duration; } } if (currentDecoder != NULL && decoder != NULL && decoder->fileType != k_rawAAC) { if (!isEOFReached()) ma_data_source_set_next(currentDecoder, decoder); } return 0; } #endif ma_libvorbis *getFirstVorbisDecoder(void) { return firstVorbisDecoder; } ma_libopus *getFirstOpusDecoder(void) { return firstOpusDecoder; } ma_libvorbis *getCurrentVorbisDecoder(void) { if (vorbisDecoderIndex == -1) return getFirstVorbisDecoder(); else return vorbisDecoders[vorbisDecoderIndex]; } ma_libopus *getCurrentOpusDecoder(void) { if (opusDecoderIndex == -1) return getFirstOpusDecoder(); else return opusDecoders[opusDecoderIndex]; } void getCurrentFormatAndSampleRate(ma_format *format, ma_uint32 *sampleRate) { *format = ma_format_unknown; if (isRadioPlaying()) { ma_uint32 channels; ma_data_source_get_data_format(radioContext.decoder.pBackend, format, &channels, sampleRate, NULL, 0); return; } else 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) { #ifdef USE_FAAD m4a_decoder *decoder = getCurrentM4aDecoder(); if (decoder != NULL) *format = decoder->format; #endif } *sampleRate = audioData.sampleRate; } 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 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); } } 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); uninitPreviousDecoder((void **)vorbisDecoders, vorbisDecoderIndex, (uninit_func)uninitVorbisDecoder); 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; setNextDecoder((void **)vorbisDecoders, (void **)&decoder, (void **)&firstVorbisDecoder, &vorbisDecoderIndex, (uninit_func)uninitVorbisDecoder); if (currentDecoder != NULL && decoder != NULL) { if (!isEOFReached()) 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((void **)decoders, decoderIndex, (uninit_func)uninitMaDecoder); 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((void **)decoders, (void **)&decoder, (void **)&firstDecoder, &decoderIndex, (uninit_func)uninitMaDecoder); if (currentDecoder != NULL && decoder != NULL) { if (!isEOFReached()) ma_data_source_set_next(currentDecoder, decoder); } return 0; } int prepareNextOpusDecoder(char *filepath) { ma_libopus *currentDecoder; if (opusDecoderIndex == -1) { currentDecoder = getFirstOpusDecoder(); } else { currentDecoder = opusDecoders[opusDecoderIndex]; } 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); uninitPreviousDecoder((void **)opusDecoders, opusDecoderIndex, (uninit_func)uninitOpusDecoder); 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; } if (firstOpusDecoder != NULL) { decoder->pReadSeekTellUserData = (AudioData *)firstOpusDecoder->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; setNextDecoder((void **)opusDecoders, (void **)&decoder, (void **)&firstOpusDecoder, &opusDecoderIndex, (uninit_func)uninitOpusDecoder); if (currentDecoder != NULL && decoder != NULL) { if (!isEOFReached()) ma_data_source_set_next(currentDecoder, decoder); } return 0; } int getBufferSize(void) { return bufSize; } void setBufferSize(int value) { bufSize = value; } void setAudioBuffer(ma_int32 *buf, int numSamples) { int bufIndex = 0; while (bufIndex < numSamples) { if (writeHead >= MAX_BUFFER_SIZE) { break; } int spaceLeft = FFT_SIZE - writeHead; int samplesLeft = numSamples - bufIndex; int samplesToCopy = (samplesLeft < spaceLeft) ? samplesLeft : spaceLeft; // Clamp copy to avoid buffer overflow int maxCopy = MAX_BUFFER_SIZE - writeHead; if (samplesToCopy > maxCopy) samplesToCopy = maxCopy; memcpy(&audioBuffer[writeHead], &buf[bufIndex], sizeof(ma_int32) * samplesToCopy); writeHead += samplesToCopy; bufIndex += samplesToCopy; // As long as we have at least FFT_SIZE samples, process one window while (writeHead >= FFT_SIZE) { bufferReady = true; // Shift the buffer left by HOP_SIZE memmove(audioBuffer, audioBuffer + HOP_SIZE, sizeof(ma_int32) * (FFT_SIZE - HOP_SIZE)); writeHead -= HOP_SIZE; } } } void resetAudioBuffer(void) { memset(audioBuffer, 0, sizeof(ma_int32) * MAX_BUFFER_SIZE); writeHead = 0; bufferReady = false; } ma_int32 *getAudioBuffer(void) { return audioBuffer; } bool isRepeatEnabled(void) { return repeatEnabled; } void setRepeatEnabled(bool value) { repeatEnabled = value; } bool isRepeatListEnabled(void) { return repeatListEnabled; } void setRepeatListEnabled(bool value) { repeatListEnabled = value; } bool isShuffleEnabled(void) { return shuffleEnabled; } void setShuffleEnabled(bool value) { shuffleEnabled = value; } bool isSkipToNext(void) { return skipToNext; } void setSkipToNext(bool value) { skipToNext = value; } double getSeekElapsed(void) { return seekElapsed; } void setSeekElapsed(double value) { seekElapsed = value; } bool isEOFReached(void) { return atomic_load(&EOFReached); } void setEOFReached(void) { atomic_store(&EOFReached, true); } void setEOFNotReached(void) { atomic_store(&EOFReached, false); } bool isImplSwitchReached(void) { return atomic_load(&switchReached) ? true : false; } void setImplSwitchReached(void) { atomic_store(&switchReached, true); } void setImplSwitchNotReached(void) { atomic_store(&switchReached, false); } bool isPlaying(void) { return ma_device_is_started(&device); } bool isPlaybackDone(void) { if (isEOFReached()) { return true; } else { return false; } } float getSeekPercentage(void) { return seekPercent; } bool isSeekRequested(void) { return seekRequested; } void setSeekRequested(bool value) { seekRequested = value; } void seekPercentage(float percent) { seekPercent = percent; seekRequested = true; } void resumePlayback(void) { // If this was unpaused with no song loaded if (audioData.restart && !isRadioPlaying()) { audioData.endOfListReached = false; } if (!ma_device_is_started(&device)) { ma_device_start(&device); } paused = false; stopped = false; if (appState.currentView != TRACK_VIEW) { refresh = true; } } void stopPlayback(void) { if (ma_device_is_started(&device)) { ma_device_stop(&device); } stopped = true; if (appState.currentView != TRACK_VIEW) { refresh = true; } } void pausePlayback(void) { if (ma_device_is_started(&device)) { ma_device_stop(&device); } paused = true; if (appState.currentView != TRACK_VIEW) { refresh = true; } } void cleanupPlaybackDevice(void) { ma_device_uninit(&device); memset(&device, 0, sizeof(device)); } void clearCurrentTrack(void) { if (ma_device_is_started(&device)) { // Stop the device (which stops playback) ma_device_stop(&device); } resetAllDecoders(); ma_data_source_set_next(currentDecoder, NULL); } void togglePausePlayback(void) { if (ma_device_is_started(&device)) { pausePlayback(); } else if (isPaused() || isStopped()) { resumePlayback(); } } bool isPaused(void) { return paused; } bool isStopped(void) { return stopped; } ma_device *getDevice(void) { 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 setCurrentFileIndex(AudioData *pAudioData, int index) { pthread_mutex_lock(&switchMutex); pAudioData->currentFileIndex = index; pthread_mutex_unlock(&switchMutex); } 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; } gint64 getLengthInMicroSec(double duration) { return floor(llround(duration * G_USEC_PER_SEC)); } void executeSwitch(AudioData *pAudioData) { pAudioData->switchFiles = false; switchDecoder(&decoderIndex); switchDecoder(&opusDecoderIndex); switchDecoder(&m4aDecoderIndex); switchDecoder(&vorbisDecoderIndex); pAudioData->pUserData->currentSongData = (pAudioData->currentFileIndex == 0) ? pAudioData->pUserData->songdataA : pAudioData->pUserData->songdataB; pAudioData->totalFrames = 0; pAudioData->currentPCMFrame = 0; setSeekElapsed(0.0); setEOFReached(); } int getCurrentVolume(void) { return soundVolume; } int extractPercentage(char *str) { int volume = -1; char *percentSign = strchr(str, '%'); if (percentSign != NULL) { // Find the start of the number before the '%' while (percentSign > str && *(percentSign - 1) == ' ') percentSign--; while (percentSign > str && *(percentSign - 1) >= '0' && *(percentSign - 1) <= '9') percentSign--; volume = getNumber(percentSign); } return volume; } int extractPercentageMac(const char *buf) { int volume; if (sscanf(buf, "%d", &volume) == 1) { return volume; } return -1; } int getSystemVolumeMac(void) { FILE *fp; char buf[256]; int currentVolume = -1; fp = popen("osascript -e 'output volume of (get volume settings)'", "r"); if (fp != NULL) { if (fgets(buf, sizeof(buf), fp) != NULL) { buf[strcspn(buf, "\n")] = '\0'; int tmpVolume = extractPercentageMac(buf); if (tmpVolume != -1) { currentVolume = tmpVolume; } } pclose(fp); } return currentVolume; } int getSystemVolume(void) { #ifdef __APPLE__ return getSystemVolumeMac(); #else FILE *fp; char command_str[1000]; char buf[256]; int currentVolume = -1; // Use '@DEFAULT_SINK@' to get the default sink's volume directly snprintf(command_str, sizeof(command_str), "pactl get-sink-volume @DEFAULT_SINK@"); fp = popen(command_str, "r"); if (fp != NULL) { while (fgets(buf, sizeof(buf), fp) != NULL) { int tmpVolume = extractPercentage(buf); if (tmpVolume != -1) { currentVolume = tmpVolume; break; } } pclose(fp); } // ALSA fallback if `pactl` fails if (currentVolume == -1) { snprintf(command_str, sizeof(command_str), "amixer get Master"); fp = popen(command_str, "r"); if (fp != NULL) { while (fgets(buf, sizeof(buf), fp) != NULL) { int tmpVolume = extractPercentage(buf); if (tmpVolume != -1) { currentVolume = tmpVolume; break; } } pclose(fp); } } return currentVolume; #endif } 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; #ifdef USE_FAAD 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 (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()) { if (decoder->fileType != k_rawAAC) { ma_uint64 totalFrames = pAudioData->totalFrames; ma_uint64 seekPercent = getSeekPercentage(); if (seekPercent >= 100.0) seekPercent = 100.0; ma_uint64 targetFrame = (ma_uint64)((totalFrames - 1) * seekPercent / 100.0); if (targetFrame >= totalFrames) targetFrame = totalFrames - 1; // 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); } setAudioBuffer(pFramesOut, framesRead); 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; } #endif 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 (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 = (ma_uint64)((totalFrames - 1) * seekPercent / 100.0); if (targetFrame >= totalFrames) targetFrame = totalFrames - 1; // 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; } if (isEOFReached()) { 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); } setAudioBuffer(pFramesOut, framesRead); 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 (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; } ma_libvorbis *decoder = getCurrentVorbisDecoder(); if (pAudioData->totalFrames == 0) ma_data_source_get_length_in_pcm_frames(decoder, &(pAudioData->totalFrames)); if ((getCurrentImplementationType() != VORBIS && !isSkipToNext()) || (decoder == NULL)) { pthread_mutex_unlock(&dataSourceMutex); return; } // Check if seeking is requested if (isSeekRequested()) { ma_uint64 totalFrames = 0; ma_libvorbis_get_length_in_pcm_frames(decoder, &totalFrames); ma_uint64 seekPercent = getSeekPercentage(); if (seekPercent >= 100.0) seekPercent = 100.0; ma_uint64 targetFrame = (ma_uint64)((totalFrames - 1) * seekPercent / 100.0); if (targetFrame >= totalFrames) targetFrame = totalFrames - 1; // Set the read pointer for the decoder ma_result seekResult = ma_libvorbis_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 framesRequested = frameCount - framesRead; ma_libvorbis *firstDecoder = getFirstVorbisDecoder(); ma_uint64 cursor = 0; if (firstDecoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } if (isEOFReached()) { pthread_mutex_unlock(&dataSourceMutex); return; } result = ma_data_source_read_pcm_frames(firstDecoder, (ma_int32 *)pFramesOut + framesRead * pAudioData->channels, framesRequested, &framesToRead); ma_data_source_get_cursor_in_pcm_frames(decoder, &cursor); if (((cursor != 0 && cursor >= pAudioData->totalFrames) || isSkipToNext() || result != MA_SUCCESS) && !isEOFReached()) { activateSwitch(pAudioData); pthread_mutex_unlock(&dataSourceMutex); continue; } framesRead += framesToRead; setBufferSize(framesToRead); pthread_mutex_unlock(&dataSourceMutex); } setAudioBuffer(pFramesOut, framesRead); 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-3.2.0/src/soundcommon.h000066400000000000000000000140371500206121000155730ustar00rootroot00000000000000#ifndef SOUND_COMMON_H #define SOUND_COMMON_H #include #include #include #include #include #include #include #include #include #ifdef USE_FAAD #include "m4a.h" #endif #include #include #include #include "appstate.h" #include "file.h" #include "utils.h" #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef MAX_BUFFER_SIZE #define MAX_BUFFER_SIZE 8192 #endif #ifndef MAX_DECODERS #define MAX_DECODERS 2 #endif #ifndef TAGSETTINGS_STRUCT #define TAGSETTINGS_STRUCT #define METADATA_MAX_LENGTH 256 typedef struct { char title[METADATA_MAX_LENGTH]; char artist[METADATA_MAX_LENGTH]; char album_artist[METADATA_MAX_LENGTH]; char album[METADATA_MAX_LENGTH]; char date[METADATA_MAX_LENGTH]; double replaygainTrack; double replaygainAlbum; } 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; unsigned char *cover; int avgBitRate; int coverWidth; int coverHeight; 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; ma_uint32 avgBitRate; bool switchFiles; int currentFileIndex; ma_uint64 totalFrames; bool endOfListReached; bool restart; } AudioData; #endif enum AudioImplementation { PCM, BUILTIN, VORBIS, OPUS, M4A, NONE }; #define FFT_SIZE 4096 #define HOP_SIZE 1024 typedef void (*uninit_func)(void *decoder); extern AppState appState; extern AudioData audioData; extern bool bufferReady; extern double elapsedSeconds; extern bool hasSilentlySwitched; extern pthread_mutex_t dataSourceMutex; extern pthread_mutex_t switchMutex; extern bool paused; extern bool stopped; extern ma_device device; enum AudioImplementation getCurrentImplementationType(); void setCurrentImplementationType(enum AudioImplementation value); int getBufferSize(void); void setBufferSize(int value); void setPlayingStatus(bool playing); bool isPlaying(void); ma_decoder *getFirstDecoder(void); ma_decoder *getCurrentBuiltinDecoder(void); ma_decoder *getPreviousDecoder(void); void getCurrentFormatAndSampleRate(ma_format *format, ma_uint32 *sampleRate); void resetAllDecoders(); ma_libopus *getCurrentOpusDecoder(void); #ifdef USE_FAAD m4a_decoder *getCurrentM4aDecoder(void); m4a_decoder *getFirstM4aDecoder(void); void getM4aFileInfo(const char *filename, ma_format *format, ma_uint32 *channels, ma_uint32 *sampleRate, ma_channel *channelMap, int *avgBitRate, k_m4adec_filetype *fileType); #endif ma_libopus *getFirstOpusDecoder(void); ma_libvorbis *getFirstVorbisDecoder(void); void getVorbisFileInfo(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); void switchVorbisDecoder(void); int prepareNextDecoder(char *filepath); int prepareNextOpusDecoder(char *filepath); int prepareNextVorbisDecoder(char *filepath); int prepareNextM4aDecoder(SongData *songData); ma_libvorbis *getFirstVorbisDecoder(void); void getFileInfo(const char* filename, ma_uint32* sampleRate, ma_uint32* channels, ma_format* format); void initAudioBuffer(void); ma_int32 *getAudioBuffer(void); void setAudioBuffer(ma_int32 *buf, int numSamples); void resetAudioBuffer(void); void freeAudioBuffer(void); bool isRepeatEnabled(void); void setRepeatEnabled(bool value); bool isRepeatListEnabled(void); void setRepeatListEnabled(bool value); bool isShuffleEnabled(void); void setShuffleEnabled(bool value); bool isSkipToNext(void); void setSkipToNext(bool value); double getSeekElapsed(void); void setSeekElapsed(double value); bool isEOFReached(void); void setEOFReached(void); void setEOFNotReached(void); bool isImplSwitchReached(void); void setImplSwitchReached(void); void setImplSwitchNotReached(void); bool isPlaybackDone(void); float getSeekPercentage(void); bool isSeekRequested(void); void setSeekRequested(bool value); void seekPercentage(float percent); void resumePlayback(void); void stopPlayback(void); void pausePlayback(void); void cleanupPlaybackDevice(void); void togglePausePlayback(void); bool isPaused(void); bool isStopped(void); ma_device *getDevice(void); bool hasBuiltinDecoder(char *filePath); void setCurrentFileIndex(AudioData *pAudioData, int index); void activateSwitch(AudioData *pPCMDataSource); void executeSwitch(AudioData *pPCMDataSource); gint64 getLengthInMicroSec(double duration); 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); void clearCurrentTrack(void); void cleanupDbusConnection(); void freeLastCover(void); #endif kew-3.2.0/src/soundradio.c000066400000000000000000000742071500206121000154010ustar00rootroot00000000000000#include "miniaudio.h" #include "soundradio.h" /* radio.c Radio playback and search functions. */ #define MAX_SERVERS 12 #define MAX_STATIONS 1000 #define NI_MAXHOST 1025 #define WAIT_TIMEOUT_SECONDS 3 #define MAX_RECONNECT_RETRIES 20 typedef struct { char *memory; size_t size; } Memory; typedef struct { bool isBroken; char url[2048]; } Server; Server servers[MAX_SERVERS]; int serverCount = 0; bool hasUpdatedServerList = false; RadioSearchResult *currentlyPlayingRadioStation = NULL; RadioPlayerContext radioContext = {0}; int reconnectCounter = 0; bool radioIsActive = false; pthread_mutex_t radioLifecycleMutex = PTHREAD_MUTEX_INITIALIZER; typedef struct { const char *name; const char *code; } Country; typedef enum { STREAM_TYPE_UNKNOWN, STREAM_TYPE_HLS, STREAM_TYPE_MP3, STREAM_TYPE_AAC, STREAM_TYPE_MPEGTS, STREAM_TYPE_DASH, NUM_STREAM_TYPES } StreamType; Country countries[] = { {"Andorra", "AD"}, {"The United Arab Emirates", "AE"}, {"Afghanistan", "AF"}, {"Antigua And Barbuda", "AG"}, {"Anguilla", "AI"}, {"Albania", "AL"}, {"Armenia", "AM"}, {"Angola", "AO"}, {"Antarctica", "AQ"}, {"Argentina", "AR"}, {"American Samoa", "AS"}, {"Austria", "AT"}, {"Australia", "AU"}, {"Aruba", "AW"}, {"Aland Islands", "AX"}, {"Azerbaijan", "AZ"}, {"Bosnia And Herzegovina", "BA"}, {"Barbados", "BB"}, {"Bangladesh", "BD"}, {"Belgium", "BE"}, {"Burkina Faso", "BF"}, {"Bulgaria", "BG"}, {"Bahrain", "BH"}, {"Burundi", "BI"}, {"Benin", "BJ"}, {"Bermuda", "BM"}, {"Brunei Darussalam", "BN"}, {"Bolivia", "BO"}, {"Bonaire", "BQ"}, {"Brazil", "BR"}, {"The Bahamas", "BS"}, {"Botswana", "BW"}, {"Belarus", "BY"}, {"Belize", "BZ"}, {"Canada", "CA"}, {"The Democratic Republic Of The Congo", "CD"}, {"The Central African Republic", "CF"}, {"The Congo", "CG"}, {"Switzerland", "CH"}, {"Coted Ivoire", "CI"}, {"The Cook Islands", "CK"}, {"Chile", "CL"}, {"Cameroon", "CM"}, {"China", "CN"}, {"Colombia", "CO"}, {"Costa Rica", "CR"}, {"Cuba", "CU"}, {"Cabo Verde", "CV"}, {"Curacao", "CW"}, {"Christmas Island", "CX"}, {"Cyprus", "CY"}, {"Czechia", "CZ"}, {"Germany", "DE"}, {"Denmark", "DK"}, {"Dominica", "DM"}, {"The Dominican Republic", "DO"}, {"Algeria", "DZ"}, {"Ecuador", "EC"}, {"Estonia", "EE"}, {"Egypt", "EG"}, {"Eritrea", "ER"}, {"Spain", "ES"}, {"Ethiopia", "ET"}, {"Finland", "FI"}, {"Fiji", "FJ"}, {"The Falkland Islands Malvinas", "FK"}, {"The Faroe Islands", "FO"}, {"France", "FR"}, {"The United Kingdom Of Great Britain And Northern Ireland", "UK"}, {"Grenada", "GD"}, {"Georgia", "GE"}, {"French Guiana", "GF"}, {"Guernsey", "GG"}, {"Ghana", "GH"}, {"Gibraltar", "GI"}, {"Greenland", "GL"}, {"The Gambia", "GM"}, {"Guadeloupe", "GP"}, {"Equatorial Guinea", "GQ"}, {"Greece", "GR"}, {"Guatemala", "GT"}, {"Guam", "GU"}, {"Guinea Bissau", "GW"}, {"Guyana", "GY"}, {"Hong Kong", "HK"}, {"Honduras", "HN"}, {"Croatia", "HR"}, {"Haiti", "HT"}, {"Hungary", "HU"}, {"Indonesia", "ID"}, {"Ireland", "IE"}, {"Israel", "IL"}, {"Isle Of Man", "IM"}, {"India", "IN"}, {"British Indian Ocean Territory", "IO"}, {"Iraq", "IQ"}, {"Islamic Republic Of Iran", "IR"}, {"Iceland", "IS"}, {"Italy", "IT"}, {"Jamaica", "JM"}, {"Jordan", "JO"}, {"Japan", "JP"}, {"Kenya", "KE"}, {"Kyrgyzstan", "KG"}, {"Cambodia", "KH"}, {"The Comoros", "KM"}, {"Saint Kitts And Nevis", "KN"}, {"The Democratic Peoples Republic Of Korea", "KP"}, {"The Republic Of Korea", "KR"}, {"Kuwait", "KW"}, {"The Cayman Islands", "KY"}, {"Kazakhstan", "KZ"}, {"The Lao Peoples Democratic Republic", "LA"}, {"Lebanon", "LB"}, {"Saint Lucia", "LC"}, {"Liechtenstein", "LI"}, {"Sri Lanka", "LK"}, {"Lesotho", "LS"}, {"Lithuania", "LT"}, {"Luxembourg", "LU"}, {"Latvia", "LV"}, {"Libya", "LY"}, {"Morocco", "MA"}, {"Monaco", "MC"}, {"The Republic Of Moldova", "MD"}, {"Montenegro", "ME"}, {"Madagascar", "MG"}, {"Republic Of North Macedonia", "MK"}, {"Mali", "ML"}, {"Myanmar", "MM"}, {"Mongolia", "MN"}, {"Macao", "MO"}, {"Martinique", "MQ"}, {"Montserrat", "MS"}, {"Malta", "MT"}, {"Mauritius", "MU"}, {"Malawi", "MW"}, {"Mexico", "MX"}, {"Malaysia", "MY"}, {"Mozambique", "MZ"}, {"Namibia", "NA"}, {"New Caledonia", "NC"}, {"The Niger", "NE"}, {"Nigeria", "NG"}, {"Nicaragua", "NI"}, {"The Netherlands", "NL"}, {"Norway", "NO"}, {"Nepal", "NP"}, {"Niue", "NU"}, {"New Zealand", "NZ"}, {"Oman", "OM"}, {"Panama", "PA"}, {"Peru", "PE"}, {"French Polynesia", "PF"}, {"Papua New Guinea", "PG"}, {"The Philippines", "PH"}, {"Pakistan", "PK"}, {"Poland", "PL"}, {"Saint Pierre And Miquelon", "PM"}, {"Puerto Rico", "PR"}, {"State Of Palestine", "PS"}, {"Portugal", "PT"}, {"Palau", "PW"}, {"Paraguay", "PY"}, {"Qatar", "QA"}, {"Reunion", "RE"}, {"Romania", "RO"}, {"Serbia", "RS"}, {"The Russian Federation", "RU"}, {"Rwanda", "RW"}, {"Saudi Arabia", "SA"}, {"Seychelles", "SC"}, {"The Sudan", "SD"}, {"Sweden", "SE"}, {"Singapore", "SG"}, {"Ascension And Tristan Da Cunha Saint Helena", "SH"}, {"Slovenia", "SI"}, {"Svalbard And Jan Mayen", "SJ"}, {"Slovakia", "SK"}, {"Sierra Leone", "SL"}, {"San Marino", "SM"}, {"Senegal", "SN"}, {"Somalia", "SO"}, {"Suriname", "SR"}, {"South Sudan", "SS"}, {"Sao Tome And Principe", "ST"}, {"El Salvador", "SV"}, {"Syrian Arab Republic", "SY"}, {"Eswatini", "SZ"}, {"The Turks And Caicos Islands", "TC"}, {"Chad", "TD"}, {"The French Southern Territories", "TF"}, {"Togo", "TG"}, {"Thailand", "TH"}, {"Tajikistan", "TJ"}, {"Timor Leste", "TL"}, {"Turkmenistan", "TM"}, {"Tunisia", "TN"}, {"Türkiye", "TR"}, {"Trinidad And Tobago", "TT"}, {"Taiwan, Republic Of China", "TW"}, {"United Republic Of Tanzania", "TZ"}, {"Ukraine", "UA"}, {"Uganda", "UG"}, {"The United States Minor Outlying Islands", "UM"}, {"The United States Of America", "US"}, {"Uruguay", "UY"}, {"Uzbekistan", "UZ"}, {"The Holy See", "VA"}, {"Saint Vincent And The Grenadines", "VC"}, {"Bolivarian Republic Of Venezuela", "VE"}, {"British Virgin Islands", "VG"}, {"US Virgin Islands", "VI"}, {"Vietnam", "VN"}, {"Vanuatu", "VU"}, {"Wallis And Futuna", "WF"}, {"Kosovo", "XK"}, {"Yemen", "YE"}, {"Mayotte", "YT"}, {"South Africa", "ZA"}, {"Zambia", "ZM"}, {"Zimbabwe", "ZW"}}; typedef struct { char *searchTerm; void (*callback)(const char *, const char *, const char *, const char *, const int, const int); bool *stopFlag; } SearchThreadArgs; pthread_mutex_t server_list_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_t currentThread; bool stopFlag = false; const char *getCountryCode(const char *country_name) { for (int i = 0; i < (int)sizeof(countries) / (int)sizeof(countries[0]); i++) { if (strcmp(countries[i].name, country_name) == 0) { return countries[i].code; } } return ""; } size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { size_t real_size = size * nmemb; Memory *mem = (Memory *)userdata; char *ptr_new = realloc(mem->memory, mem->size + real_size + 1); if (!ptr_new) return 0; mem->memory = ptr_new; memcpy(mem->memory + mem->size, ptr, real_size); mem->size += real_size; mem->memory[mem->size] = '\0'; return real_size; } const char *json_get(const char *json, const char *key, char *dst, size_t dst_size) { const char *pos = strstr(json, key); if (!pos) return NULL; pos = strchr(pos, ':'); if (!pos) return NULL; pos++; // Skip whitespace and quotes while (*pos && (*pos == ' ' || *pos == '\"')) pos++; const char *end = pos; while (*end && *end != '\"' && *end != ',') end++; size_t len = end - pos; if (len >= dst_size) len = dst_size - 1; strncpy(dst, pos, len); dst[len] = '\0'; return end; } #define GETF(k, v) json_get(ptr, k, v, sizeof(v)) bool isControlChar(char c) { return (c >= 0 && c <= 31) || c == 127; } bool sanitizeUrl(const char *url) { if (strstr(url, "javascript:") != NULL || strstr(url, "data:") != NULL || strstr(url, "file:") != NULL || strstr(url, "vbscript:") != NULL) { return false; } for (const char *p = url; *p; p++) { if (isControlChar(*p)) { return false; } } return true; } bool isSafeURL(const char *url) { // Sanitize the URL first if (!sanitizeUrl(url)) { return false; } CURLUcode rc; CURLU *curlu = curl_url(); if (!curlu) { fprintf(stderr, "Failed to initialize CURLU.\n"); return false; } rc = curl_url_set(curlu, CURLUPART_URL, url, 0); if (rc != CURLUE_OK) { fprintf(stderr, "Invalid URL: %s\n", url); curl_url_cleanup(curlu); return false; } char *scheme = NULL; rc = curl_url_get(curlu, CURLUPART_SCHEME, &scheme, 0); if (rc != CURLUE_OK || !scheme) { fprintf(stderr, "Failed to get URL scheme.\n"); curl_free(scheme); curl_url_cleanup(curlu); return false; } bool is_safe = strcmp(scheme, "http") == 0 || strcmp(scheme, "https") == 0; curl_free(scheme); curl_url_cleanup(curlu); if (!is_safe) { fprintf(stderr, "Unsafe URL scheme: %s\n", url); } return is_safe; } int updateServerList() { struct addrinfo hints, *res, *p; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if (getaddrinfo("all.api.radio-browser.info", NULL, &hints, &res) != 0) return -1; serverCount = 0; for (p = res; p != NULL && serverCount < MAX_SERVERS; p = p->ai_next) { char host[NI_MAXHOST]; if (getnameinfo(p->ai_addr, p->ai_addrlen, host, sizeof(host), NULL, 0, NI_NAMEREQD) == 0) { snprintf(servers[serverCount].url, sizeof(servers[serverCount].url), "https://%s", host); servers[serverCount].isBroken = false; serverCount++; } } freeaddrinfo(res); return serverCount > 0 ? 0 : -1; } Server *pickServer() { for (int attempts = 0; attempts < serverCount; attempts++) { int idx = rand() % serverCount; if (!servers[idx].isBroken && isSafeURL(servers[idx].url)) return &servers[idx]; } return NULL; } bool IsActiveRadio() { return radioIsActive; } void *searchThreadFunction(void *arg) { SearchThreadArgs *args = (SearchThreadArgs *)arg; CURL *curl = NULL; Memory res = {.memory = NULL, .size = 0}; Server *server = NULL; char *encodedTerm = NULL; pthread_mutex_lock(&server_list_mutex); if (!hasUpdatedServerList) { if (updateServerList() != 0) { pthread_mutex_unlock(&server_list_mutex); free(args->searchTerm); free(args); setErrorMessage("Radio database unavailable."); return NULL; } hasUpdatedServerList = true; } pthread_mutex_unlock(&server_list_mutex); while (true) { pthread_mutex_lock(&server_list_mutex); server = pickServer(); pthread_mutex_unlock(&server_list_mutex); if (!server) { break; } free(res.memory); res.memory = NULL; res.size = 0; curl = curl_easy_init(); if (!curl) { continue; } encodedTerm = curl_easy_escape(curl, args->searchTerm, 0); if (!encodedTerm) { curl_easy_cleanup(curl); curl = NULL; continue; } char url[2070]; snprintf(url, sizeof(url), "%s/json/stations/byname/%s", server->url, encodedTerm); curl_free(encodedTerm); encodedTerm = NULL; curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res); char userAgent[64]; snprintf(userAgent, sizeof(userAgent), "kew/%s", VERSION); curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); CURLcode result = curl_easy_perform(curl); curl_easy_cleanup(curl); curl = NULL; if (result == CURLE_OK && res.memory != NULL) { char *ptr = res.memory; int count = 0; while ((ptr = strchr(ptr, '{')) != NULL && count < MAX_STATIONS) { char name[128] = {0}, resolved[4096] = {0}, country[64] = {0}, codec[32] = {0}, bitrate_str[16] = {0}, votes_str[16] = {0}; int br = 0, vo = 0; if (!GETF("\"name\"", name)) { ptr++; continue; } if (!GETF("\"url_resolved\"", resolved)) { ptr++; continue; } GETF("\"country\"", country); GETF("\"codec\"", codec); if (GETF("\"bitrate\"", bitrate_str)) br = atoi(bitrate_str); if (GETF("\"votes\"", votes_str)) vo = atoi(votes_str); if (!isSafeURL(resolved)) { ptr++; continue; } args->callback(name, resolved, getCountryCode(country), codec, br, vo); count++; ptr++; if (count % 10 == 0) { refresh = true; c_sleep(100); } } free(res.memory); res.memory = NULL; res.size = 0; free(encodedTerm); if (curl) curl_easy_cleanup(curl); free(args->searchTerm); free(args); return NULL; } else { pthread_mutex_lock(&server_list_mutex); server->isBroken = true; pthread_mutex_unlock(&server_list_mutex); free(res.memory); res.memory = NULL; res.size = 0; } } return NULL; } void stopCurrentThread() { if (currentThread) { stopFlag = true; pthread_cancel(currentThread); pthread_join(currentThread, NULL); stopFlag = false; } } int internetRadioSearch(const char *searchTerm, void (*callback)(const char *, const char *, const char *, const char *, const int, const int)) { pthread_t threadId; SearchThreadArgs *args; if (!searchTerm || !callback) { return -1; } stopCurrentThread(); args = malloc(sizeof(SearchThreadArgs)); if (!args) { return -1; } args->searchTerm = strdup(searchTerm); if (!args->searchTerm) { free(args); return -1; } args->callback = callback; args->stopFlag = &stopFlag; if (pthread_create(&threadId, NULL, searchThreadFunction, args) != 0) { free(args->searchTerm); free(args); return -1; } pthread_detach(threadId); currentThread = threadId; return 0; } static bool radioIsPlaying = false; bool isRadioPlaying() { return radioIsPlaying; } static size_t curl_writefunc(void *ptr, size_t size, size_t nmemb, void *userdata) { stream_buffer *buf = (stream_buffer *)userdata; size_t bytes = size * nmemb; size_t written = 0; pthread_mutex_lock(&(buf->mutex)); if (buf->eof) { pthread_mutex_unlock(&(buf->mutex)); return 0; } unsigned char *src = (unsigned char *)ptr; while (written < bytes) { size_t nextWritePos = (buf->write_pos + 1) % STREAM_BUFFER_SIZE; // If buffer is full, advance read pointer (drop oldest) if (nextWritePos == buf->read_pos) { buf->read_pos = (buf->read_pos + 1) % STREAM_BUFFER_SIZE; } buf->buffer[buf->write_pos] = src[written]; buf->write_pos = nextWritePos; written++; } buf->last_data_time = time(NULL); // Signal to the consumer that the data is ready pthread_cond_signal(&(buf->cond)); pthread_mutex_unlock(&(buf->mutex)); return bytes; } static ma_result decoder_read_callback(ma_decoder *decoder, void *pBufferOut, size_t bytesToRead, size_t *bytesRead) { stream_buffer *buf = decoder->pUserData; size_t total_read = 0; unsigned char *out = (unsigned char *)pBufferOut; #ifndef __APPLE__ int rc = pthread_mutex_lock(&(buf->mutex)); if (rc == EOWNERDEAD) { // Thread died without unlocking pthread_mutex_consistent(&(buf->mutex)); } #else pthread_mutex_lock(&(buf->mutex)); #endif while (total_read < bytesToRead) { if ((buf->read_pos == buf->write_pos) && buf->eof) { pthread_mutex_unlock(&(buf->mutex)); *bytesRead = total_read; return (total_read > 0) ? MA_SUCCESS : MA_AT_END; } // Wait until data is available OR EOF is flagged. while ((buf->read_pos == buf->write_pos) && !buf->eof) { // Set up a timespec for timeout struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += WAIT_TIMEOUT_SECONDS; // Use timed wait int wait_result = pthread_cond_timedwait(&(buf->cond), &(buf->mutex), &ts); if (wait_result == ETIMEDOUT) { // Check how long it's been since last data arrived time_t now = time(NULL); if ((now - buf->last_data_time) >= WAIT_TIMEOUT_SECONDS) { // We haven't received data in too long, so signal error. pthread_mutex_unlock(&(buf->mutex)); *bytesRead = total_read; buf->stale = true; return MA_ERROR; } } } if ((buf->read_pos == buf->write_pos) && buf->eof) { pthread_mutex_unlock(&(buf->mutex)); *bytesRead = total_read; return (total_read > 0) ? MA_SUCCESS : MA_AT_END; } size_t chunk = (buf->write_pos > buf->read_pos) ? (buf->write_pos - buf->read_pos) : (STREAM_BUFFER_SIZE - buf->read_pos); size_t to_copy = (chunk < (bytesToRead - total_read)) ? chunk : (bytesToRead - total_read); memcpy(out + total_read, &(buf->buffer[buf->read_pos]), to_copy); buf->read_pos = (buf->read_pos + to_copy) % STREAM_BUFFER_SIZE; total_read += to_copy; } pthread_mutex_unlock(&(buf->mutex)); *bytesRead = total_read; return MA_SUCCESS; } static ma_result decoder_seek_callback(ma_decoder *decoder, ma_int64 offset, ma_seek_origin origin) { (void)decoder; (void)offset; (void)origin; return MA_ERROR; // Can't seek in internet radio } static void audio_data_callback(ma_device *device, void *output, const void *input, ma_uint32 frameCount) { (void)input; ma_decoder *decoder = (ma_decoder *)device->pUserData; ma_uint64 framesRead = 0; ma_decoder_read_pcm_frames(decoder, output, frameCount, &framesRead); if (framesRead < frameCount) { memset((char *)output + framesRead * device->playback.channels * sizeof(float), 0, (frameCount - framesRead) * device->playback.channels * sizeof(float)); } setBufferSize(framesRead); radioIsActive = true; setAudioBuffer(output, framesRead); } RadioSearchResult *copyRadioSearchResult(const RadioSearchResult *original) { if (!original || strlen(original->name) <= 0) return NULL; RadioSearchResult *copy = malloc(sizeof(RadioSearchResult)); if (!copy) { fprintf(stderr, "Memory allocation failed.\n"); return NULL; } memcpy(copy, original, sizeof(RadioSearchResult)); return copy; } RadioSearchResult *getCurrentPlayingRadioStation(void) { return currentlyPlayingRadioStation; } void setCurrentlyPlayingRadioStation(const RadioSearchResult *result) { if (currentlyPlayingRadioStation != NULL) freeCurrentlyPlayingRadioStation(); if (result != NULL) currentlyPlayingRadioStation = copyRadioSearchResult(result); } void freeCurrentlyPlayingRadioStation(void) { if (currentlyPlayingRadioStation != NULL) { free(currentlyPlayingRadioStation); currentlyPlayingRadioStation = NULL; } } void initRadioMutexes() { #ifndef __APPLE__ pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST); pthread_mutex_init(&(radioContext.buf.mutex), &attr); #else pthread_mutex_init(&(radioContext.buf.mutex), NULL); #endif pthread_cond_init(&(radioContext.buf.cond), NULL); } void destroyRadioMutexes() { pthread_mutex_destroy(&(radioContext.buf.mutex)); pthread_cond_destroy(&(radioContext.buf.cond)); pthread_mutex_destroy(&radioLifecycleMutex); } int stopRadio(void) { #ifndef __APPLE__ int rc = pthread_mutex_lock(&(radioContext.buf.mutex)); if (rc == EOWNERDEAD) { // Thread died without unlocking pthread_mutex_consistent(&(radioContext.buf.mutex)); } #else pthread_mutex_lock(&(radioContext.buf.mutex)); #endif radioContext.buf.eof = 1; radioContext.buf.stale = false; pthread_cond_broadcast(&(radioContext.buf.cond)); pthread_mutex_unlock(&(radioContext.buf.mutex)); cleanupPlaybackDevice(); if (radioContext.curl_thread) { pthread_join(radioContext.curl_thread, NULL); radioContext.curl_thread = 0; } ma_decoder_uninit(&(radioContext.decoder)); if (radioContext.curl != NULL) { curl_easy_cleanup(radioContext.curl); radioContext.curl = NULL; } radioContext.buf.read_pos = radioContext.buf.write_pos = 0; memset(&(radioContext.decoder), 0, sizeof(ma_decoder)); radioIsPlaying = false; freeCurrentlyPlayingRadioStation(); radioIsActive = false; return 0; } void reconnectRadioIfNeeded() { if (radioContext.buf.stale && radioIsPlaying && reconnectCounter < MAX_RECONNECT_RETRIES) { RadioSearchResult *station = copyRadioSearchResult(getCurrentPlayingRadioStation()); playRadioStation(station); reconnectCounter++; if (station) free(station); } } void *curl_perform_wrapper(void *arg) { CURL *curl = (CURL *)arg; CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); } return NULL; } int playRadioStation(const RadioSearchResult *station) { if (isRadioPlaying() && !radioIsActive) // Don't stop and start if things haven't really started return -1; if (station == NULL) { return -1; } pthread_mutex_lock(&radioLifecycleMutex); if (isRadioPlaying()) { RadioSearchResult *radio = getCurrentPlayingRadioStation(); // If it's not the same station, reset the reconnect counter if (radio && strcmp(radio->url_resolved, station->url_resolved) != 0) reconnectCounter = 0; } if (stopRadio() < 0) { pthread_mutex_unlock(&radioLifecycleMutex); return -1; } if (!(radioContext.curl = curl_easy_init())) { pthread_mutex_unlock(&radioLifecycleMutex); return -1; } radioContext.buf.eof = 0; radioContext.buf.stale = false; curl_easy_setopt(radioContext.curl, CURLOPT_URL, station->url_resolved); curl_easy_setopt(radioContext.curl, CURLOPT_WRITEFUNCTION, curl_writefunc); curl_easy_setopt(radioContext.curl, CURLOPT_WRITEDATA, &(radioContext.buf)); curl_easy_setopt(radioContext.curl, CURLOPT_FOLLOWLOCATION, 1L); char userAgent[64]; snprintf(userAgent, sizeof(userAgent), "kew/%s", VERSION); curl_easy_setopt(radioContext.curl, CURLOPT_USERAGENT, userAgent); pthread_create(&(radioContext.curl_thread), NULL, curl_perform_wrapper, radioContext.curl); ma_decoder_config decoderConfig = ma_decoder_config_init(ma_format_f32, 2, 44100); decoderConfig.encodingFormat = ma_encoding_format_mp3; if (ma_decoder_init(decoder_read_callback, decoder_seek_callback, &(radioContext.buf), &decoderConfig, &(radioContext.decoder)) != MA_SUCCESS) { fprintf(stderr, "Decoder init failed\n"); setErrorMessage("Radio station unsupported or unavailable."); pthread_mutex_unlock(&radioLifecycleMutex); return -1; } ma_device_config devConfig = ma_device_config_init(ma_device_type_playback); devConfig.playback.format = radioContext.decoder.outputFormat; devConfig.playback.channels = radioContext.decoder.outputChannels; devConfig.sampleRate = radioContext.decoder.outputSampleRate; devConfig.dataCallback = audio_data_callback; devConfig.pUserData = &(radioContext.decoder); if (ma_device_init(NULL, &devConfig, &device) != MA_SUCCESS) { fprintf(stderr, "Device init failed\n"); ma_decoder_uninit(&(radioContext.decoder)); pthread_mutex_unlock(&radioLifecycleMutex); return -1; } setVolume(getCurrentVolume()); if (ma_device_start(&device) != MA_SUCCESS) { fprintf(stderr, "Failed to start device playback\n"); ma_device_uninit(&device); ma_decoder_uninit(&(radioContext.decoder)); pthread_mutex_unlock(&radioLifecycleMutex); return -1; } while (ma_device_get_state(&device) == ma_device_state_starting) { c_sleep(100); } stopped = false; paused = false; refresh = true; pthread_cond_signal(&(radioContext.buf.cond)); radioIsPlaying = true; setCurrentlyPlayingRadioStation(station); pthread_mutex_unlock(&radioLifecycleMutex); return 0; } kew-3.2.0/src/soundradio.h000066400000000000000000000030751500206121000154010ustar00rootroot00000000000000#ifndef SOUND_RADIO_H #define SOUND_RADIO_H #include #include #include #include #include #include #include #include #include #include #include #include #include "soundcommon.h" #include "common.h" typedef struct RadioSearchResult { char name[256]; char url_resolved[2048]; char country[64]; char codec[32]; int bitrate; int votes; } RadioSearchResult; #define STREAM_BUFFER_SIZE (256 * 1024) typedef struct { unsigned char buffer[STREAM_BUFFER_SIZE]; size_t write_pos; size_t read_pos; int eof; pthread_mutex_t mutex; pthread_cond_t cond; time_t last_data_time; bool stale; } stream_buffer; typedef struct { stream_buffer buf; CURL *curl; pthread_t curl_thread; ma_decoder decoder; bool isRunning; } RadioPlayerContext; extern RadioPlayerContext radioContext; int internetRadioSearch(const char *searchTerm, void (*callback)(const char *, const char *, const char *, const char *, const int, const int)); int playRadioStation(const RadioSearchResult *station); int stopRadio(void); void reconnectRadioIfNeeded(); bool isRadioPlaying(void); bool IsActiveRadio(void); RadioSearchResult *getCurrentPlayingRadioStation(void); void setCurrentlyPlayingRadioStation(const RadioSearchResult * result); void freeCurrentlyPlayingRadioStation(void); void initRadioMutexes(void); void destroyRadioMutexes(void); #endif kew-3.2.0/src/tagLibWrapper.cpp000066400000000000000000001232621500206121000163310ustar00rootroot00000000000000// taglib_wrapper.cpp #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* tagLibWrapper.cpp Related to extracting meta tags and cover from audio files. */ #if defined(__linux__) #include #else #include #endif #include "tagLibWrapper.h" // Base64 character map for decoding static const std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/"; static inline bool is_base64(unsigned char c) { return (isalnum(c) || (c == '+') || (c == '/')); } // Function to decode Base64-encoded data std::vector decodeBase64(const std::string &encoded_string) { size_t in_len = encoded_string.size(); size_t i = 0; size_t in_ = 0; unsigned char char_array_4[4], char_array_3[3]; std::vector decoded_data; while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { char_array_4[i++] = encoded_string[in_]; in_++; if (i == 4) { for (i = 0; i < 4; i++) char_array_4[i] = static_cast(base64_chars.find(char_array_4[i])); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (i = 0; i < 3; i++) decoded_data.push_back(char_array_3[i]); i = 0; } } if (i) { for (size_t j = i; j < 4; j++) char_array_4[j] = 0; for (size_t j = 0; j < 4; j++) char_array_4[j] = static_cast(base64_chars.find(char_array_4[j])); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (size_t j = 0; j < i - 1; j++) decoded_data.push_back(char_array_3[j]); } return decoded_data; } extern "C" { // Function to read a 32-bit unsigned integer from buffer in big-endian format unsigned int read_uint32_be(const unsigned char *buffer, size_t offset) { return (static_cast(buffer[offset]) << 24) | (static_cast(buffer[offset + 1]) << 16) | (static_cast(buffer[offset + 2]) << 8) | static_cast(buffer[offset + 3]); } void parseFlacPictureBlock(const std::vector &data, std::string &mimeType, std::vector &imageData) { const unsigned char *ptr = data.data(); size_t offset = 0; auto readUInt32 = [&](uint32_t &value) { value = (ptr[offset] << 24) | (ptr[offset + 1] << 16) | (ptr[offset + 2] << 8) | ptr[offset + 3]; offset += 4; }; uint32_t pictureType, mimeLength, descLength, width, height, depth, colors, dataLength; readUInt32(pictureType); readUInt32(mimeLength); mimeType = std::string(reinterpret_cast(&ptr[offset]), mimeLength); offset += mimeLength; readUInt32(descLength); offset += descLength; // Skip description readUInt32(width); readUInt32(height); readUInt32(depth); readUInt32(colors); readUInt32(dataLength); imageData.assign(&ptr[offset], &ptr[offset + dataLength]); } int extractCoverArtFromOgg(const std::string &audioFilePath, const std::string &outputFileName) { TagLib::File *file = nullptr; TagLib::Tag *tag = nullptr; // Try to open as Ogg Vorbis file = new TagLib::Vorbis::File(audioFilePath.c_str()); if (!file->isValid()) { delete file; // Try to open as Opus file = new TagLib::Ogg::Opus::File(audioFilePath.c_str()); if (!file->isValid()) { delete file; std::cerr << "Error: File not found or not a valid Ogg Vorbis or Opus file." << std::endl; return false; // File not found or invalid } } tag = file->tag(); const TagLib::Ogg::XiphComment *xiphComment = dynamic_cast(tag); if (!xiphComment) { std::cerr << "Error: No XiphComment found in the file." << std::endl; delete file; return false; // No cover art found } // Check METADATA_BLOCK_PICTURE TagLib::StringList pictureList = xiphComment->fieldListMap()["METADATA_BLOCK_PICTURE"]; if (!pictureList.isEmpty()) { std::string base64Data = pictureList.front().to8Bit(true); std::vector decodedData = decodeBase64(base64Data); std::string mimeType; std::vector imageData; parseFlacPictureBlock(decodedData, mimeType, imageData); std::ofstream outFile(outputFileName, std::ios::binary); if (!outFile) { std::cerr << "Error: Could not write to output file." << std::endl; delete file; return false; // Could not write to output file } outFile.write(reinterpret_cast(imageData.data()), imageData.size()); outFile.close(); delete file; return 0; // Success } // Check COVERART and COVERARTMIME TagLib::StringList coverArtList = xiphComment->fieldListMap()["COVERART"]; TagLib::StringList coverArtMimeList = xiphComment->fieldListMap()["COVERARTMIME"]; if (!coverArtList.isEmpty() && !coverArtMimeList.isEmpty()) { std::string base64Data = coverArtList.front().to8Bit(true); std::vector imageData = decodeBase64(base64Data); std::ofstream outFile(outputFileName, std::ios::binary); if (!outFile) { std::cerr << "Error: Could not write to output file." << std::endl; delete file; return false; // Could not write to output file } outFile.write(reinterpret_cast(imageData.data()), imageData.size()); outFile.close(); delete file; return true; // Success } std::cerr << "No cover art found in the file." << std::endl; delete file; return false; // No cover art found } bool extractCoverArtFromOggVideo(const std::string &audioFilePath, const std::string &outputFileName) { FILE *oggFile = fopen(audioFilePath.c_str(), "rb"); if (!oggFile) { std::cerr << "Error: Could not open file." << std::endl; return false; // File not found or could not be opened } ogg_sync_state oy; ogg_sync_init(&oy); ogg_page og; ogg_packet op; std::map streams; std::map> imageDataStreams; std::map isImageStream; // Track if a stream is an image (JPEG/PNG) char *buffer; int bytes; bool coverArtFound = false; // Read through the Ogg container to find and extract PNG or JPEG streams while (true) { buffer = ogg_sync_buffer(&oy, 4096); bytes = fread(buffer, 1, 4096, oggFile); ogg_sync_wrote(&oy, bytes); while (ogg_sync_pageout(&oy, &og) == 1) { int serialNo = ogg_page_serialno(&og); // Initialize stream if not already done if (streams.find(serialNo) == streams.end()) { ogg_stream_state os; ogg_stream_init(&os, serialNo); streams[serialNo] = os; isImageStream[serialNo] = false; // Start by assuming this is not an image stream } ogg_stream_state &os = streams[serialNo]; ogg_stream_pagein(&os, &og); while (ogg_stream_packetout(&os, &op) == 1) { // Check the packet for PNG signature (\x89PNG\r\n\x1A\n) or JPEG SOI (\xFF\xD8) if (op.bytes >= 8 && std::memcmp(op.packet, "\x89PNG\r\n\x1A\n", 8) == 0) { isImageStream[serialNo] = true; } else if (op.bytes >= 2 && op.packet[0] == 0xFF && op.packet[1] == 0xD8) { isImageStream[serialNo] = true; } else if (op.bytes >= 12 && std::memcmp(op.packet, "RIFF", 4) == 0 && std::memcmp(op.packet + 8, "WEBP", 4) == 0) { isImageStream[serialNo] = true; } // If this is a detected image stream (either PNG or JPEG), collect the packet data if (isImageStream[serialNo]) { imageDataStreams[serialNo].insert( imageDataStreams[serialNo].end(), op.packet, op.packet + op.bytes); } } } // Stop reading if the file ends if (bytes == 0) { break; } } fclose(oggFile); ogg_sync_clear(&oy); // Process collected image data streams for (const auto &entry : imageDataStreams) { if (isImageStream[entry.first] && !entry.second.empty()) { // Save the image data to a file using outputFileName std::ofstream outFile(outputFileName, std::ios::binary); if (!outFile) { std::cerr << "Error: Could not write to output file." << std::endl; // Clean up stream states for (auto &streamEntry : streams) { ogg_stream_clear(&(streamEntry.second)); } return false; // Could not write to output file } outFile.write(reinterpret_cast(entry.second.data()), entry.second.size()); outFile.close(); coverArtFound = true; break; // Stop after finding and writing the first cover art } } // Clean up stream states for (auto &streamEntry : streams) { ogg_stream_clear(&(streamEntry.second)); } // Return whether the cover art was successfully found and written if (!coverArtFound) { std::cerr << "No cover art found in the file." << std::endl; return false; // No cover art found } return true; // Success } bool extractCoverArtFromMp3(const std::string &inputFile, const std::string &coverFilePath) { TagLib::MPEG::File file(inputFile.c_str()); if (!file.isValid()) { return false; } const TagLib::ID3v2::Tag *id3v2tag = file.ID3v2Tag(); if (id3v2tag) { // Collect all attached picture frames TagLib::ID3v2::FrameList frames; frames.append(id3v2tag->frameListMap()["APIC"]); frames.append(id3v2tag->frameListMap()["PIC"]); if (!frames.isEmpty()) { for (auto it = frames.begin(); it != frames.end(); ++it) { const TagLib::ID3v2::AttachedPictureFrame *picFrame = dynamic_cast(*it); if (picFrame) { // Access picture data and MIME type TagLib::ByteVector pictureData = picFrame->picture(); TagLib::String mimeType = picFrame->mimeType(); // Construct the output file path std::string outputFilePath = coverFilePath; // Write the image data to a file FILE *outFile = fopen(outputFilePath.c_str(), "wb"); if (outFile) { fwrite(pictureData.data(), 1, pictureData.size(), outFile); fclose(outFile); return true; } else { return false; // Failed to open output file } // Break if only the first image is needed break; } } } else { return false; // No picture frames found } } else { return false; // No ID3v2 tag found } return true; // Success } bool extractCoverArtFromFlac(const std::string &inputFile, const std::string &coverFilePath) { TagLib::FLAC::File file(inputFile.c_str()); if (file.pictureList().size() > 0) { const TagLib::FLAC::Picture *picture = file.pictureList().front(); if (picture) { FILE *coverFile = fopen(coverFilePath.c_str(), "wb"); if (coverFile) { fwrite(picture->data().data(), 1, picture->data().size(), coverFile); fclose(coverFile); return true; } else { return false; } } } return false; } bool extractCoverArtFromWav(const std::string &inputFile, const std::string &coverFilePath) { TagLib::RIFF::WAV::File file(inputFile.c_str()); if (!file.isValid()) { return false; } const TagLib::ID3v2::Tag *id3v2tag = file.ID3v2Tag(); if (id3v2tag) { // Collect all attached picture frames TagLib::ID3v2::FrameList frames; frames.append(id3v2tag->frameListMap()["APIC"]); frames.append(id3v2tag->frameListMap()["PIC"]); if (!frames.isEmpty()) { for (auto it = frames.begin(); it != frames.end(); ++it) { const TagLib::ID3v2::AttachedPictureFrame *picFrame = dynamic_cast(*it); if (picFrame) { // Access picture data and MIME type TagLib::ByteVector pictureData = picFrame->picture(); TagLib::String mimeType = picFrame->mimeType(); // Construct the output file path std::string outputFilePath = coverFilePath; // Write the image data to a file FILE *outFile = fopen(outputFilePath.c_str(), "wb"); if (outFile) { fwrite(pictureData.data(), 1, pictureData.size(), outFile); fclose(outFile); return true; } else { return false; // Failed to open output file } // Break if only the first image is needed break; } } } else { return false; // No picture frames found } } else { return false; // No ID3v2 tag found } return true; // Success } bool extractCoverArtFromOpus(const std::string &audioFilePath, const std::string &outputFileName) { int error; OggOpusFile *of = op_open_file(audioFilePath.c_str(), &error); if (error != OPUS_OK || of == nullptr) { std::cerr << "Error: Failed to open Opus file." << std::endl; return false; } const OpusTags *tags = op_tags(of, -1); if (!tags) { std::cerr << "Error: No tags found in Opus file." << std::endl; op_free(of); return false; } // Search through the metadata for an attached picture (if present) for (int i = 0; i < tags->comments; ++i) { // Check for METADATA_BLOCK_PICTURE const char *comment = tags->user_comments[i]; if (strncmp(comment, "METADATA_BLOCK_PICTURE=", 23) == 0) { // Extract the value after "METADATA_BLOCK_PICTURE=" std::string metadataBlockPicture(comment + 23); // Base64-decode this value to get the binary PICTURE block std::vector pictureBlock = decodeBase64(metadataBlockPicture); if (pictureBlock.empty()) { std::cerr << "Failed to decode Base64 data." << std::endl; op_free(of); return false; } // Now parse the binary pictureBlock to extract the image data size_t offset = 0; if (pictureBlock.size() < 32) { std::cerr << "Picture block too small." << std::endl; op_free(of); return false; } // Read PICTURE TYPE read_uint32_be(pictureBlock.data(), offset); offset += 4; // Read MIME TYPE LENGTH unsigned int mimeTypeLength = read_uint32_be(pictureBlock.data(), offset); offset += 4; // Read MIME TYPE if (offset + mimeTypeLength > pictureBlock.size()) { op_free(of); return false; } offset += mimeTypeLength; // Read DESCRIPTION LENGTH unsigned int descriptionLength = read_uint32_be(pictureBlock.data(), offset); offset += 4; // Read DESCRIPTION if (offset + descriptionLength > pictureBlock.size()) { op_free(of); return false; } offset += descriptionLength; // Optionally print or ignore description // Read WIDTH read_uint32_be(pictureBlock.data(), offset); offset += 4; // Read HEIGHT read_uint32_be(pictureBlock.data(), offset); offset += 4; ; // Read COLOR DEPTH read_uint32_be(pictureBlock.data(), offset); offset += 4; // Read NUMBER OF COLORS read_uint32_be(pictureBlock.data(), offset); offset += 4; // Read DATA LENGTH unsigned int dataLength = read_uint32_be(pictureBlock.data(), offset); offset += 4; if (offset + dataLength > pictureBlock.size()) { std::cerr << "Invalid image data length." << std::endl; op_free(of); return false; } // Extract image data std::vector imageData(pictureBlock.begin() + offset, pictureBlock.begin() + offset + dataLength); // Save image data to file std::ofstream outFile(outputFileName, std::ios::binary); if (!outFile) { std::cerr << "Error: Could not write to output file." << std::endl; op_free(of); return false; } outFile.write(reinterpret_cast(imageData.data()), imageData.size()); outFile.close(); op_free(of); return true; } } std::cerr << "No cover art found in the metadata." << std::endl; op_free(of); return false; } bool extractCoverArtFromMp4(const std::string &inputFile, const std::string &coverFilePath) { TagLib::MP4::File file(inputFile.c_str()); if (!file.isValid()) { return false; } const TagLib::MP4::Item coverItem = file.tag()->item("covr"); if (coverItem.isValid()) { TagLib::MP4::CoverArtList coverArtList = coverItem.toCoverArtList(); if (!coverArtList.isEmpty()) { const TagLib::MP4::CoverArt &coverArt = coverArtList.front(); FILE *coverFile = fopen(coverFilePath.c_str(), "wb"); if (coverFile) { fwrite(coverArt.data().data(), 1, coverArt.data().size(), coverFile); fclose(coverFile); return true; // Success } else { fprintf(stderr, "Could not open output file '%s'\n", coverFilePath.c_str()); return false; // Failed to open the output file } } } return false; // No valid cover item or cover art found } void trimcpp(std::string &str) { // Remove leading spaces str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](unsigned char ch) { return !std::isspace(ch); })); // Remove trailing spaces str.erase(std::find_if(str.rbegin(), str.rend(), [](unsigned char ch) { return !std::isspace(ch); }) .base(), str.end()); } void turnFilePathIntoTitle(const char *filePath, char *title, size_t titleMaxLength) { std::string filePathStr(filePath); // Convert the C-style string to std::string size_t lastSlashPos = filePathStr.find_last_of("/\\"); // Find the last '/' or '\\' size_t lastDotPos = filePathStr.find_last_of('.'); // Find the last '.' // Validate that both positions exist and the dot is after the last slash if (lastSlashPos != std::string::npos && lastDotPos != std::string::npos && lastDotPos > lastSlashPos) { // Extract the substring between the last slash and the last dot std::string extractedTitle = filePathStr.substr(lastSlashPos + 1, lastDotPos - lastSlashPos - 1); // Trim any unwanted spaces trimcpp(extractedTitle); // Ensure title is not longer than titleMaxLength, including the null terminator if (extractedTitle.length() >= titleMaxLength) { extractedTitle = extractedTitle.substr(0, titleMaxLength - 1); } // Copy the result into the output char* title, ensuring no overflow c_strcpy(title, extractedTitle.c_str(), titleMaxLength - 1); // Copy up to titleMaxLength - 1 characters title[titleMaxLength - 1] = '\0'; // Null-terminate the string } else { // If no valid title is found, ensure title is an empty string title[0] = '\0'; } } double parseDecibelValue(const TagLib::String &dbString) { double val = 0.0; try { std::string valStr = dbString.to8Bit(true); std::string filtered; for (char c : valStr) { if (std::isdigit((unsigned char)c) || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E') { filtered.push_back(c); } } val = std::stod(filtered); } catch (...) { } return val; } int extractTags(const char *input_file, TagSettings *tag_settings, double *duration, const char *coverFilePath) { memset(tag_settings, 0, sizeof(TagSettings)); // Initialize tag settings tag_settings->replaygainTrack = 0.0; tag_settings->replaygainAlbum = 0.0; // Use TagLib's FileRef for generic file parsing. TagLib::FileRef f(input_file); if (f.isNull() || !f.file()) { fprintf(stderr, "FileRef is null or file could not be opened: '%s'\n", input_file); char title[4096]; turnFilePathIntoTitle(input_file, title, 4096); c_strcpy(tag_settings->title, title, sizeof(tag_settings->title) - 1); tag_settings->title[sizeof(tag_settings->title) - 1] = '\0'; return -1; } // Extract tags using the stable method that worked before. const TagLib::Tag *tag = f.tag(); if (!tag) { fprintf(stderr, "Tag is null for file '%s'\n", input_file); return -2; } // Copy the title c_strcpy(tag_settings->title, tag->title().toCString(true), sizeof(tag_settings->title) - 1); tag_settings->title[sizeof(tag_settings->title) - 1] = '\0'; // Check if the title is empty, and if so, use the file path to generate a title if (strnlen(tag_settings->title, 10) == 0) { char title[4096]; turnFilePathIntoTitle(input_file, title, 4096); c_strcpy(tag_settings->title, title, sizeof(tag_settings->title) - 1); tag_settings->title[sizeof(tag_settings->title) - 1] = '\0'; } else { // Copy the artist c_strcpy(tag_settings->artist, tag->artist().toCString(true), sizeof(tag_settings->artist) - 1); tag_settings->artist[sizeof(tag_settings->artist) - 1] = '\0'; // Copy the album c_strcpy(tag_settings->album, tag->album().toCString(true), sizeof(tag_settings->album) - 1); tag_settings->album[sizeof(tag_settings->album) - 1] = '\0'; // Copy the year as date snprintf(tag_settings->date, sizeof(tag_settings->date), "%d", (int)tag->year()); if (tag_settings->date[0] == '0') { tag_settings->date[0] = '\0'; } } // Extract audio properties for duration. if (f.audioProperties()) { *duration = f.audioProperties()->lengthInSeconds(); } else { *duration = 0.0; fprintf(stderr, "No audio properties found for file '%s'\n", input_file); return -2; } // Extract replay gain information if (std::string(input_file).find(".mp3") != std::string::npos) { TagLib::MPEG::File mp3File(input_file); TagLib::ID3v2::Tag *id3v2Tag = mp3File.ID3v2Tag(); if (id3v2Tag) { // Retrieve all TXXX frames TagLib::ID3v2::FrameList frames = id3v2Tag->frameList("TXXX"); for (TagLib::ID3v2::FrameList::Iterator it = frames.begin(); it != frames.end(); ++it) { // Cast to the user-text (TXXX) frame class TagLib::ID3v2::TextIdentificationFrame *txxx = dynamic_cast(*it); if (!txxx) continue; TagLib::StringList fields = txxx->fieldList(); if (fields.size() >= 2) { TagLib::String desc = fields[0]; TagLib::String val = fields[1]; if (desc.upper() == "REPLAYGAIN_TRACK_GAIN") { tag_settings->replaygainTrack = parseDecibelValue(val); } else if (desc.upper() == "REPLAYGAIN_ALBUM_GAIN") { tag_settings->replaygainAlbum = parseDecibelValue(val); } } } } TagLib::APE::Tag *apeTag = mp3File.APETag(); if (apeTag) { TagLib::APE::ItemListMap items = apeTag->itemListMap(); for (auto it = items.begin(); it != items.end(); ++it) { std::string key = it->first.upper().toCString(); TagLib::String value = it->second.toString(); if (key == "REPLAYGAIN_TRACK_GAIN") { tag_settings->replaygainTrack = parseDecibelValue(value); } else if (key == "REPLAYGAIN_ALBUM_GAIN") { tag_settings->replaygainAlbum = parseDecibelValue(value); } } } } else if (std::string(input_file).find(".flac") != std::string::npos) { TagLib::FLAC::File flacFile(input_file); TagLib::Ogg::XiphComment *xiphComment = flacFile.xiphComment(); if (xiphComment) { const TagLib::Ogg::FieldListMap &fieldMap = xiphComment->fieldListMap(); auto trackGainIt = fieldMap.find("REPLAYGAIN_TRACK_GAIN"); if (trackGainIt != fieldMap.end()) { const TagLib::StringList &trackGainList = trackGainIt->second; if (!trackGainList.isEmpty()) { tag_settings->replaygainTrack = parseDecibelValue(trackGainList.front()); } } auto albumGainIt = fieldMap.find("REPLAYGAIN_ALBUM_GAIN"); if (albumGainIt != fieldMap.end()) { const TagLib::StringList &albumGainList = albumGainIt->second; if (!albumGainList.isEmpty()) { tag_settings->replaygainAlbum = parseDecibelValue(albumGainList.front()); } } } } std::string filename(input_file); std::string extension = filename.substr(filename.find_last_of('.') + 1); bool coverArtExtracted = false; if (extension == "mp3") { coverArtExtracted = extractCoverArtFromMp3(input_file, coverFilePath); } else if (extension == "flac") { coverArtExtracted = extractCoverArtFromFlac(input_file, coverFilePath); } else if (extension == "m4a" || extension == "aac") { coverArtExtracted = extractCoverArtFromMp4(input_file, coverFilePath); } if (extension == "opus") { coverArtExtracted = extractCoverArtFromOpus(input_file, coverFilePath); } else if (extension == "ogg") { coverArtExtracted = extractCoverArtFromOggVideo(input_file, coverFilePath); if (!coverArtExtracted) { coverArtExtracted = extractCoverArtFromOgg(input_file, coverFilePath); } } else if (extension == "wav") { coverArtExtracted = extractCoverArtFromWav(input_file, coverFilePath); } if (coverArtExtracted) { return 0; } else { return -1; } } } kew-3.2.0/src/tagLibWrapper.h000066400000000000000000000014261500206121000157730ustar00rootroot00000000000000// taglib_wrapper.h #ifndef TAGLIB_WRAPPER_H #define TAGLIB_WRAPPER_H #ifdef __cplusplus extern "C" { #endif #include "utils.h" #ifndef TAGSETTINGS_STRUCT #define METADATA_MAX_LENGTH 256 #define TAGSETTINGS_STRUCT typedef struct { char title[METADATA_MAX_LENGTH]; char artist[METADATA_MAX_LENGTH]; char album_artist[METADATA_MAX_LENGTH]; char album[METADATA_MAX_LENGTH]; char date[METADATA_MAX_LENGTH]; double replaygainTrack; double replaygainAlbum; } TagSettings; #endif int extractTags(const char *input_file, TagSettings *tag_settings, double *duration, const char *coverFilePath); #ifdef __cplusplus } #endif #endif // TAGLIB_WRAPPER_H kew-3.2.0/src/term.c000066400000000000000000000122001500206121000141620ustar00rootroot00000000000000#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. */ 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() { // Print ANSI escape codes to reset terminal, clear screen, and move cursor to top-left printf("\033\143"); // Reset to Initial State (RIS) printf("\033[3J"); // Clear scrollback buffer printf("\033[H\033[J"); // Move cursor to top-left and clear screen fflush(stdout); } void clearRestOfScreen() { printf("\033[J"); } void clearScreen() { printf("\033[3J"); // Clear scrollback buffer printf("\033[2J\033[3J\033[H"); // Move cursor to top-left and clear screen and scrollback buffer } void enableScrolling() { printf("\033[?7h"); } void disableInputBuffering(void) { struct termios term; tcgetattr(STDIN_FILENO, &term); term.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); } void enableInputBuffering() { struct termios term; tcgetattr(STDIN_FILENO, &term); term.c_lflag |= ICANON | ECHO; tcsetattr(STDIN_FILENO, TCSAFLUSH, &term); } void cursorJump(int numRows) { printf("\033[%dA", numRows); printf("\033[0m"); } void cursorJumpDown(int numRows) { printf("\033[%dB", numRows); } int readInputSequence(char *seq, size_t seqSize) { char c; ssize_t bytesRead; seq[0] = '\0'; bytesRead = read(STDIN_FILENO, &c, 1); if (bytesRead <= 0) { return 0; } // If it's 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; } void enterAlternateScreenBuffer() { // Enter alternate screen buffer printf("\033[?1049h"); } void exitAlternateScreenBuffer() { // Exit alternate screen buffer printf("\033[?1049l"); } void enableTerminalMouseButtons() { // Enable program to accept mouse input as codes printf("\033[?1002h"); } void disableTerminalMouseButtons() { // Disable program to accept mouse input as codes printf("\033[?1002l"); } kew-3.2.0/src/term.h000066400000000000000000000023731500206121000142010ustar00rootroot00000000000000#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 #include "utils.h" #ifdef __GNU__ # define _BSD_SOURCE #endif 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 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); void enterAlternateScreenBuffer(void); void exitAlternateScreenBuffer(void); void enableTerminalMouseButtons(void); void disableTerminalMouseButtons(void); #endif kew-3.2.0/src/utils.c000066400000000000000000000242441500206121000143660ustar00rootroot00000000000000#include "utils.h" /* utils.c Utility functions for instance for replacing some standard functions with safer alternatives. */ #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) #include // For arc4random #include // For uint32_t uint32_t arc4random_uniform(uint32_t upper_bound); int getRandomNumber(int min, int max) { return min + arc4random_uniform(max - min + 1); } #else #include // For getrandom int getRandomNumber(int min, int max) { unsigned int random_value; if (getrandom(&random_value, sizeof(random_value), 0) != sizeof(random_value)) { return min; } return min + (random_value % (max - min + 1)); } #endif 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, const char *src, size_t dest_size) { if (dest && dest_size > 0 && src) { size_t src_length = strnlen(src, dest_size - 1); memcpy(dest, src, src_length); dest[src_length] = '\0'; } } char *stringToLower(const char *str) { return g_utf8_strdown(str, -1); } char *stringToUpper(const char *str) { return g_utf8_strup(str, -1); } char *c_strcasestr(const char *haystack, const char *needle, int maxScanLen) { if (!haystack || !needle) return NULL; size_t needleLen = strnlen(needle, maxScanLen); size_t haystackLen = strnlen(haystack, maxScanLen); if (needleLen > haystackLen) return NULL; for (size_t i = 0; i <= haystackLen - needleLen; i++) { if (strncasecmp(&haystack[i], needle, needleLen) == 0) { return (char *)(haystack + i); } } return NULL; } int match_regex(const 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 ext_size, char *ext) { size_t length = strnlen(filename, MAXPATHLEN); // Find the last '.' character in the filename const char *dot = NULL; for (size_t i = 0; i < length; i++) { if (filename[i] == '.') { dot = &filename[i]; } } // If no dot was found, there's no extension if (!dot || dot == filename + length - 1) { ext[0] = '\0'; // No extension found return; } size_t i = 0, j = 0; while (dot[i + 1] != '\0' && j < ext_size - 1) { unsigned char c = dot[i + 1]; // Determine the number of bytes for the current UTF-8 character size_t charSize; if (c < 0x80) { charSize = 1; // 1-byte character (ASCII) } else if ((c & 0xE0) == 0xC0) { charSize = 2; // 2-byte character } else if ((c & 0xF0) == 0xE0) { charSize = 3; // 3-byte character } else if ((c & 0xF8) == 0xF0) { charSize = 4; // 4-byte character } else { // Invalid UTF-8 byte sequence, stop copying break; } // Make sure we don't copy past the buffer if (j + charSize >= ext_size) { break; } // Copy the UTF-8 character memcpy(ext + j, dot + 1 + i, charSize); j += charSize; i += charSize; } // Null-terminate the result ext[j] = '\0'; } int pathEndsWith(const char *str, const char *suffix) { size_t length = strnlen(str, MAXPATHLEN); size_t suffixLength = strnlen(suffix, MAXPATHLEN); if (suffixLength > length) { return 0; } const char *strSuffix = str + (length - suffixLength); return strcmp(strSuffix, suffix) == 0; } int pathStartsWith(const char *str, const char *prefix) { size_t length = strnlen(str, MAXPATHLEN); size_t prefixLength = strnlen(prefix, MAXPATHLEN); if (prefixLength > length) { return 0; } return strncmp(str, prefix, prefixLength) == 0; } void trim(char *str, int maxLen) { char *start = str; while (*start && isspace(*start)) { start++; } char *end = str + strnlen(str, maxLen) - 1; while (end > start && isspace(*end)) { end--; } *(end + 1) = '\0'; if (str != start) { memmove(str, start, end - start + 2); } } const char *getHomePath(void) { struct passwd *pw = getpwuid(getuid()); if (pw && pw->pw_dir) { return pw->pw_dir; } return NULL; } char *getConfigPath(void) { 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) { #ifdef __APPLE__ snprintf(configPath, MAXPATHLEN, "%s/Library/Preferences/kew", home); #else snprintf(configPath, MAXPATHLEN, "%s/.config/kew", home); #endif } else { struct passwd *pw = getpwuid(getuid()); if (pw) { #ifdef __APPLE__ snprintf(configPath, MAXPATHLEN, "%s/Library/Preferences/kew", pw->pw_dir); #else snprintf(configPath, MAXPATHLEN, "%s/.config/kew", pw->pw_dir); #endif } else { free(configPath); return NULL; } } } return configPath; } char *getFilePath(const char *filename) { if (filename == NULL) { return NULL; } char *configdir = getConfigPath(); if (configdir == NULL) { return NULL; } size_t configdir_length = strnlen(configdir, MAXPATHLEN); size_t filename_length = strnlen(filename, MAXPATHLEN); size_t filepath_length = configdir_length + 1 + filename_length + 1; if (filepath_length > MAXPATHLEN) { free(configdir); return NULL; } char *filepath = (char *)malloc(filepath_length); if (filepath == NULL) { free(configdir); return NULL; } snprintf(filepath, filepath_length, "%s/%s", configdir, filename); free(configdir); return filepath; } void removeUnneededChars(char *str, int length) { // Do not remove characters if filename only contains digits bool stringContainsLetters = false; for (int i = 0; str[i] != '\0'; i++) { if (!isdigit(str[i])) { stringContainsLetters = true; } } if (!stringContainsLetters) { return; } for (int i = 0; i < 3 && str[i] != '\0' && str[i] != ' '; i++) { if (isdigit(str[i]) || 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 length--; } } // Remove hyphens and underscores from filename for (int i = 0; str[i] != '\0'; i++) { // Only remove if there are no spaces around if ((str[i] == '-' || str[i] == '_') && (i > 0 && i < length && str[i - 1] != ' ' && str[i + 1] != ' ')) { str[i] = ' '; } } } void shortenString(char *str, size_t maxLength) { size_t length = strnlen(str, maxLength + 2); if (length > maxLength) { str[maxLength] = '\0'; } } void printBlankSpaces(int numSpaces) { if (numSpaces < 1) return; printf("%*s", numSpaces, " "); } int getNumber(const char *str) { char *endptr; long value = strtol(str, &endptr, 10); if (value < INT_MIN || value > INT_MAX) { return 0; } return (int)value; } float getFloat(const char *str) { char *endptr; float value = strtof(str, &endptr); if (str == endptr) { return 0.0f; } if (isnan(value) || isinf(value) || value < -FLT_MAX || value > FLT_MAX) { return 0.0f; } return value; } kew-3.2.0/src/utils.h000066400000000000000000000027011500206121000143650ustar00rootroot00000000000000#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 #include #include #include #include #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif int getRandomNumber(int min, int max); void c_sleep(int milliseconds); void c_usleep(int microseconds); void c_strcpy(char *dest, const char *src, size_t dest_size); char *stringToUpper(const char *str); char *stringToLower(const char *str); char *utf8_strstr(const char *haystack, const char *needle); char *c_strcasestr(const char *haystack, const char *needle, int maxScanLen); int match_regex(const regex_t *regex, const char *ext); void extractExtension(const char *filename, size_t numChars, char *ext); int pathEndsWith(const char *str, const char *suffix); int pathStartsWith(const char *str, const char *prefix); void trim(char *str, int maxLen); const char *getHomePath(void); char *getConfigPath(void); void removeUnneededChars(char *str, int length); void shortenString(char *str, size_t maxLength); void printBlankSpaces(int numSpaces); int getNumber(const char *str); char *getFilePath(const char *filename); float getFloat(const char *str); #endif kew-3.2.0/src/visuals.c000066400000000000000000000466051500206121000147210ustar00rootroot00000000000000#include "visuals.h" /* visuals.c This file should contain only functions related to the spectrum visualizer. */ #ifndef M_PI #define M_PI 3.14159265358979323846 #endif #define MAX_BARS 64 int prevBufferSize = 0; float alpha = 0.02f; float smoothFramesAlpha = 0.8f; float lastMax = -1.0f; float *fftInput = NULL; float *fftPreviousInput = NULL; fftwf_complex *fftOutput = NULL; int bufferIndex = 0; ma_format format = ma_format_unknown; ma_uint32 sampleRate = 44100; float magnitudeBuffer[MAX_BARS] = {0.0f}; float lastMagnitudes[MAX_BARS] = {0.0f}; float smoothedMagnitudes[MAX_BARS] = {0.0f}; float smoothedFramesMagnitudes[MAX_BARS] = {0.0f}; float minMaxMagnitude = 10.0f; float enhancePeak = 2.6f; float exponent = 0.85f; // Lower than 1.0 makes quiet sounds more visible float baseDecay = 0.85f; float rangeDecay = 0.2f; float baseAttack = 0.35f; float rangeAttack = 0.4f; float maxMagnitude = 0.0f; float tweenFactor = 0.23f; float tweenFactorFall = 0.13f; #define MOVING_AVERAGE_WINDOW_SIZE 2 void smoothMovingAverageMagnitudes(int numBars, float *magnitudes) { // Apply moving average smoothing for (int i = 0; i < numBars; 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 < numBars) { sum += magnitudes[i + j]; count++; } } smoothedMagnitudes[i] = sum / count; } memcpy(magnitudes, smoothedMagnitudes, numBars * sizeof(float)); } float applyAttackandDecay(float linearVal, float lastMagnitude, float maxMagnitude) { float ratio = linearVal / maxMagnitude; // 0..1 if (ratio > 1.0f) ratio = 1.0f; // Clamp float decreaseFactor = baseDecay + rangeDecay * ratio; float decayedMagnitude = linearVal * decreaseFactor; float attackFactor = baseAttack + rangeAttack * ratio; float newVal; if (linearVal < decayedMagnitude) { // Decay newVal = decayedMagnitude; } else { // Attack newVal = lastMagnitude + attackFactor * (linearVal - lastMagnitude); } return newVal; } void updateMagnitudes(int height, int numBars, float maxMagnitude, float *magnitudes) { smoothMovingAverageMagnitudes(numBars, magnitudes); for (int i = 0; i < numBars; i++) { float newVal = applyAttackandDecay(magnitudes[i], lastMagnitudes[i], maxMagnitude); lastMagnitudes[i] = newVal; // Normalize float displayRatio = newVal / maxMagnitude; if (displayRatio > 1.0f) displayRatio = 1.0f; magnitudes[i] = powf(displayRatio, exponent) * height; } } float calcMaxMagnitude(int numBars, float *magnitudes) { maxMagnitude = 0.0f; for (int i = 0; i < numBars; i++) { if (magnitudes[i] > maxMagnitude) { maxMagnitude = magnitudes[i]; } } if (maxMagnitude < minMaxMagnitude) maxMagnitude = minMaxMagnitude; if (lastMax < 0.0f) { lastMax = maxMagnitude; return maxMagnitude; } // Apply exponential smoothing lastMax = (1 - alpha) * lastMax + alpha * maxMagnitude; return lastMax; } void clearMagnitudes(int numBars, float *magnitudes) { for (int i = 0; i < numBars; i++) { magnitudes[i] = 0.0f; } } void enhancePeaks(int numBars, float *magnitudes, int height, float enhancePeak) { for (int i = 2; i < numBars - 1; i++) // Don't enhance bass sounds as they already dominate too much in most music { if (magnitudes[i] > magnitudes[i - 1] && magnitudes[i] > magnitudes[i + 1]) { magnitudes[i] *= enhancePeak; magnitudes[i] = fminf(magnitudes[i], (float)height); } } } void applyBlackmanHarris(float *fftInput, int bufferSize) { float alpha0 = 0.35875f; float alpha1 = 0.48829f; float alpha2 = 0.14128f; float alpha3 = 0.01168f; for (int i = 0; i < bufferSize; i++) { float fraction = (float)i / (float)(bufferSize - 1); // i / (N-1) float window = alpha0 - alpha1 * cosf(2.0f * M_PI * fraction) + alpha2 * cosf(4.0f * M_PI * fraction) - alpha3 * cosf(6.0f * M_PI * fraction); fftInput[i] *= window; } } void smoothFrames( float *magnitudes, float *smoothedFramesMagnitudes, int numBars, float tweenFactor, float tweenFactorFall) { for (int i = 0; i < numBars; i++) { float currentVal = smoothedFramesMagnitudes[i]; float targetVal = magnitudes[i]; float delta = targetVal - currentVal; if (delta > 0) smoothedFramesMagnitudes[i] += delta * tweenFactor; else smoothedFramesMagnitudes[i] += delta * tweenFactorFall; } } // Average All Bins in Bar Range void fillSpectrumBars( const fftwf_complex *fftOutput, int bufferSize, float sampleRate, float *magnitudes, int numBars, float minFreq, float maxFreq) { float *barFreqLo = (float *)malloc(sizeof(float) * numBars); float *barFreqHi = (float *)malloc(sizeof(float) * numBars); float logMin = log10f(minFreq); float logMax = log10f(maxFreq); float spacingPower = 0.8f; int numBins = bufferSize / 2 + 1; float binSpacing = sampleRate / bufferSize; for (int i = 0; i < numBars; i++) { float tLo = (float)i / numBars; float tHi = (float)(i + 1) / numBars; float skewLo = powf(tLo, spacingPower); float skewHi = powf(tHi, spacingPower); barFreqLo[i] = powf(10.0f, logMin + (logMax - logMin) * skewLo); barFreqHi[i] = powf(10.0f, logMin + (logMax - logMin) * skewHi); } for (int i = 0; i < numBars; i++) { // Find bins covered by this bar range int binLo = (int)ceilf(barFreqLo[i] / binSpacing); int binHi = (int)floorf(barFreqHi[i] / binSpacing); // Clamp to valid bins if (binLo < 0) binLo = 0; if (binHi >= numBins) binHi = numBins - 1; // Special case: if range selects no bins, pick the closest one if (binHi < binLo) binHi = binLo; float sum = 0.0f; int count = 0; for (int k = binLo; k <= binHi; k++) { float real = fftOutput[k][0]; float imag = fftOutput[k][1]; sum += sqrtf(real * real + imag * imag); count++; } magnitudes[i] = (count > 0) ? sum / count : 0.0f; } free(barFreqLo); free(barFreqHi); } void calc(int height, int numBars, ma_int32 *audioBuffer, int bitDepth, float *fftInput, fftwf_complex *fftOutput, float *magnitudes, fftwf_plan plan) { static float localBuffer[MAX_BUFFER_SIZE]; if (!bufferReady) return; memcpy(localBuffer, audioBuffer, sizeof(float) * MAX_BUFFER_SIZE); bufferReady = false; if (audioBuffer == NULL) { fprintf(stderr, "Audio buffer is NULL.\n"); return; } for (int i = 0; i < FFT_SIZE; i++) { ma_int32 sample = audioBuffer[i]; float normalizedSample = 0.0f; switch (bitDepth) { case 8: normalizedSample = ((float)sample - 128.0f) / 127.0f; break; case 16: normalizedSample = (float)sample / 32768.0f; break; case 24: { int32_t lower24Bits = sample & 0xFFFFFF; if (lower24Bits & 0x800000) { lower24Bits |= 0xFF000000; // Sign extension } normalizedSample = (float)lower24Bits / 8388608.0f; break; } case 32: // Assuming 32-bit integers normalizedSample = (float)sample / 2147483648.0f; break; default: fprintf(stderr, "Unsupported bit depth: %d\n", bitDepth); return; } fftInput[i] = normalizedSample; } applyBlackmanHarris(fftInput, FFT_SIZE); fftwf_execute(plan); clearMagnitudes(numBars, magnitudes); fillSpectrumBars(fftOutput, FFT_SIZE, sampleRate, magnitudes, numBars, 20.0f, (float)(sampleRate / 2 > 48000 ? 48000 : sampleRate / 2)); float maxMagnitude = calcMaxMagnitude(numBars, magnitudes); updateMagnitudes(height, numBars, maxMagnitude, magnitudes); enhancePeaks(numBars, magnitudes, height, enhancePeak); smoothFrames(magnitudes, smoothedFramesMagnitudes, numBars, tweenFactor, tweenFactorFall); } char *upwardMotionCharsBlock[] = { " ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"}; char *upwardMotionCharsBraille[] = { " ", "⣀", "⣀", "⣤", "⣤", "⣶", "⣶", "⣿", "⣿"}; char *inbetweenCharsRising[] = { " ", "⣠", "⣠", "⣴", "⣴", "⣾", "⣾", "⣿", "⣿"}; char *inbetweenCharsFalling[] = { " ", "⡀", "⡀", "⣄", "⣄", "⣦", "⣦", "⣷", "⣷"}; char *getUpwardMotionChar(int level, bool braille) { if (level < 0 || level > 8) { level = 8; } if (braille) return upwardMotionCharsBraille[level]; else return upwardMotionCharsBlock[level]; } char *getInbetweendMotionChar(float magnitudePrev, float magnitudeNext, int prev, int next) { if (prev < 0) prev = 0; if (prev > 8) prev = 8; if (next < 0) next = 0; if (next > 8) next = 8; if (magnitudeNext > magnitudePrev) return inbetweenCharsRising[prev]; else if (magnitudeNext < magnitudePrev) return inbetweenCharsFalling[prev]; else return upwardMotionCharsBraille[prev]; } char *getInbetweenChar(float prev, float next) { int firstDecimalDigit = (int)(fmod(prev * 10, 10)); int secondDecimalDigit = (int)(fmod(next * 10, 10)); return getInbetweendMotionChar(prev, next, firstDecimalDigit, secondDecimalDigit); } int calcSpectrum(int height, int numBars, float *fftInput, fftwf_complex *fftOutput, float *magnitudes, fftwf_plan plan) { ma_int32 *g_audioBuffer = getAudioBuffer(); getCurrentFormatAndSampleRate(&format, &sampleRate); 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 numBars, float *magnitudes, PixelData color, int indentation, bool useConfigColors, int visualizerColorType, int brailleMode) { printf("\n"); PixelData tmp; for (int j = height; j > 0; j--) { printf("\r"); printBlankSpaces(indentation); if (color.r != 0 || color.g != 0 || color.b != 0) { if (!useConfigColors && (visualizerColorType == 0 || visualizerColorType == 2)) { if (visualizerColorType == 0) { tmp = increaseLuminosity(color, round(j * height * 4)); } else if (visualizerColorType == 2) { tmp = increaseLuminosity(color, round((height - j) * height * 4)); } printf("\033[38;2;%d;%d;%dm", tmp.r, tmp.g, tmp.b); } } else { setDefaultTextColor(); } if (isPaused() || isStopped()) { for (int i = 0; i < numBars; i++) { printf(" "); } printf("\n "); continue; } for (int i = 0; i < numBars; i++) { if (!useConfigColors && visualizerColorType == 1) { tmp = (PixelData){color.r / 2, color.g / 2, color.b / 2}; // Make colors half as bright before increasing brightness tmp = increaseLuminosity(tmp, round(magnitudes[i] * 10 * 4)); printf("\033[38;2;%d;%d;%dm", tmp.r, tmp.g, tmp.b); } if (i == 0 && brailleMode) { printf(" "); } else if (i > 0 && brailleMode) { if (magnitudes[i - 1] >= j) { printf("%s", getUpwardMotionChar(10, brailleMode)); } else if (magnitudes[i - 1] + 1 >= j) { printf("%s", getInbetweenChar(magnitudes[i - 1], magnitudes[i])); } else { printf(" "); } } if (!brailleMode) { printf(" "); } if (magnitudes[i] >= j) { printf("%s", getUpwardMotionChar(10, brailleMode)); } else if (magnitudes[i] + 1 >= j) { int firstDecimalDigit = (int)(fmod(magnitudes[i] * 10, 10)); printf("%s", getUpwardMotionChar(firstDecimalDigit, brailleMode)); } else { printf(" "); } } printf("\n "); } printf("\r"); fflush(stdout); } void freeVisuals(void) { if (fftInput != NULL) { free(fftInput); fftInput = NULL; } if (fftPreviousInput != NULL) { free(fftPreviousInput); fftPreviousInput = NULL; } if (fftOutput != NULL) { fftwf_free(fftOutput); fftOutput = NULL; } } void drawSpectrumVisualizer(AppState *state, int indentation) { int height = state->uiSettings.visualizerHeight; PixelData color; color.r = state->uiSettings.color.r; color.g = state->uiSettings.color.g; color.b = state->uiSettings.color.b; int numBars = state->uiState.numProgressBars; bool useConfigColors = state->uiSettings.useConfigColors; int visualizerColorType = state->uiSettings.visualizerColorType; bool brailleMode = state->uiSettings.visualizerBrailleMode; tweenFactor = state->uiSettings.tweenFactor; tweenFactorFall = state->uiSettings.tweenFactorFall; height = height - 1; if (height <= 0 || numBars <= 0) { return; } if (numBars > MAX_BARS) numBars = MAX_BARS; if (FFT_SIZE != prevBufferSize) { lastMax = -1.0f; freeVisuals(); memset(smoothedFramesMagnitudes, 0, sizeof(smoothedFramesMagnitudes)); fftInput = (float *)malloc(sizeof(float) * FFT_SIZE); if (fftInput == NULL) { for (int i = 0; i <= height; i++) { printf("\n"); } return; } fftPreviousInput = (float *)malloc(sizeof(float) * FFT_SIZE); if (fftPreviousInput == NULL) { fftwf_free(fftInput); fftInput = NULL; for (int i = 0; i <= height; i++) { printf("\n"); } return; } for (int i = 0; i < FFT_SIZE; i++) { fftPreviousInput[i] = 0.0f; } fftOutput = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * FFT_SIZE); if (fftOutput == NULL) { fftwf_free(fftInput); fftwf_free(fftPreviousInput); fftInput = NULL; fftPreviousInput = NULL; for (int i = 0; i <= height; i++) { printf("\n"); } return; } prevBufferSize = FFT_SIZE; } fftwf_plan plan = fftwf_plan_dft_r2c_1d(FFT_SIZE, fftInput, fftOutput, FFTW_ESTIMATE); float magnitudes[numBars]; calcSpectrum(height, numBars, fftInput, fftOutput, magnitudes, plan); printSpectrum(height, numBars, smoothedFramesMagnitudes, color, indentation, useConfigColors, visualizerColorType, brailleMode); fftwf_destroy_plan(plan); } kew-3.2.0/src/visuals.h000066400000000000000000000010131500206121000147060ustar00rootroot00000000000000 #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); void freeVisuals(void); void drawSpectrumVisualizer(AppState *state, int indentation); PixelData increaseLuminosity(PixelData pixel, int amount);