kew/000077500000000000000000000000001507107350600116505ustar00rootroot00000000000000kew/.clang-format000066400000000000000000000001261507107350600142220ustar00rootroot00000000000000BasedOnStyle: LLVM IndentWidth: 8 TabWidth: 8 UseTab: Never BreakBeforeBraces: Allman kew/.clangd000066400000000000000000000001111507107350600130720ustar00rootroot00000000000000Diagnostics: ClangTidy: Remove: bugprone-unchecked-optional-access kew/.editorconfig000066400000000000000000000003441507107350600143260ustar00rootroot00000000000000# 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/.github/000077500000000000000000000000001507107350600132105ustar00rootroot00000000000000kew/.github/FUNDING.yml000066400000000000000000000000411507107350600150200ustar00rootroot00000000000000ko_fi: ravachol github: ravachol kew/.github/ISSUE_TEMPLATE/000077500000000000000000000000001507107350600153735ustar00rootroot00000000000000kew/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000005741507107350600173710ustar00rootroot00000000000000blank_issues_enabled: true contact_links: - name: 💬 Support or Questions url: https://chatgpt.com/ about: | For support or general questions (like installation help, etc.), please consider reading the readme, or asking ChatGPT or Mistral.ai **before** opening an issue here. This issue tracker is for **bug reports and feature requests primarily**. kew/.github/workflows/000077500000000000000000000000001507107350600152455ustar00rootroot00000000000000kew/.github/workflows/appimage_alpine.yml000066400000000000000000000051761507107350600211140ustar00rootroot00000000000000name: 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/.github/workflows/arm_macos.yml000066400000000000000000000013761507107350600177400ustar00rootroot00000000000000name: 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/.github/workflows/ci.yml000066400000000000000000000011641507107350600163650ustar00rootroot00000000000000name: 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/.github/workflows/claude_security_review.yml000066400000000000000000000172631507107350600225460ustar00rootroot00000000000000name: Full Codebase Security Scan on: workflow_dispatch: inputs: max_files: description: 'Maximum number of files to scan (cost control)' required: false default: '50' extensions: description: 'File extensions (comma-separated)' required: false default: 'c,cpp,cc,cxx,h,hpp' jobs: security-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install Claude SDK run: pip install anthropic - name: Run Security Analysis env: ANTHROPIC_API_KEY: ${{ secrets.CLAUDE_API_KEY }} MAX_FILES: ${{ github.event.inputs.max_files || '50' }} EXTENSIONS: ${{ github.event.inputs.extensions || 'c,cpp,cc,cxx,h,hpp' }} run: | python3 << 'EOF' import anthropic import os import glob import json from datetime import datetime def get_code_files(extensions, max_files): """Find code files to analyze""" code_files = [] exclude_dirs = {'.git', 'node_modules', '__pycache__', 'venv', 'build', 'dist', 'target'} for ext in extensions.split(','): ext = ext.strip() files = glob.glob(f"**/*.{ext}", recursive=True) # Filter out excluded directories for file in files: if not any(excluded in file for excluded in exclude_dirs): code_files.append(file) # Remove duplicates, sort, and limit code_files = sorted(list(set(code_files)))[:int(max_files)] return code_files def analyze_file(client, file_path): """Analyze a single file for security issues""" try: # Read file with size limit for cost control with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read()[:6000] # Limit to ~6k chars per file prompt = f"""Analyze this {file_path} file for security vulnerabilities in a C/C++ Linux terminal music player application. CONTEXT: This is a terminal music player with NO database, web access, or network functionality. Focus on vulnerabilities specific to: CRITICAL AUDIO FILE PARSING VULNERABILITIES: 1. Buffer overflows when parsing music file headers (MP3, FLAC, OGG, WAV, etc.) 2. Integer overflows in size calculations for audio data 3. Heap corruption from malformed audio metadata 4. Stack buffer overflows in filename/path handling 5. Format string vulnerabilities in audio tag parsing 6. Unsafe memory operations when reading variable-length audio data GENERAL C/C++ SECURITY ISSUES: 7. Buffer overflows (strcpy, strcat, gets, scanf, etc.) 8. Use-after-free and double-free vulnerabilities 9. Null pointer dereferences 10. Integer overflows in memory allocations 11. Unsafe pointer arithmetic 12. Missing bounds checking on arrays 13. Race conditions in file I/O 14. Path traversal in file operations 15. Memory leaks that could lead to DoS AUDIO-SPECIFIC ATTACK VECTORS: 16. Malicious embedded album art (large images causing memory exhaustion) 17. Crafted playlists with overly long file paths 18. Audio files with corrupted or oversized metadata tags 19. Files with misleading file extensions vs actual format 20. Symbolic link attacks in music directory traversal Rate severity as: CRITICAL, HIGH, MEDIUM, LOW Pay special attention to any code that processes untrusted audio file data. Code: ``` {content} ``` Respond with findings in this format: SEVERITY: Description (function name) - AUDIO_PARSING/BUFFER/MEMORY/etc """ response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1500, messages=[{"role": "user", "content": prompt}] ) return response.content[0].text except Exception as e: return f"Error analyzing {file_path}: {str(e)}" def main(): # Setup api_key = os.environ.get('ANTHROPIC_API_KEY') if not api_key: print("⌠ERROR: ANTHROPIC_API_KEY not found") return max_files = os.environ.get('MAX_FILES', '50') extensions = os.environ.get('EXTENSIONS', 'c,cpp,h') print(f"🔠Security Scan Starting") print(f"📠Max files: {max_files}") print(f"📄 Extensions: {extensions}") print("=" * 60) client = anthropic.Anthropic(api_key=api_key) # Find files code_files = get_code_files(extensions, max_files) print(f"Found {len(code_files)} files to analyze\n") # Analyze files total_issues = 0 critical_issues = 0 for i, file_path in enumerate(code_files, 1): print(f"[{i:2d}/{len(code_files)}] 🔠{file_path}") analysis = analyze_file(client, file_path) # Count issues if "CRITICAL:" in analysis: critical_issues += analysis.count("CRITICAL:") total_issues += analysis.count("CRITICAL:") if "HIGH:" in analysis: total_issues += analysis.count("HIGH:") if "MEDIUM:" in analysis: total_issues += analysis.count("MEDIUM:") if "LOW:" in analysis: total_issues += analysis.count("LOW:") # Print results if any(severity in analysis for severity in ["CRITICAL:", "HIGH:", "MEDIUM:", "LOW:"]): print(f" âš ï¸ Issues found:") for line in analysis.split('\n'): if any(severity in line for severity in ["CRITICAL:", "HIGH:", "MEDIUM:", "LOW:"]): print(f" {line}") print() else: print(" ✅ No issues found") print("-" * 50) # Summary print(f"\n🎯 SECURITY SCAN COMPLETE") print(f"📊 Files analyzed: {len(code_files)}") print(f"âš ï¸ Total issues: {total_issues}") print(f"🚨 Critical issues: {critical_issues}") print(f"💰 Estimated cost: ~${len(code_files) * 0.15:.2f}") if critical_issues > 0: print("\n🚨 CRITICAL ISSUES FOUND - Review immediately!") elif total_issues > 0: print(f"\nâš ï¸ {total_issues} security issues found - Review recommended") else: print("\n✅ No security issues detected") if __name__ == "__main__": main() EOF - name: Upload Results Summary if: always() run: | echo "Security scan completed at $(date)" > scan-summary.txt echo "Check the logs above for detailed findings" >> scan-summary.txt - name: Upload Artifact uses: actions/upload-artifact@v4 if: always() with: name: security-scan-summary path: scan-summary.txt kew/.gitignore000066400000000000000000000005411507107350600136400ustar00rootroot00000000000000# 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 tags *.idx compile_commands.json debug.log kew/LICENSE000066400000000000000000000432541507107350600126650ustar00rootroot00000000000000 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/Makefile000066400000000000000000000166501507107350600133200ustar00rootroot00000000000000CC ?= 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/local 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) # Check if we're in Termux environment ifneq ($(wildcard /data/data/com.termux/files/usr),) # Termux environment - check common installation paths USE_FAAD = $(shell [ -f "$(PREFIX)/lib/libfaad.so" ] || \ [ -f "$(PREFIX)/lib/libfaad2.so" ] || \ [ -f "$(PREFIX)/local/lib/libfaad.so" ] || \ [ -f "$(PREFIX)/local/lib/libfaad2.so" ] || \ [ -f "/data/data/com.termux/files/usr/lib/libfaad.so" ] || \ [ -f "/data/data/com.termux/files/usr/bin/faad" ] || \ [ -f "/data/data/com.termux/files/usr/lib/libfaad2.so" ] || \ [ -f "/data/data/com.termux/files/usr/local/lib/libfaad.so" ] || \ [ -f "/data/data/com.termux/files/usr/local/lib/libfaad2.so" ] && echo 1 || echo 0) else # Non-Android build - try pkg-config first 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/lib64/libfaad.so ] || [ -f /usr/lib64/libfaad2.so ] || \ [ -f /usr/bin/faad ] || [ -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 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/lib64/chafa/include -I/usr/include/ogg -I/usr/include/opus \ -I/usr/include/stb -Iinclude/stb_image -I/usr/include/glib-2.0 \ -I/usr/lib/glib-2.0/include -I/usr/lib64/glib-2.0/include -Iinclude/miniaudio -Iinclude -Iinclude/nestegg -I/usr/include/gdk-pixbuf-2.0 ifeq ($(DEBUG), 1) COMMONFLAGS += -g -DDEBUG 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 += -DMA_NO_AAUDIO COMMONFLAGS += -fstack-protector-strong -Wformat -Werror=format-security -fPIE -D_FORTIFY_SOURCE=2 COMMONFLAGS += -Wall -Wextra -Wpointer-arith # Check if we're in Termux environment ifneq ($(wildcard /data/data/com.termux/files/usr),) # Termux environment COMMONFLAGS += -D__ANDROID__ endif CFLAGS = $(COMMONFLAGS) # Compiler flags for C++ code CXXFLAGS = $(COMMONFLAGS) -std=c++11 # Libraries LIBS = -L/usr/lib -lm -lopusfile -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 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 else ifeq ($(UNAME_S), Darwin) LIBS += -framework CoreAudio -framework CoreFoundation endif # Conditionally add USE_DBUS is enabled ifeq ($(USE_DBUS), 1) DEFINES += -DUSE_DBUS endif DEFINES += -DPREFIX=\"$(PREFIX)\" # 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 ifeq ($(UNAME_O),Android) CFLAGS += -I$(PREFIX)/include LIBS += -L$(PREFIX)/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/theme.c src/sound.c src/directorytree.c src/notifications.c \ src/soundcommon.c src/m4a.c src/search_ui.c src/playlist_ui.c \ src/player_ui.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 DATADIR ?= $(PREFIX)/share THEMEDIR = $(DATADIR)/kew/themes all: kew # Generate object lists OBJS_C = $(SRCS:src/%.c=$(OBJDIR)/%.o) NESTEGG_SRCS = include/nestegg/nestegg.c NESTEGG_OBJS = $(NESTEGG_SRCS:include/nestegg/%.c=$(OBJDIR)/nestegg/%.o) # All objects together OBJS = $(OBJS_C) $(NESTEGG_OBJS) # Create object directories $(OBJDIR): mkdir -p $(OBJDIR) ## Compile C sources $(OBJDIR)/%.o: src/%.c Makefile | $(OBJDIR) @mkdir -p $(dir $@) $(CC) $(CFLAGS) $(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 $@ # Compile C files in include/nestegg $(OBJDIR)/nestegg/%.o: include/nestegg/%.c Makefile | $(OBJDIR) @mkdir -p $(dir $@) $(CC) $(CFLAGS) $(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 mkdir -p $(DESTDIR)$(THEMEDIR) install -m 0755 kew $(DESTDIR)$(PREFIX)/bin/kew install -m 0644 docs/kew.1 $(DESTDIR)$(MAN_DIR)/man1/kew.1 @if [ -d themes ]; then \ for theme in themes/*.theme; do \ if [ -f "$$theme" ]; then \ install -m 0644 "$$theme" $(DESTDIR)$(THEMEDIR)/; \ fi; \ done; \ for theme in themes/*.txt; do \ if [ -f "$$theme" ]; then \ install -m 0644 "$$theme" $(DESTDIR)$(THEMEDIR)/; \ fi; \ done; \ fi .PHONY: uninstall uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/kew rm -f $(DESTDIR)$(MAN_DIR)/man1/kew.1 rm -rf $(DESTDIR)$(THEMEDIR) .PHONY: clean clean: rm -rf $(OBJDIR) kew kew/README.md000066400000000000000000000151031507107350600131270ustar00rootroot00000000000000
kew Logo

Screenshot


[![License](https://img.shields.io/github/license/ravachol/kew?color=333333&style=for-the-badge)](./LICENSE)
kew (/kjuË/) is a terminal music player. ## Features * Search a music library with partial titles from the command-line. * Creates a playlist automatically based on matched song, album or artist. * Private, no data is collected by kew. * Full color covers in sixel-capable terminals. * Visualizer with various settings. * Edit the playlist by adding, removing and reordering songs. * Gapless playback. * Explore the library and enqueue files or folders. * Search your music library and add to the queue. * Supports MP3, FLAC, MPEG-4/M4A (AAC), OPUS, OGG, Webm and WAV audio. * Supports desktop events through MPRIS. * Use themes or colors derived from covers. ## Installing Packaging status Install through your package manager or homebrew (macOS). If you can't find it on your distro, or you want the bleeding edge, follow the [Manual Installation Instructions](docs/MANUAL-INSTALL-INSTRUCTIONS.md). ## Usage kew creates a playlist with the contents of the first directory or file whose name matches the arguments you provide in the command-line. ```bash kew cure great ``` This creates and starts playing a playlist with 'The cure greatest hits' if it's in your music library. It works best when your music library is organized this way: artist folder->album folder(s)->track(s).
Example commands ``` 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 theme midnight (sets the 'midnight.theme' theme). 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 favorites.m3u kew path "/home/joe/Musik/" (changes the path) ```
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 (macOS/Android) to show/hide playlist view. * F3 or Shift+x (macOS/Android) to show/hide library view. * F4 or Shift+c (macOS/Android) to show/hide track view. * F5 or Shift+v (macOS/Android) to show/hide search view. * F6 or Shift+b (macOS/Android) to show/hide key bindings view. * u to update the library. * v to toggle the visualizer. * i to cycle colors derived from kewrc, theme or track cover. * t to cycle themes. * b to toggle album covers drawn in ascii or as a normal image. * n to toggle notifications. * 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. * f, g to move songs up or down the playlist. * number + G or Enter to go to specific song number in the playlist. * . to add currently playing song to kew favorites.m3u (run with "kew ."). * Esc to quit.
## Configuration Linux: ~/.config/kew/ macOS: ~/Library/Preferences/kew/ ## Themes Press t to cycle available themes. To set a theme from the command-line, run: ```bash kew theme (ie 'kew theme midnight') ``` Put themes in \~/.config/kew/themes (\~/Library/Preferences/kew/themes on macOS). ## If Colors Look Wrong Cycle i until they look right. Press v to turn off visualizer. Press b for ASCII covers. A terminal emulator that can handle TrueColor and sixels is recommended. See [Sixels in Terminal](https://www.arewesixelyet.com/). ## Favorites Playlist Add current song: press . To load 'kew favorites.m3u': ```bash kew . ``` ## Join The Development Effort Join the development of kew if you can handle writing C with camelCase. We are also looking for beta testers and bug reporters. See [Contributing](docs/CONTRIBUTING.md) and [Developers](docs/DEVELOPERS.md). ## License Licensed under GPL. [See LICENSE for more information](./LICENSE).
Attributions kew makes use of the following great open source projects: Chafa by Hans Petter Jansson - https://hpjansson.org/chafa/ 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 Nestegg by Mozilla - https://github.com/mozilla/nestegg Img_To_Txt by Danny Burrows - https://github.com/danny-burrows/img_to_txt
## Sponsors and Donations Wanted Please support this effort:
https://ko-fi.com/ravachol https://github.com/sponsors/ravachol. ## Contact Comments? Suggestions? Send mail to kew-player@proton.me. kew/docs/000077500000000000000000000000001507107350600126005ustar00rootroot00000000000000kew/docs/ANDROID-INSTRUCTIONS.md000066400000000000000000000036751507107350600161570ustar00rootroot00000000000000### **Basic Installation Requirements :** To run kew on Android please install the following applications : - **Termux** : A terminal emulator for Android that allows you to run Linux commands on your device. [![Download Termux](https://img.shields.io/badge/Download-Termux-brightgreen?style=for-the-badge&logo=android)](https://github.com/termux/termux-app/releases/) - click to go to downloads page - **Termux-Api** : A plugin for Termux that executes Termux-api package commands. [![Download Termux-Api](https://img.shields.io/badge/Download-Termux--API-blue?style=for-the-badge&logo=android)](https://github.com/termux/termux-api/releases/download/v0.53.0/termux-api-app_v0.53.0+github.debug.apk) - click to download ### **Termux Setup:** 1. **Update and install dependencies** ```sh pkg install tur-repo -y && yes | pkg upgrade -y && pkg install clang pkg-config taglib fftw git make chafa glib libopus opusfile libvorbis libogg dbus termux-api ``` 2. **Make sure termux has sound:** On your phone, go to Settings -> Sound and Vibration -> Volume and make sure the level for Media is not 0.
Building Faad2 from source (needed to run .m4a files) ```sh pkg install cmake make clang git clone https://github.com/knik0/faad2 cd faad2 cmake -DCMAKE_EXE_LINKER_FLAGS="-lm" . -D CMAKE_INSTALL_PREFIX=/data/data/com.termux/files/usr make install ```
3. **Enable storage permissions** ```sh termux-setup-storage ``` Tap allow for the setup to finish 4. **Setup dbus for kew** * edit/create `~/.bashrc` ``` nano ~/.bashrc ``` * In nano, add this line and save it (ctrl+x): ```bash alias kew="dbus-launch kew" ``` * Restart the shell: `exec $SHELL ### **Compiling Kew:** ```sh git clone https://codeberg.org/ravachol/kew.git cd kew make -j4 make install ``` ### **Run kew:** 1. **Set kew's music library path** This could be ~/storage/music for instance: ``` kew path ``` 2. **Run kew** ``` kew ``` kew/docs/AUTHORS.md000066400000000000000000000026431507107350600142540ustar00rootroot00000000000000# 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 * Robertson D. Torress @Welpyes * 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/docs/CHANGELOG.md000066400000000000000000001163421507107350600144200ustar00rootroot00000000000000# CHANGELOG ### 3.5.2 @hartalex - Fixed line in cover being erased in landscape mode on some terminals. - Fixed long names no longer scrolling. - Fixed cover in landscape mode jumping from line 1 to line 2 and back when resizing window. ### 3.5.1 - Fix issue/test on homebrew. ### 3.5.0 Now with themes and Android support New in this release: - Fully customizable colors - Themes supporting both TrueColor RGB and the terminal 16-color palette - Theme pack with 16 included themes - Android support - Fixed TTY flickering - Improved search #### Themes Press t to cycle available themes. To set a theme from the command-line, run: kew theme themename (ie 'kew theme midnight') Put themes in \~/.config/kew/themes (\~/Library/Preferences/kew/themes on macOS). #### Android I haven't looked at battery life aspects yet, but staying in library view will be easier on the battery than using track view with the visualizer. You can also press v to toggle the visualizer on or off. #### TTY problems resolved The flickering in TTY has been fixed. Btw, if you are on tty or have limited colors and font on your terminal, try pressing i (for other color modes), v (for visualizer off) and b (for ascii cover). That should make it look much more easy on the eye! #### Move to Codeberg We now have a repo on Codeberg and that will be the preferred repo going forward. But people will be welcome to contribute in whichever place they prefer. Except for PRs, PRs need to go to codeberg, develop branch. #### OpenSuse We are now back on openSuse, our package there hadn't been updated in a long time, due to openSuse not having faad lib. We still need a Fedora package. We already have a RPM spec that @kazeevn added and everything. Thank you to @welpyes for bringing up Termux and helping out with that, and @arcathrax for fixing the ultrawide monitor bug. Thank you to mantarimay for updating the openSuse library. #### Sponsors and Donations Wanted Thank you to a new sponsor, @BasedScience! Please support this effort: https://ko-fi.com/ravachol https://github.com/sponsors/ravachol. - Ravachol #### New Features / Improvements @ravachol Theme colors, both TrueColor and 16-color palette theming. Cycle by pressing 't'. @welpyes, @ravachol Android compatibility! Please see ANDROID-INSTRUCTIONS.md for how to get kew on your phone. @ravachol Improved the search function so that albums are shown below an artist hit. @ravachol Improved installation instructions for Fedora and openSuse in the README. @ravachol Enabled the detection of FAAD2 (which handles m4a) on Fedora properly in the makefile. @ravachol Made makefile compatible with openSuse Tumbleweed. The kew package has been updated on openSuse for the first time in a long time, thank you mantarimay (maintainer on openSuse). @ravachol Added an icon indicating if the song is playing or paused before the title at the top when the logo is hidden. @ravachol Shows the playlist from the first song (if it's in view), instead of always starting from the playing song. Suggested by @affhp. @ravachol Improved the safety of various functions and addressed potential vulnerabilities. @ravachol Don't make a space for the cover if there is none on landscape view. @ravachol Improved the instructions in the help view. #### Bug Fixes @arcathrax Fixed visualizer crashing the app on ultrawide monitors. @ravachol Added null check for when exporting an empty playlist to .m3u. @ravachol Prevent flickering when scrolling on TTY and likely on some other terminals as well. @ravachol Search: fixed files being reordered when scrolling on macOS/kitty. ### 3.4.1 Adds a few minor bug fixes and you can now use playlists from the library view. - Added ability to see and enqueue playlists (m3u and m3u8) from library view. By Ravachol. Suggested by Kansei994. - Removed -flto from the makefile since it was causing compatibility problems, for instance Ubuntu 25.04. - Removed ALAC file support due to CVEs in the Apple ALAC decoder: https://github.com/macosforge/alac/issues/22. By Ravachol. Reported by @werdahias. #### Bug Fixes - Fixed G key not bound for new config files. By @Chromium-3-Oxide. - Fixed restarting from stop by pressing space bar only working once. By @ravachol. Found by @Chromium-3-Oxide. - Fixed status/error message sometimes not being cleared. By @SynthSlayer. Found by @SynthSlayer. - Fixed playlist sometimes starting from last song when enqueueing all by pressing "MUSIC LIBRARY". By @ravachol. Found by j-lakeman. - Fixed fullwidth characters not being truncated correctly. By @ravachol. Found by Kuuuube. - Fixed running kew --version to show the version screen along with the logo makes the logo have a really dark gradient that is barely readable on some terminals. By @ravachol. Found by @Chromium-3-Oxide. ### 3.4.0 - Landscape View (horizontal layout). Something that was long overdue. Widen the window and it automatically goes into landscape mode. By @Ravachol. Suggested by @Saijin-Naib. - Added ability to switch views by using the mouse. By @Chromium-3-Oxide. - Added ability to drag the progress bar using the mouse. Suggested by @icejix. - Faster loading of library cache from file, for people with very large music collections. - Show the actual keys bound to different views on the last row on macOS and show Shift+key instead of Sh+key for clarity. By @arcathrax. - Added an indicator when the library is being updated. Suggested by @Saijin-Naib. - Now (optionally) sets the currently playing track as the window title. By @Chromium-3-Oxide. Suggested by @Saijin-Naib. - Added back the special playlist, but renamed as the kew favorites playlist. This is a playlist you can add songs to by pressing "."." while the song is playing. Requested by @nikolasdmtr. #### Bug Fixes - Don't strip numbers from titles when presenting the actual title taken from metadata. We still strip numbers like 01-01 from the beginning of filenames before presenting them though. - Reset clock when resuming playback when stopped. Found by @Knusper. - Better way of checking for embedded opus covers, some covers weren't detected. Reported by @LeahTheSlug. - Better way of extracting ogg vorbis covers. Reported by @musselmandev. - Fixed 'kew all' not being shuffled if 'save repeat and shuffle settings' was turned on. By @j-lakeman. ### 3.3.3 - Don't show zero's for hours unless the duration is more than an hour long. - Show shuffle, repeat settings even if last row not visible. - Better handling of comments in config file. - Volume settings now follow a more conventional pattern where it increases or decreases based directly on the system output, instead of being relative to maximum output. Was able to remove a lot of code related to getting system volume on linux and macOS. Suggested by @arcathrax. - Escape for quit is no longer hard coded and can be disabled or changed in the settings file. Suggested by @0023-119. - Page Up and Page Down for scrolling are no longer hard coded and can be changed in the settings file. Suggested by The Linux Cast. - Remove special playlist function. It's kew's most odd feature and confuses people because they think it's related to the normal saving playlist function. Plus nobody has ever mentioned using it. - Tmux + kitty and Tmux + foot now displays images correctly. Reported by @amigthea and @acdcbyl. #### Bug Fixes - Fixed "ghost" visualizer bars showing up at higher frequencies in a zoomed out terminal window. Reported by @Chromium-3-Oxide. - Fixed bug in library related to handling of sub-directories several levels deep. - Fixed volume up/down not working when audio interface plugged in on macOS. Reported by @arcathrax. - Fixed bug with library cache setting not being remembered. Reported by @Saijin-Naib. ### 3.3.2 - Remove -lcurl from makefile. ### 3.3.1 Removal of Internet Radio Feature: We have decided to remove the internet radio feature from this release. This decision was made after careful consideration of stability and security concerns. Why This Change: To focus on core functionality. By removing the internet radio feature, we can concentrate on making the core music player really high quality, and making kew a more enjoyable experience. Security and Stability: It has been challenging addressing issues related to the internet radio feature. Removing it allows us to focus on other aspects of the application without compromising its stability and security. To summarize: By removing internet access completely from kew, we can make it a simple, secure and robust tool much more easily. - Also Fixes an issue with visualizer height on macOS. ### 3.3.0 - Reworked the visualizer to make it have more punch and to make higher frequency value changes more visible. The visualizer now also runs at 30fps instead of 60fps. 60 fps was just at odds with kew's very low system requirements profile, and we want kew to consume very little resources. The lower framerate cuts the cpu utilization in half when in track view with no big noticeable differences. - Added webm support. Suggested by @Kuuuube. - kew now remembers the playlist between sessions, unless you tell it to load something else. - Added a new setting, visualizerBarWidth. 0=Thin,1=Fat (twice as wide bars) or 2=Auto (depends on the window size. the default). Also line progressbar is now the default, mainly because it looks better with fat bars. - The appearance of the progress bar can now be configured in detail. There's even a pill muncher mode, where this round guy eats everything in his path. There are commented out examples in the kewrc so you can go back to the old look, if you don't like the new. By @Chromium-3-Oxide. - Gradient in library and playlist that makes the bottom rows a bit darker when using album colors. - Replay gain source can now be set in the config file. 0 = track, 1 = album or 2 = disabled. Suggested by @ksushagryaznaya. - Logging to error.log is now enabled if you run make DEBUG=1. - Prevent Z,X,C,V,B,N to trigger view changes in search or radio search if not on macOS. These are the shortcuts that are used instead of the F1-F7 keys on macOS, because there F-keys don't always mean F-functions. Delete the config file kewrc if you want to type uppercase letters in search. - Now saves repeat and shuffle settings betweens sessions. This can be turned off in the settings file. #### Bug Fixes - Fixed a format conversion issue with the visualizer. - The clock wasn't getting reset completely when restarting songs after seeking or when using alt+s to stop. Found by @Chromium-3-Oxide and @Kuuuube. - Fixed ascii cover image being too narrow on gnome terminal. - Fixed error (invalid read 8 bytes) when using backspace to clear a stopped playlist. - Gave the last row more minimum space. - Fixed bug where on some terminals when in a small window and visualizer disabled, the time progress row would get repeated. - Removed a use-after-unlock race in radio search. - Eliminated memory leak on radio search cancel by switching to cooperative thread cancellation (stop flag) instead of pthread_cancel. ### 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/docs/CONTRIBUTING.md000066400000000000000000000041211507107350600150270ustar00rootroot00000000000000# 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 codeberg, 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 Codeberg. - Please contact me (kew-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". ### 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/docs/DEVELOPERS.md000066400000000000000000000204021507107350600145700ustar00rootroot00000000000000# 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) - [VSCodium](https://vscodium.com/) or [VSCode](https://code.visualstudio.com/) (or other debugger) ### Building the Project 1. Clone the repository: ``` git clone https://codeberg.org/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 ``` ### camelCase instead of snake_case kew uses camelCase, which is unorthodox, but it's what I am used to from my background in other languages. There are plenty of examples of projects using other things than strict snake_case in c. Please respect this choice in your contributions. ### Commenting Please refrain from using a lot of comments, and make sure that they are in English. I am not a big believer in comments and avoid commenting as much as possible. If you feel you need to add a comment, please first consider if you can make the function or variable names clearer, or if you can structure the code differently so that it is simpler and the intent is clear, or if you can make the code block into a function with a name that explains crystally clear what is going on. If you used AI make sure to remove comments that aren't strictly needed. ### Building the Project 1. Clone the repository: ``` git clone https://codeberg.org/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 VSCodium 1. Install extension clangd, C/C++ Debug (gdb) and EditorConfig. 2. Install the program bear that can generate a compile_commands.json. This helps clangd find libs. 3. Run bear -- make. This should enable you to develop kew on VSCodium. ### 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}/include/miniaudio", "${workspaceFolder}/include/nestegg", "${workspaceFolder}/**", "/usr/include", "/usr/include/opus", "/usr/include/vorbis", "/usr/include/chafa/", "/lib/chafa/include", "/usr/include/glib-2.0", "/usr/lib/glib-2.0/include", "/usr/include/libmount", "/usr/include/blkid", "/usr/include/sysprof-6", "/usr/include/glib-2.0/gio", "/usr/include/glib-2.0", "${workspaceFolder}/include" ], "browse": { "path": [ "${workspaceFolder}/include/miniaudio", "${workspaceFolder}/src", "${workspaceFolder}/include", "${workspaceFolder}/**" ], "limitSymbolsToIncludedHeaders": true }, "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 kew with debug symbols. Run this command: make DEBUG=1 -j4 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 CONTRIBUTING.md. kew/docs/FEDORA-RPM-INSTRUCTIONS.md000066400000000000000000000022161507107350600164610ustar00rootroot00000000000000# Building an RPM package For RPM-based distributions (like Fedora, CentOS, RHEL), you can build the package from source using the provided `.spec` file. 1. **Install Build Tools & Dependencies** First, install the necessary build dependencies for `kew` by following the instructions for your distribution (e.g., Fedora) in the "Building the project manually" section. Then, install the RPM build tools: ```bash sudo dnf install rpm-build ``` 2. **Prepare Source Tarball** Create the source tarball from the git repository and place it where `rpmbuild` can find it: ```bash # Define the version based on the spec file VERSION=$(grep 'Version:' kew.spec | awk '{print $2}') # Create the rpmbuild directory structure mkdir -p ~/rpmbuild/SOURCES # Create the source tarball git archive --format=tar.gz --prefix=kew-$VERSION/ -o ~/rpmbuild/SOURCES/kew-$VERSION.tar.gz HEAD ``` 3. **Build the RPM** Now, you can build the binary and source RPMs: ```bash rpmbuild -ba kew.spec ``` The resulting RPM files will be created in the `~/rpmbuild/RPMS/` and `~/rpmbuild/SRPMS/` directories.kew/docs/MANUAL-INSTALL-INSTRUCTIONS.md000066400000000000000000000106131507107350600171460ustar00rootroot00000000000000## Manually Installing kew kew dependencies are: * FFTW * Chafa * libopus * opusfile * libvorbis * TagLib * faad2 (optional) * libogg * pkg-config * glib2.0 Install the necessary dependencies using your distro's package manager and then install kew. Below are some examples.
Debian/Ubuntu Install dependencies: ```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 ``` [Install kew](#install-kew)
Arch Linux Install dependencies: ```bash sudo pacman -Syu --noconfirm --needed pkg-config faad2 taglib fftw git gcc make chafa glib2 opus opusfile libvorbis libogg ``` [Install kew](#install-kew)
Android Follow the instructions here: [ANDROID-INSTRUCTIONS.md](ANDROID-INSTRUCTIONS.md)
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 ``` 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. [Install kew](#install-kew)
Fedora Install dependencies: ```bash sudo 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 ``` Option: add faad2-devel for AAC, M4A support. ```bash sudo dnf install faad2-devel faad2 ``` [Install kew manually](#install-kew)/[Build an RPM package](FEDORA-RPM-INSTRUCTIONS.md)
OpenSUSE Install dependencies: ```bash sudo zypper install -y pkgconf taglib fftw3-devel opusfile-devel libvorbis-devel libogg-devel git chafa-devel gcc make glib2-devel faad2 faad2-devel gcc-c++ libtag-devel ``` [Install kew](#install-kew)
CentOS/Red Hat Install dependencies: ```bash sudo dnf config-manager --set-enabled crb sudo dnf install -y pkgconfig taglib taglib-devel fftw-devel opus-devel opusfile-devel libvorbis-devel libogg-devel git gcc make chafa-devel glib2-devel gcc-c++ ``` Option: add faad2-devel for AAC,M4A support (Requires RPM-fusion to be enabled). Enable RPM Fusion Free repository: ```bash sudo dnf install https://download1.rpmfusion.org/free/el/rpmfusion-free-release-$(rpm -E %rhel).noarch.rpm ``` Install faad2: ```bash sudo dnf install faad2-devel ``` [Install kew manually](#install-kew)/[Build an RPM package](FEDORA-RPM-INSTRUCTIONS.md)
Void Linux Install dependencies: ```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 ``` [Install kew](#install-kew)
Alpine Linux Install dependencies: ```bash sudo apk add pkgconfig faad2-dev taglib-dev fftw-dev opus-dev opusfile-dev libvorbis-dev libogg-dev git build-base chafa-dev glib-dev ``` [Install kew](#install-kew)
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
#### Install kew Download the latest release (recommended) or, if you are feeling adventurous, clone from the latest in main: ```bash git clone https://codeberg.org/ravachol/kew.git ``` ```bash cd kew ``` Then run: ```bash make -j4 ``` ```bash sudo make install ``` ### 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. kew/docs/SECURITY.md000066400000000000000000000012761507107350600143770ustar00rootroot00000000000000# SECURITY ## Reporting a Bug If you find a security related issue, please contact us at kew-player@proton.me. When a fix is published, you will receive credit under your real name or bug tracker handle in Codeberg. 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/docs/kew.1000066400000000000000000000077771507107350600134720ustar00rootroot00000000000000.\" 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 \fBpath \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 \fBtheme \fR Sets a theme from ~//kew/themes .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 theme midnight Sets the 'midnight.theme' theme. .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 favorites.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+n Show help 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 x Save currently loaded playlist to a .m3u file in the music folder. .TP 9n "." Add currently playing song to "kew favorites.m3u" playlist. Play this list with "kew .". .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/docs/kew.spec000066400000000000000000000017701507107350600142470ustar00rootroot00000000000000%global debug_package %{nil} %define _hardened_build 1 Name: kew Version: 3.5.0 Release: 1%{?dist} Summary: Terminal music player License: GPLv3 URL: https://codeberg.org/ravachol/kew Source0: kew-%{version}.tar.gz BuildRequires: gcc BuildRequires: gcc-c++ BuildRequires: make BuildRequires: pkg-config BuildRequires: taglib-devel BuildRequires: fftw-devel BuildRequires: opus-devel BuildRequires: opusfile-devel BuildRequires: libvorbis-devel BuildRequires: libogg-devel BuildRequires: chafa-devel BuildRequires: glib2-devel BuildRequires: faad2-devel %description kew is a terminal music player with a customizable interface, playlist management, and support for various audio formats. %prep %autosetup -n %{name}-%{version} %build %make_build %install make install DESTDIR=%{buildroot} PREFIX=%{_prefix} %files %{_bindir}/kew %{_datadir}/kew/ %{_mandir}/man1/kew.1.gz %changelog * Sun Oct 05 2025 Jules - 3.5.0-1 - Initial RPM release.kew/images/000077500000000000000000000000001507107350600131155ustar00rootroot00000000000000kew/images/kew.gif000066400000000000000000001730761507107350600144100ustar00rootroot00000000000000GIF89a¢fçÔ  $1 ; + # #$ %%#A&"&##(! (&3+!.0-32)H26H33#3& 5(!5356(B9:0:0&:?L<">A&4B+!DD5D5(D:3EHRG2PI?&I?5IO`J&KQ:0TTD8V4#VD#VI?V\nW/@[[PF]8Q]Q2^$'` `L?cC\chwd7Df5"fI5fV5fVGfmfr‰gD%h#(h[Qk^Tkf`n^4opbHrVDuf5ufTvD.ww&-wT8zk`{NZ{^q|:C|vo-5@OrV‚‚V=‚cO„r?‡uc‰CP‰h6‰o„ŠH,‹)(Œ™·}N‘|h–S[–‚h—Q2—„s™7>š$œv:œ‰bœŠzž‡G HN “U ›‘£]1¦’s¨2/¨:@¨ –©€—ª“‚«! ±‡B²™w²™„³Ÿr´?HµB7µh?µžU·KN·–µ· ‡·¥“·¯ ¸±q»(¼[V¼¼rÀD9Á=IÁe6Ák?Áž¼Á¨†Äª“IJ¦ÄÆwÅ®]ÆBLÆLOÆ“IÉ0ÉrBɲ‰Ì¥É̯™ÌÍ{Î7%ÑCLÑÖwÓB9Ó²[Ó·hÓ· Ô̺Öo>Ör:Ö‰)Ö²‰Ö·›Ö¿¥ÖØØDQØQYØÁmØÁ–ÙI7ÙI<ÙN=Ú7"Ú¦RÛDIÛVDÛY_ÛfBÛrrÛÑ‘ÜegÜkmÞG4ßB6ßfHàDLàwwàƒ€à‘‰à×ÅâQLâYOâº[åfO圖忇åÉkæI6çMUèuO誠èÝŒê„OêÌ›êÎpêΛìT@ì¶«ìОíŽ&í‘QíĶíèÓï™Oï¥Oñ\`ò²QòÌ¿òèÓôɉôÖÆôàÌôïÝüaiÿu}ÿ‡ÿ‰‘ÿ¥ÿ!ÿ NETSCAPE2.0!ù ÿ,¢fþH° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3jÜȱ£Ç CŠI²¤É“(Sª\ɲ¥Ë—0cÊœI³¦Í›8sêÜɳ§ÏŸ@ƒ J´¨Ñ£H“*]Ê´©Ó§P£JJµªÕ«X³jÝʵ«×¯`ÊK¶¬Ù³hÓª]˶­Û·pãÊK’šÝ»xóêÝË·¯ß¿€ L¸°áÈ+^̸±ãÇzEB†œl²ÞÊ–3kÞÌ™æÎ C–,ZðçÒ¨S«^:ÙiÖ™IÃæëÚµçÚ¶që¶}W7ÞÚ³ƒ ?Œ»7o»Ào'¿\ùîÏÀ}çͽ|x^ÙÖ=3›Æ=wî̘þ)›¶{6ïßÅÛåž‹»2»ÊÂO{O¹/æ×„ñ+Ö_ø>bþ—g|å)SY6ßøÝ|{ èž‚ó!ÈŒ6ÚM“ 3è7_xf(€³ag3º”¨K2$š¨‹2¹¨h Šºäò^Š%¾—M‹&ÒÇÜo—!—Ÿg=î•›} yÛtöùˆd‘Î iœ‘z±˜#5R–(#5ٔȌ]Yš(ãk4Ƹ¥25v¹å4*&ƒcŽk®¨¦—fw—ˆÂ½ya.ÞáØž”2sž‰)Nƒ¢–~R¨•ú)“ 5Úl·èoÚHÈM| æ•Mx3n—L|Š’§ ~ŸzGe6¤2š¡26>Š×–Þ%#aeþÓ`©ê]¥X¬v5g^¨rãh”ä¹f`©ÓáÞy-^X¢ fri¥•:ÚEâ5’¹b³Éjj¡•óeëç›1Ö(§]tg'nÖbxnejæ‚g²ƒâY¢Ì´í]áigàt±Æ·©÷b‰á™Â2ëÚ®yÍÇ"Ѝ¾'¨ ®á‰ª^߯*(ª(ºº¥¥g kó1<ñiÈï¦Ù,¬h”ZÚ–n¼Šjé¬.቙״ñňj™ZÞHó¦žI©n‹êR)æËP Wîlànl4Ížé 1¼ð¦è®«6w°w«7«󕲡SIßÁúù9Šf“-(•2®ü5Ƭ>,p”3^Šþ§Ôôë4¾•!Ü[Ý,¦|/ŠöÞeí{dÊØìÓ&{³–ð.nf2Vî*tÔ1^ñÏð&ÜÒ³Y 5ÜPƒceɆ»e˜¤Š«õeÁ¢8+r±fÜ(»¯²¡@²Ýàxoê'žêr*÷Í^º^«Îó*^Ç•¡úzÃx“­×Øcz»¡%.Z}øXòü3ê‘Ç8¹‹Oï|áÓç§Ë9¢˜ÿ<-“©‘Û¦82NtÔî Ïç¥Ba jR XÊâT(½äl‚㦨q!€åâ<Â ÂÆt<‰'>Ú˜X”¶ä¼Œ1ëoðÙÐ¥4…¡XÕ+‚†š~¸ç§ -_äƒOâ§(¢Oþ‚’ܪ£Ø­hsð[”ü줩7½nuãÒßj€–"ÿ¥^˜q]êî‡&okào¨8 ¬UoÃÝÁhƒ©í´­…ìªaÀÚ=8F/z«!f50¯ñJ†Û{O«jø½—¹ær8óÄÒ…DÆÍ^‹Ûœ­ÄµÄAŒsZÖq†#EÕ°¨^äÛ\Ó¾Ô9‚fêIð³_ê{¢Ê`ß^ã¼ÈæCf’¯468jGMßÃ’ »=3ö²lj²‚Ö¥Àá‡mÝ&ˈ–J-i²K´–»Ä5DšõOUT(/¹ù \Æ5:™Ÿi²œªs“–^6-J¢ P¯þñŽ­^I;í`iSi¤ª,ö±_Bó@T œ¾FùÓxÙ [%ÂÞ4T=³Þ¥¶/RTQ¶‚%()¹¦/uiž0 ¨´`¤®{¶ì¤íù!«Ú´P©Ô:ìD ×4•BŒ’g;Ó¦~ZÁºkŽÔàÈdµ %ƒB—ª·c OçJŒ¦ÞbÕ¶·ía\ÔØ4î¤P©ÛSâ¦URQÈR•ùTƒšÈ¸`Tg:¬à^è8 þNzä!ÏÙêµ§Àõio‚Õ‡‚Š)ü©&§­É'o¤³œ|þf“½1’zDþ(I³PâDÛ$Úç>ú9ÍqH›ZÓB'³´™þls8+ºËVÖ9 š-l«£NȪ0’µÌkcû#Ψ60Žõ,ryûÛæþÅ·¿Mî`,›¤Òˆ¶µ­IÌk¥ëî:W1Ðý®xÇKÞòš/á=¯z×ËÞö‚&½î¯|çK_¿À·¾øÍ¯~Ë{ßýæ·HØõ¯€Óß·w³NðcC¢àçÁÆ2–ñ h#/Ïh††¡ñŒ ÿåÂñœ,b_õH±ŠÇ`ÐCÅ)F14ø²Œktãön‰×[à«Æ5 Ì3Þc[HË(rŠßqäwÅ*¦ÇŒ}<àSÙ2A0žqq¨ã׌,î¡äzàãv¹Fþ™ëÑ cÍÖø 4ºaeh:¾²ÒH¬çáÜ™Õ ó;ˆœbw$÷ÍE&2=‚A uÔƒÐ0¦G^š¡bH?ºq¶K0TŒzÐy}>¯•C7£îÀG˜“\æ)Æk®‡:‚e%ã#ÈÔpG¬ÍŒf»èºÈm®ŒKjœò¹Ø¡ †5ºñâ%×ãxv´’Y\[½ÐZÉ„þõšAmVïzÑv鯥U|véZÝ€*Ž„ìÑ»ÝÂårªc}äjX:Ƹö 0¤½k8ßÖý~G›©23µ~¶:2$x¿÷ÝgL2–Ñ 2ïšÈîÀó=îéÁ¨¹ßk85þÄ r|<á)Žó3°j£ÏÈwÄ;3ê™kÚâ•æxŠÑ ð"«£Úzñ6È}Ž~_|àˆ†1‹{ã6?Å÷èF3dnóÉÔÜæÉ8ñÐeír”§xøηŽo_ƒœÈЦÆÇalènXšÈG^ù¸£Îíª[æê6ûÖñaç¤WZä€!9ÙUÌè\Ÿ=ÅÍp¹ÁµÍi°\ÉG&­Ýƒw{—éØ63=Æj¹™TßK5ãÄ¾Èø‡×ÛìbÈ—ù²t«=³ wX£ð“/Ä«~¶/z=ƒ‘e£_Úô€Aøá‹æFCžà‰6ôÈ•|‹2ÿ\öJîõãSþoáÜ#¦ò NÆ3žœqÇšã½þ,æ‰Lmàë׼Чlü[cÿâ§æ¿nJ?ZÑ·zÞÇà'`É ÖÀx —|ÍVdí7cWvÀµfc^zú§dÕfJj×r`—JVxL7hfÖ !8€€Q€þe hf¸hr|±v0Ö|€±iJöd„¶öçyKÖ ®±3– î s0m•aƒ„n»¦ÐgÇ‚ñ….¦sãàX­·ƒÄöl'€-Æxvc)v¨àu3¦|ØwÏ7}ýv}nh æwk樂¸{}Flзf®£gk§l®6Wwþ!… çÆˆÏ·o¶ö~Lf·k"§ˆõ€Îrpˆ‡×¡‡z Ð` ¤h 1ç„e¦‰Àu„J†nÍ–q”Xf€ç@HxË€G†‰Ç„±6‰Ô çj˜‹Žr³è‰äŠ> ª§d÷x[¨d8•¸kSVŒI†w¡|–¶¦x*‹:8tM¶‰±d˜¨•! ¸kwˆŒè¥Œ% /¨b×Wk(ø†ÿVfÕÀ$É n–lÂfk†ýöŽ˜iAF‡lf²°z×@–~𘌠Ql·¸kö‡D‡\‡„ü÷‹/؉~±ôíHd$¸ué‡À†þ9pjXiG&ŠÒ¦ÞG…çõ~m¸…÷öŽ|bÉà“1Y˜HƒÉçu1èMv… ‡{5Ù†v1“Ý6{wÁe:™{¼ƒÁl€ª¬˜Ê¾mý¨Ê0pÒ˜­Ò•Ù.½Òj0‘ÀÆÈ ÍÖ~Ò9Àh}Ì ÛNôƒÚ×K¦ÃÆ«Š.1EOOôˆ„uìc‡‹o­+?]Ò°ŽóB^ïa_äNö•¹n©ãuÖŸwp™Í•vfœþ¢a¿kmÒFùÝoÑ ‘pTmæ.í‹^øg~óÑ.ä=`E ‘@h@|ðñ>=.ivFËuØô‰A˜—NªéŒ#ìÐ=K¦ÆÌå›QhÐáeþÞ÷îÒ`úhþ÷«öÎUtp`ð1@Óyq‘ Íæî·[†<ëÇ8;Áî¦òš± P&º2Ì LÖœH¾ÛàñKà/S ‚5KáB† ; \(V'а̇ÚFŽ=~2d²dÔ€¡:¬cI“=Sf=PQcæƒUè R)RèP¢KëVOi½w÷–u·TêRzA‹^Å*RæV®]½~U,ÇgS—6µ¶ ˜Õ«Ïè™ÚíØ¡À*üRxÑTÂN/"„eªoC¦ æý58p'Ã…¡Ùã':=.¼Œô1Y°2/dþÞˆêL \ "YõÇkL•VÝØí\ªsWߦÖY÷î°¸…&=_0±,7S7x½q¶}Âw–Þ„/^ŸXðÂÆ'&\øp &¾‹&©JŠ`ÆLí1ÒhÝ€¦Ö£«ûÌG›Êמ)©¥–3‹}ã­A¯)9¸Üqh®QÇwÔ馚¹ÐkMªåÞyÇâ Q©G=²ŸnŽ»‡6©èYf™`ÖºR,Ï”©Ì’y ®ápâk¢rþsœeÖäš{‚#qª{\ W¬¯‹"Z±“í¬[10†h„ÿV³r#&ÁB£#`.h²Á€AÓ®RPI´÷bàcÒ+› JÏ¢ ŸæÔ¼ M4ieÓ5³ÔÓ¤9½”ª›9 &*û¬Ÿf ß=ôYE «q Gšè"T¥‹¥Oª¨BIœbøÊRã´U#jÐèt¦>¡†Ž¸º  hõ(Ýlsª>­Á—([ÏÄ7¹W—ÂGC|<ÖzîYVÓ¤„ «°Z¡¶!±ë˜»‹Tà–®Hz ï‚”6Š¤Ý­øã[ϕ٫Lcx«2 ˜K™þRg_~¦g­nßeÚ¬àä2J_|^mVóˆDº%*ô:±ï¯Á<¹d¬€A„Ü— T`ôûªŽ>AÓ½C6‚{+b­\u=2›. Öµ …V6£?B:iZ—i:rÚxõh™6—S'p¬0ÖzcÁöšˆ±ÏÁcèã‹Æ&{ ’Í;äœÝ½jZþê%kîê0 ïåø w4^ß*†Å×l†ðÙœ¢fbª—R‡¤ã9jœ7\›—<{©(4Šä(ÎÂ0Ô@LcÆH ¡EÃÖË”íØßÖ7ôpî, `ªÀt#`zßêÉ%]‘T²?¯D,²[‰Iþð…¸ˆ%„£ À¦·‘êí_èÙ“ö²÷Žðiê ã„è0¨CÅH _ÛNC&bÂë N!ú…¤T 4üíüóJ Ắ˜«JjãJXÄ­( 2ÞG>Ѫi_ôž˜8A'7Ýi‚ÔMÏž¡< R¬aõ¸H A9!" +*AH—ÂG Z{ñ˜_’É$#) Ò'X"¯Îi^qÙÕ•Oáäˆ\¹Ûì΄+¡h­®aµŽLL9ø°Ø±–€¨‹N‹•»§¹± Bh4áwØè17¾q³h%%@Ê8UAHàÑé‡4¹Qƒn^Ñ0ˆþ“dj-¢ÚM!5E?÷ÄNM,©RI–榈ir“2Z5 ¨ÁÙ¬›[Zܦä(­WÒès[KÌah4TŠ(ˆ@fà¿dô i¶ÛÈ0e}ÀBÜÈ‘v“ÈŽ´´ÌŠqJ‚8)k‚Ô»&6{ ·|ÒÝè—Ãêñ¡gAsáúLg¾W²ï7ÀÏU€aP®¨+|¨d¨<4ÉtvôʹŒr©œêñ™ÔHž—0©©éET¢·IF0žÑŒf@CL \7¶É4wD ÚŒàñèp…­…´ÕzVvÚ8Î_\€”Ÿè§L¯–4ά8y¤ƒxf’¸~¥˜±ç~ˆôþ·+m”ɪ¦IÆN£µ+¸YFárAƒ-ËÇTÍây¤5#lÏPA‚­­0…­©A r¾ë ">ç'N¹B– "žhJ$“ˆ4‘:éL‘4•·ÎT¡X¡¢Y†§äàP=3,Wn“%¦ÍFKÍ0ŽœÆÑ¥íuã¸x¢C—2ŽéÅ€[CaŽÀª±ô‘u 7;7<ݤ ¥ÿXz™ ÞîF\¾ÔÿàSOAB[‘RŒ‹§b-%„KîVV£]Œ×4Œ0)(VJ†åhÃÝãñáèôêHÑ ^%@Òýàd”C¢ ŸÔÈk{÷Zþ®ø448lïo‹T] ˜XTœÍ–Ô» §I2Áì…5¡²†5"K±©p8œ0)ŒNè>ŽwÄhÌÂ\…‚Š]~Ž"IF]ÏdSDÌv?bÞH‹¹ƒèÖw+ØÏ‹´P©ø'NÑ\äÞDfiJ>®áßtãôÒrÂ8=4œñ:ˆReC2„ˆVÄ›…&d8”HИ+=` zVÒæ½'|쌫wªZС#<«uv_C)êÈ3¨;´ÒŠÐ…¦‹„>ù¥eUÊ@ˆ–ʰŒ†ž˜4b-Ý^D—>Ðn | I©iYÏ.HÀ.5äì •þ™Ö_·þL­[ý©Ù3¨ÀÝVô !ô¸ÃÏÔ€†dÏE5»ŽÁ]6ô¤­¿aëÝ2­øpƒÍE¯ÌÖ_:=ÎY@‚G&Æc2¡J×€V¡ö-õÖ$qM©õŠMggôr#ƒôJ$H‚cðBJí&à…ûÈà’Qö'»¹”{Xc.¾vx=÷P"dìÒåóì*Å+ÑD/$èÏP©ò¯´­€ ‘Þ;Ÿ¯T¥[Ii=y·• ¤J$$ÞVº)ÕFÇÄ©Ï(ð¡q7â·9zd,™p³P¨ÍÓC׌h‚…åL9ÙX(8æhœG€ó˜Ë H"½þ,IR(λò >ð €;Ððw¢ h¨B @ïÛꆨøcW%+§™,Sâé¹ÈQí‹ %0¾d›§$ˆð€—¯¥u®± ±ªÕkGë/­"·Š0ã6;ó7kÁOŒõúSBÅ–%ltB¯&éÀrk°u€ /ºä[¼_ š¢“ÐÀ¢@Ƃ쩠·º¹’ˆM†$H¨ÎÎ-ˆÒ¤ xzùªGœ2XMº(‹¥¬ã¸†k¨,|EÜž`*Û4 QzO3d½Üð ˜ÑƒHP¢` [:÷x€&;ÝOäÁûùoXuR{0„xþÃ\tLø¨GSs™3Ã¥™:“èwpQ9QI†gø?Ûœ´íôX‹6mÓØÄ T ¹Ýx€¿†äHÒΕ:txÒ?…H¼ Î- TЮÐÓ\w˜Eëˆj4WdI¡¨> 9ÓeKS5Ý38Åâ쌹ÚÈݳÉäƒøÓTµGP&ÕhE3ýA{sJ‹gp‡¢a à æ¡Ë1ý$SV‰ Š"@4åÔ½, 9=}›Ýˆ7¬ø„èLU}Ò6Ð/ÉH)µÔ¹n5±’耑¦ 1ÏŽ@OŽˆÅ{€°#s´Âi.©ÓdmÀ£ÅqÓ+¾“DþÕ‚SúrPkMU»ÕМÖÂSyªSK ʱ††81­Tð«>AsNýi†)“œ £W5eV4QAŽÄ!U D@UMUV•¼†l;Fà ࠇg`>¸ÀXÙVâbŠƒ šK­†wm&ûؽ×i¦OT=6´-ºè€UÙ?…p-޳©‚—è ) IÌ™¾Ù?ÉH×¶–{èQZÐ ûÙqPz¸‡{ w€É¹Zz Y$Š&—‚ŠŒÍzZUýßZYè|%·0èQ•‚ždBÅtmG¦Áup¬J,’H†Ã[zÍÄ 0Ò º5×ÕÈ”¾MUþo0E|)ÒÎСbm“”Ô³ z˜ w€SÆÉÙÇcÏ/q:3,[ÍÜß]‰gm}»½ Œ "pZÒuR KùùÎ5Ú“Œ¥Q‹†:¹ÙȨË~Óžå ÛyÕ ¤(Çg à5ß™‚^gÔó¸ÓjU^½½;UÜÕ@2¥˜Å3ǶlÅóÌÙ×…WíQ‡ $ÃèÚKÌ=_¨'"€J°pµÚÈ °‡&}_½ƒð\Æ‹…õ ¡YøP¢†·ÀóDÏgÚËv=+ñµW Àsmà2±’ÊlÆn^¬€ÍÄà öÌ&ýöLä!3“Ú[ÑF•þ € Ü‹t5‰jxÜO’òít¤»aýˆýé\ù½ Tøa!†Ð?0Ti=mUù™a§0–r”¥ø_3¹]À¹†ñ¤²æ3œÝM†X%z à/¾Ê‹·5  hUd°à FcÏÌ#¾ÔTT˜E*ïÙ¦åHá×u‡oÄ *ÎîZí![;T²@4dÉ«'˜xƴ쌥½MqßHÞÌ6 ä:ò®°ŸBŠ} °zð*CQÈjd,^e߀µHD%Ü6Ĩe[>Yže4Hb›Ár4‹-‘“yÄã‰5Ã( ®ÜÝC_nfAƒ'6ìf°(1ó¨‚@þ°eêŒù¹9 µÝ+Y†®å ²ÈÞrN0J!ee~ fnçA%\@z½L¾¾'ÈgÏ…î”TˆD8>àƒ¶È‹ô]Z 8³¸éåPÎc´ìý±â©±MU~è ÒF:íá± å‹¶5èšÆ æ’Š”–Ø„tN¸.êéÉj ‚dÜle–;àiFdgz]¨múdŽhj°%´lë°ë±&ë²ëĶcñkh걦°Þ³Žk±æj³¶ë»Æë¼Öë½Öë·Ž3[¹€`€k¾ækà»æ÷}ƒ0 ìÅfìÆ&ìf¶jpì²>¶ÉÞ똎ׇ±ìÍæìÎöl²>Ðþá„Ï6k4à!Æ`^Á&íÖní`¸†n€Ò®l×&ëeØã µíÝæmÛæ~ý`Dèm°V†HÞ÷áfíáfîæ6ëcÓ†è–îé¦îê¶néÎ†ëæ†éŠ•ëöîïïðïñ&ïò6oêŽ"˜g® N8ï÷Ö†Té|ßu€‚0€ïüÖïýæoð†îþ.ïi²©ê7ðGpýž¬Œ ؽ ˆ4pïÿîJSx_/  çððcã†ñ'ñ7ñ?ñi˜†n |‡ñ—ñ§ñ·ñÇñixfx†iÀñ/qmà†J¸Gè[ox$`þ gò&wò'—q‡r&Ÿ†fè†q˜ò,×ò-Ÿrçòç„€‚Ã~Ðup˜ƒ/Wó5×r)gs÷ò7—ó9§ó:ñi˜ƒ h2÷LGH;ôAñc‡CGôDWôEgôFwôG‡ôH—ôI§ôJ·ôKÇôL×ôGŸ†/¸Hào õQø1¨„MgõVwõWgtC‡õY§õZ·õ[Çu\…9p‚H Hõ/(nÈõb7öV—õcWöegöfwvJ/‡g—vg?¶r°ökÇöl×ömçön÷öo÷p÷q'÷r7÷sG÷tW÷ug÷vw÷w×öj‡÷yþ§÷z·÷{Ç÷|×÷}çwt?¶vøvÈö€'ø€¿v‚ïv„Çö‚gx€·v/‡†—x‡7ø…ŸøŠø‹wøŒ×ø?øŽ—øyƒßx„/ù†÷‘GyŽWøˆy‹×ø‡wyŒgù—ÿx•‡ømÇù'øçùŸú ú¡'ú¢7ú£Gú¤Wú¥gz‰?6x€ú¨—ú©§úª·ú«Çú¬×ú­çú®÷ú¯û°û±'û²7û³Gû´§ú§Wû¶wû·‡û¸—û¹§ûº·{±?¶xÐû½çû¾÷û¿üÀüÁ'üÂ7üÃGüÄWüÅgüÆwüLJüÈ—üÉ÷û¼§üËÇüÌ×üÍçüþÎ÷üÏ}Æ·üÐ'ýÒ7ýÓGýÔW}Ó?¶ypýׇýØ—ýÙ§ýÚ·ýÛÇýÜ×ýÝçýÞ÷ýßþàþá'þâ×ýx }ä7~Úoýåwþç‡þè—þé§þê·þëw~dˆd’"ÃþïÿðÛ߆mÐ~í¯…í‡ôG†öwÿ÷WdP…ù§úo„ûÇÿüÇ?àÿ@àÿÿ7~ü¸‰3ÐOœ„&4˜°KÈ#v©XQŒ˜,@ÂÑcG*"©) äJ”2VÊÐQ¢Äˆ˜#*”àÀ¡Μ8ðìéóg„ B%•Ò¤J—2mºtÔ¨R§RŠ,ÞÕZXãiÍþŠذaU!#k¶¬ª´jצmäö-\· ¤[÷àÁ‚ JÌÇ/à85fùhx$â‘&S2þÁòñJ0t¨PAâ2æÌš7göÀù²‰Ð¢G“&½bʼnÔ4V¯ÆAì8d×b»îÜE¬ð¶‚ÅÊ•+X¼`)n|Ìäg–/³çùó3€¦?ŸNÐuBÚµ'êîÝ{¦ðâ3‘*_êÖùR¾ÖûÂU {\¸†É¯ ¶ûùñëdž¿þ÷Ös ‚ *XS :ØNü4!PBY(z°á„všj¬½6·ávpÆ©ˆœrÊ1Çt{,ctÐIG þ{ä¨#ÐbÈAj·wà7Þy­°·¤}üí' €õ˜`>UZy%–Yj™å‚]zù%õä#æ—ezY ù ¹&›m‚¹à;R¶É¦™p"çxÖùåƒ}>Ø&>øì9hž…JIh™q*zf˜y¢©fšs¢ §z"*é”o"º)§zú)¨ V*g¨¥vé'ªM™º*«—¶*¯Ê:+­µÚš'ö5Ùh¨©úšÔ­Á ;,±ÅK,žUæWÊ‘ÍÉlx‰äx†§æA¼æ·Ýv‹ƒ AåWØ60þÁ…y8ÒJ/ ÷2Œ9ë 31ÅS¼Ž9›3L+—òÆAà`í &À¡h+¼ë+«“j/Ìõº¬¬/˜xÇÝu9êÌ#vêaóN•uòª ¥Df•ïø"­Œg`QÄÜàÅw@ÒÊ0ëh­1×]{í5:æX<±Æw6Ø_s­5Ûlµ²Ÿ¥*ÈE¸öZ Eœ±ºêµ‡˜cÆ<8‚ùe"-!ѽ¸øÛi§oq»AÆ&\D™k…pÄy>ÜQá %oHCX°È8ë­/g]"˜ˆç÷ºÿØ_~ð1›HΊ?]Ä •¯àçX¨aÈ(XKÌ5Úg[,þvóaS¬öÙeKOvÆd]¶ÚØO 7ª‡.ø¤~òù²û¾àðÕ­¸ß $Ü¡ÆEˆL¹œ0bêÒ9û?GÞ w‰ð€„|¤)AŠÊ]“Øõ2M} ™8Ãp` Ð j¨ZB(ÂþA„o8á>BC8¸ZÄ–G½f¬mÃà$ 1Âò°‡!,aîB/l®¤+YiF³s„Îòð‡E8b½`[ó¬—6³ezÕ‹áÖžÇ<¯ñ¢`#÷Ì–=îÏOA8‡:DÐÀ[ÝX¶ †Kd±‹c¬¡ ‡ñ C¨a †Ü‚æ™HEnþΙ;$$·`#ÞMD¥Ã–¸<`‚Ó0Ñ6ëÖ ÂµÈ6±Ánhð¯ l¨•¨@¶fGGdíb£¡ýèGëa†½Ä%0Ìajz»,¦¯7½/j­oP 8!´Á˜a[G/Z1ŠaDφ¸ÄÂÖŠøÂb­h¡ÿ N@¨sŠÛÜZÄÞÖÆ"~´„Ûbh6Œ}mjäå2‰)PaN,œkBÃØÅ­qì}Å(01 ^ £¢½h>sé¼±=¯›ÛËÞ/¿öKŽö‘¤&=)ó>Pî±tm 5iÆÚ BªÁ k_ò ÓG¨! °1þ°M0‚”þb[˜A”`^zmθ^Æ qHRõ ^7i‚y>¨†Á\æJ™Ú½€v±—e=¦-Ñ:Ãé}tmgܨZCªQµµ®_Ä¥ôŒ©×“ò•£%éÆZÁOslA'8Éb»! ‡ƒî0?5x¡²^8*NHà/Ü–[ãÑh×[vÍ y=c/*O®2åŸwu)Æ*j÷ñ‚`ÜZmsëPm*oðƒÄ(>ÛT…’´m-¦÷°¸Vï!¯qýì9 aˆ?€lsÖU‚±0|úc¼…!@f7Sâ€QƒîðBâe˜l³WÆ”‚V¤Êô§YuéO½:bþJÐØÖÚæÖ¦žÖ‹ d>÷{Ë‘rQ{(=°Y­éÚe²¶Ar%B[q‡,@s`ì+?éƒÊ…fC8ð–npãÜ„Wƒ,°‚PßÞäñ¢amæ…#€8?ˆŽt° nÆHÁÒãaƒ]#ZóJVWt[P¬±ûJ¸¨ø…k[MZ•öµÒ|MãoYa§èõ–¾Uƒ&Mf‚"xafìqHúÕ?gìœòj/6VQHxáÆ3þð€¤ ‹Ž¢„0Ä ´| /`‰uôà%²†1ðJñÎv6$òà»y@ À…h=l`KwÛÞÖ!e}z²œ¸zÄ󤽪î'·"QPŸµ1½wúÇ Îë:¢`DËY ¼ÈèY‡; na“ÜŠÂj©FKº¥+}°„=J¶ëÔÒ§+l9¶é¦¬màJÐlÄ–Qûr )e8Æ‹ `­¢²Q˜Ái”Pµ¦0 x@ÔPH=* kà ›» †0ýƒE7úš«hÛ‘³T{jëó%î`È ˜àVn“… Ð…‹V¸ëà…»_¾É(}£ ÅÙGq‰?\Vß>þå FKŒÉûÞ`…©£]ñçÚl%tt+Ô`Æ‹[ܸ 2u·`·€±3Ø8S¦kÕÓxƒ•! ;šßÍtï î´÷v¹Ðu@âv¶Ô/;â @"óN§ë¥ÓÈ\A› ¼ƒØƒ4 Ý jXï!ìt¦v½lØä&Ã;àv_V’¼ÙÂ8;´+;ÓŬ^µ6=5TüyëPƒë—9Œ øàŠÖrÃs¹=K/¸†Ã î˃²‚ Xj€¸Ä-|q‹[ôòKÙg%“ b_}üxÞ6àAqApX>1ݽåÝ ”ŽÈ¥ZŠ^ö¤iþåÁ X`Ę@Ýù•98æÙ ߣÅ` á´­˜ DÁ,&¼‚Ãü öŸÿ!…P!V¦©\&aÄ… 0-Ó:øÂà!¡9$OÄ,Êy< Zß}àʵŸöqÔ:¨ ÝýÓY‰‘\ùRiùÛyÀ \Î9Âþ!á&ÅF5aþ! r^ ²aƒaŸ~YÌ:ÜfEApYaè…!ÙŒPÉÛìÄ "Þu“ìÍ:@8=¡[iªU”6-ÂïµÂÒ1€]átíÁØœ à@¼æa/Ü¡-:Ì"¤a"&Zà*š/ÎYálAâ*¦¡fZÝ5ZþØd Å„Ð%:Ù2vTø¡ :t">Ùô‚|Ré˜À‡eˆå”ÈL•É!(ÝÁUQ DŸïí)ð_.Þ#î"0˜Vw…‘ú £Þ)ã2 ã/‚[ÈML¯5"ö­ÃÁRåݤàÒ^}ibEZ/:œ‚]Ì(ÌÜXI– 2—6bžÖÀÙäí d×#ŒÂ+äßzàbæß+\Â"ÜAH–Mâ#ÚãO>Âà@ècXy`]‘‘€Hžš¢–.Ú¢á—@c6ÎtiLËU¡_-Ô0.ØÖ›ú1`@nUväA­)Ö“»ÂÜtå$ÍÀ Ü>Õþ'N Ø)8šÀfíAú Pú$afòßþ% ŽÁ#¼‚Q*`õ9iÑ zÓ \0ΠɱŸ9ð‚(A¬ÃÌ@I6 ~¥É܃©æXBömÑf6]Xe£Z‚—t•e‘p •=€ýÁŽåâ-<‚HÀx€Â"Üï+ä¶xÀ,>B¯¨÷Š`/¦ë0äPYm!)AozX¶„šV%ч\Ëî­€ßRÍšõV÷ØR‚^YÝ#\A†< P, Ì€Èé¤ðÿVñ=Rð°…ˆ‹hÄ Áå"ÔcM’‚íj "D°3°¬±¿qñRðòž@R<RHÀ,ÞÁc¦¬˜ÊäLö1ŠöÚ= c×ñ“ƒEŒ/ ›ˆ£ÀzÁ•EÑPè%ˆ( 2Þä%‚+åÈaÉ£e©¥VQ|!l£Em¯ã­9(Ð"´’(¢Æ®²zì+”.¯®´Êî#€ "áÁýUò,;Ìþ-—Ç#Ñ ]AÆ’À,Rì®%ï«q_³67ðËAxˆc’qLÇ`í's°'=ñiˆÆe´Rb½ó;Ë! ²“­‘ ‚Ô}©‚þY±-ê_N^-ÙÒYèL¶LHÀ ÜÀ ðÀ ÈiDßB?ûó-Þ) œé dl¬Àp@eTÆ À‹-g¹½X¼ÂEÿé-,¶ʣØaÈÖ †å dKéZÀhÀ'ð6gó6k³€€Á¼‡v«þ=B\ò .ûñ’$fU¯-PÒs&¬«£š†¹åšc4ÿ²ny`Z_hyȬiNŽþAý€‹gAÂ)Ô^×Â)ìõ6ôµ_ÿõ6è5%PB ÄœŒ_ZÁØ.6qR/Pr¨Ý62•[Ÿh·¾Â"@§&¡ŽT¨áââMÞVm€hÔÀX÷®(,0Qõ‹BSp h@<Y¦>vÙ&ËO!YAo^éÒ€b«Ù%ÄäLæ¶r/÷K«ˆÎ+ѵØA PB-6vg·vgw-‚¸x‡wÈ@̘‚ˆ^ky€03ÇÞKwÞÁÙR1˜ê),œdOÒ,ò*¯_°,ÁNÓQ‰(¶w.7äîa nDAýí 1þÛ7„oxá¾ÂÐÔµuo·‰Ÿ8ŠŸxwCÁÀÈRgk8>Þ¼ñäA[ rìwHò†…ûx! z?6‡ëö.9’'¹ÃXï€xPBŠG¹”O¹-ÂH“<8ìîÁJ+÷-T@e+9ìê#™Ÿ¹?'ý€´ @Á“O9œÇ9`Û‚Ä”@DAȸOÂ÷ÐwÞ/‡{ÀX£yáš¹¡'zÈ>BÁy€ t]˹¤£¸-œB ÄEP x€÷Æî"A0ãv’º¢ú‘ŸúŠò¹ì"æxÀØÁ)L:­ƒ0P‚tðÀHW†O›À‹}Ðþ M/«÷ùbj ŽŸù-z±'z- áªzaîr‘óß+€Ì:­o·0Xza¨@K¨ÀMhœ×ýµn²6;a*æËŒy’ßÂXÑëJûu":¾oh£9)Xt¶o;`7Ba”@±Y\:Û#4|…"7»‡ì"A•Ut‰z˜“è¾æþÑîèûuF¼Ú’1Ù¾]ž<Ìmñ$´,¾XÕ\ᦹÝvtØÁv-¸A0‹ü´¯¨/À[@0õª¶Ïÿdh¼eð³ÿi?“Bíši˜é"U€üÜîï-ƒð~ViÜ%h, ƒ)ƒ)˜þÚ;9¼ýÛ;ÃÚk‚& ÂlAö¾ÁË콦!T= ¼ùÍ÷õ)ÄÁ\†|zŸ»‰²-’qËjÒØ;Ó&’‡y†n=ã'f×Ë¥1^å0ÑyÁ%L.´¡Ÿ¦¨ÇÆèã/îQ<ïé\ @‚)À=îç¾îï~îk‚!]y˜ÀÀü²jg\÷àÛB®«€ ð¤ê7ušc§ín€,5ñ7LK «{·ºÈûÿîß¿&¼uÃúT¸îðÔß•y:ÚFx´¿7ÿ%ó@±glÒ Ü¨&¸=ïÿ?@8`AƒM©9B‚Æ£[½ F”8±×Ã^‹p¨°Sþk[GA†H†‰(wHQ¤øTË[¾n]JiQeM›_ÞzµÓ#@{îŒññ€†(X¼Á±â +,Z1q)â­(Jhª|úæfW¯U=¤üªòÖÃW¤.=2´è5^¼X‰„† ˆÂ¸ÆÀ¸ü/ ”Óÿ¬°©GLp‚]Úà *7t!¦£Ä)@È0Þmƒ*x@"°˜øÀz0ß ¿â±Wä!÷ËŸ mîp"â£W¼bˆ<Üá [P ˜D$Þ¡OïºÞžˆ“ëá„J/ þÔ"ÞTàQDÀ< ŒqŒT0£0ƒÀ€$ð@„<FÁä‡<´£Wq²(Ä玽ˆ‚!ú¸CÎkOß{aµG¾?î¾xÅ% ™–Ÿ°2éÔ-01†"H`W Yˆ È@úâWÀ˜!ÐVGýQ”/$©x¨Ê½a¯*!Dôâ…EX§•½¬åŠ^+xE†‘%ó¨ãKæ½rTÊt¦÷¨8‘²Œ…EØ/Ÿ™Í[Â’òzé¦d³fÌ49ѹ¢ÝBL'#ÁÁ™ÎVÂ8X„<Å)Ïð™“DúL'&£ç b@þtæíæÏ1ÜÁ Öâgþ?êK_Åç¡#Ç"ÚËW\)Ý‹9šQP=2"í£«ìbOc™´—cð€ÄÒвô?$­MCÊ^âN:Å) _1ƒ"ÄSŸ­ù)bl™£~ P†pÍR³¹0”¥oT½’ÔÆ`Õy·˜ wæò—óE „Š4Y ¥za15©U¹z­2«9¹É#L ZN…¢øz…“MIoeyE‚Å*V. ÌñFqÌ>Þ‚­8ñ) CT²}|ƒâ:¢¹*Æyé4.õ‚>R$lgR )ñáØ,LjˆìBÌ)w`ËoÛÍf6(QÈŒÊþîX~ênõéXnͦ‚B[“Ñ’ö+l©ŠGáu‰Ï¶Áº7yE@ƒ+aÐËô“Ÿ~Å€ˆËÅ2¶©Fv¶&ŽÜî`-> “†ÀhóB4 @ÐÀ0‘„s`œ¼Áoàï ³•]•lª¼…¼`CxÁ…Á2!A²˜  Q6=¡XšP‚Ê ˆò2oÀ^N"ãš©Ëð&‘Wl‹ªÏ¼C4Ì·íŽÁÃÒÌÊŽóŠl`Qç$K㱂/{óǪ¤ ‹ùë ×¼Ö¶°Þ§\aË"Š@ð¶2P޲D8þ¼ç;Îí§·ØƒníË*íaLøUÏ€\¶H—ÁöPVpƒæ$˜3ßgªUþeÍnYK<›äZ*‡ÿÌCRÐB–g0…â ¼äåg?+ÏÊŽã•¢yÕ+¸›Ú6-@a"D"Îà ½ õ§¨N5†-ðeÂLÒÕæX°…-,ã„[Ò›!| ‘Wh ¥`K)Ð}bãÔÙÏÞÛ+p€¢FÔ…uÄöòœ2cgnz’× /ŒˆÛ¥¸ƒ ^ŠL$bÍÞ®»÷Öo C<¯ð›&׈"0ðÀU#²dLZŠÛL]®ž9 {þ–‰eR Ê{C]ÊÖmÉ\‚Y„jp¤rcB_1ŸëÍ­E“;˜`ÛP·£l’<‘EÐ`µ]yEd´‚-Xä$8ƒÂcäy´ÝV'ŸTªîö@{ÓñA :P‡:€àËyÀ‚0qoQ¶]î" ÏRšŒBO&лÞs †k‘ñP%|âÇ2ÌCý'x|„`!Ÿy EðÔ ø;^~ó71ÄY[OoýéO?4ø¼"à„P°¢¥§B!¾À5wÅЙ¬½JLèãëóYôøäþXoÇ?ž _ÐÅõ{o…BäB¹˜ƒž­,(lfÓøË ÍŸÑW\¡N(+àŠþ9è`ªúñÀç¥ðîëb8ÀÂ$®¯ûBaúzpàF@``v¬`¶ŠïáÒïÓ^“ *³i<à Xûrá?ÿt€âi>o œ '¡° ðú&ÁzAªtàý@0äomÎŽÊÏüò6XÍì EÉF`ûnÐu¡½oÊKNð÷&!ú¯_ðúæÀt`¸¯¯÷F€¡T¯øª¦ !âHÀ>HÁ °ÀÏа—a¸®ðs˜AnE,à@:@,Àÿ\ðYA¨À »ï¯/¯Jð®@" ë²ê Ñ,Æþ0÷„ѯð©à ”l L ¦àñ¦ ˜` ¦@ VÌt@öï¨@ ÷Ïópòd¼€A2z0öðÊÑé RpE+Á$@r à;@`Y ¤@@tAt€›Q- îþC¥LæödP€+)éNÖΟ‚ ÉÑ™L€Ï¿@ "@¶q ’ ¥ œà&Áûçûèò (’dÆ$Þ2#:¦ã 8²#=2 hà.À¨àB ZFžXÂò‘,*ÅÁç`€s¡@ ’ Y'rbr&¿pêq™¶ëkþ¼àǼE‰Æ†0db(_‹=„º<¹òÀ&¨0ÔÊŽÆ]ˆKÀ¤‹:.0ýn!Ž¢ »â ôÏRr ' R$ x;@€òä"r»°ÄBZÆ,W‘.Þ ¦ëb”MÌN¢` v­f¼*:¤£,$ò`üÇ~p€‰"Ð)k& lñ.QÜ’ …€j  @&E3{oð0Q›hb'ÒBxÎÇ$Þ zæëÎÖ, ”ï»nŒäôc%«%¶¯5 aJs9aQÄQ-2I0E%63ª,.3­  kõ5 ÷5¾Àè÷Uå÷o)˜"¢CïÒû| €sTåS _óPK w¢ÉMKt>>æòà)ê(î®À…ãê*àz’" j@ :8h 2*`SgÒ jmªE–'TnQPFz+8Ë"`]ׯoEä~YïP&€`5k ÄX·4%Cµø+@RQX«úÍfà4·ZŽÄ9`/ŠcÏ æÀ>™aLÕ`s€>v – ÏW&Ó ­€Y·ËÄ€ ¾î^þë î—©ödëh J׆ã ÄÜîФÄÖ&U4sA8¸C¥ v·±¶5Y¡ŒwÃø’ p™ *€oAÄŠš'˜’BO|Á¢à$0£@3~1” „3§x€ µH3XqÏ *'#€[3X¨X‚GËf€ žà * ‚}‰Â`@ ž`YEÄ *À8™¶Ø¢–ev‚˜Ç”C” ª9v>`¨ >™€’6ƒ@òu´,®¼ ëŠãž@ ÄŠ@Oiñ¸fô˜™qbÀW WdsR€´‘>ú.‰Ij8#êŠ@€¢d£«åe±XKw‘þ‚of šwò€;”¥ù_z¡çÊž!§3Ên8‡©àÚÀ *à ¬@ˆÚš;`ìób©àˆWo‚ÚjŸ`±÷ F ƒ*`Úà­·€Ì´c‘\… & Ee»™”ÇocH&ÊúFG¡ö}šÜ`&ç­û­¹ lv8H)»ÖÄx ÞÚ𨻖aú¡âÂm°+ôt€?PF`±U€¸à±e{¶i{¶y@Gº|€©¬ßg¬%L›«Æ@Ž5•u Rb»¶û¹ßÚ .ÀmWUüú¯K4lVæy¸Í+È©,î€buà(þ "à  {½Ÿ›ꓯ»5•K;´ùIŸ.á..Ù»_eL€·¿b  ì@€` Ž@8BÌã ¶€½Ÿ› xÀQоïs õàô‘»ŠuÌüoÁªºÛ™Þ€(A¾e‹Tà 9²Àê§à˜ž ¹g›  µAìBI s² ö˜ •ð>k2ì  ¦“¾Í |¢àðÂ~òÛ»C+4âž@¶£ròJ±!   ²àHj¾…r€ º RG ²À~H€“ðB÷®Ïb—v ¿@¯, ÞBÉ™©y|r™ ž€V¢×Ó(Áº#ÛT`HöþâÀÜ (= nÀȃ”`Ñ}` <  x ý&pYaÔZeT ÀTR©voÁ—Ggõ|™8«D¶€¼ L€J@¶¹ “ˆl|¶q a›âˆºö*`¸€ Ý`€$h˜‡ÊÛ.A¾€ Ø@ ¾ÀÙ‘ñ ù ž ¿u} Ü < Ü„~¶yÀ"`¶· Ú{ * „”@ ¨Šaˆõn!³ÆTÌɿœq°Ô¨Bqà" 5|€½#à*àÞgÛÜàÖÛáxà­9 žVÕ¥¶HÞ¤Yå›5Ì%ÀŒþÎ!þf`½;þãAþ±} Hþ¹MÀ Ä@»ÎݱûÕW$@Ù€ *ລ\î´Œ´E„N¶-ÀÒº~߇ޱ•à„¾¶M aíÛ€X¸åÅQxh[cg(”‚·¾å»‚T`L`³í€ ß¹9`çÏÞ±¹@n`Ñg{œ)¡’}áB7§lyÃÑjùÞ¸p¶OÓ_!ðþãiÜàç7; Ü@Ñk»ìk› üGœ{ T Ói{€à, ÙÛ`¡ ˆäJ~?üÕÀ—- Ê!b"‚ñøºÀ RÞ±M  !€ÿ­#@Ì^¶g€þhlk÷@òÛfàJ@ &¿jš2a†ƒs[¯†¾þcœœî¢—À <ˆ0¡Â… :$x«×;]|´¹xÑDJ¸`ÄXAX„gèa‡² ’·¨€…¤”8(aÙÆG—AVú… Q 8Š4©Ò¥L—þ| 5ªT¨‹=Šø“ÔÔ­\»zmx%Ð$Mܨù„¤aXzáA%;•8Û¸ìr#5󲌒唯gÀøA¢(Á¦Œ3% 9²äÉ”+[~„Ò ²$}ÈFÂo›mqF(A¥B…´uUláÑ_¬qn¹q GåþGV¬xÁ:бpÇ—‹?Ž<ùq¶8l©ëƒ&ÎN•xŽ“‹Îu¯ÃnƒC¤Žu¹Ìð@âÊÀ ÿX¹üùôëÛïu˃°^궉²Á •ž Øù‡`$¸1C0ÜHÜ]ôV2Ø1¶˜0|"u_ˆ"Ž8PD푘P~û™Á >ø0¡ØAž ”˜ ‚\¨ˆ da‡Øm¡,ª Æ)ƒ¡Ú7œh܇¢He•ÅAiåA·Xð &±…j¨&¬ÀƒQX F[+‚cŽuÁÐÅ)ˆ%ÀDP2H @pPÁ i=pN¾È'¥þ‡Y> ©W¤Ð`Á w`i¥/$|3 &TÁD[ÌPBÜP pö''Oð°Âž+àpÊ7ºvÑÈ)çüzJ n´ÓG|C¢Ê5 _¤Î>û“¡bðð€VÏÞ¢‚0¸¥Ä $Dà˜j¸A ” k¤»®B e*á†[˜® ç”+ƒ þªP&øpàG|£¦–1û´b‚ ;<_~0ÌðC¯@{ æ8ë ëz9ä”LÎ7Âü"Ì)”¼<ˆPÈð€ 3eÁ9µp@ƒ[ì ¶Ü°ñ² G1C¾Dð ALœtr¯2††8Œþí)&ÈÙÆ 7œÓE¨xnÌñwÇñ¹‚^°×xï)0l@åþ/ƒØÉ±àF ¢ºB°•á†,Xu¹+ˆÂ:'Rä60ƒ¸Á ¡âÖ☰”eWG´ ”“M0æ±enq/<âÎ Ä ¾ÑÑõö"<àÀ†à‡J@˜ LŽÐ€ H ¢c‚âÜ@€VxÃÆJF-½J’Á òæºÙ‚nF,‰q+Dau¶3eü x€µVÈ<+‰BÖW7@µ#¨y;ØA4 u/^!èf c` ®@=@á%v`ü,¨ PxÀØëÀ7’“ u/dlëx$&ƒ½#Ðk„ÜÁ6ƒ·ëãÅùÀþ+à‘<õ0 d "èÀÕâ&l sXÑö78ð Wb\*¥Á „m¹6=ZË÷Aqæú >„·¼å-fÌ€€Ì"*ðƒ³áIyÃ1)a³œËQ¸JAn¡!H`w¸C0!!l  ×jº ‚=çp è‚0²P³0á@ø¥W.aöéü ºqùËß^RXA¹0¬@.¡naG¡~{±ˆ ä@>(‚á=IJ†û$r„#È—+¯ÃZæ† ðÅ_„,ªsxáÝoŽŠÉ£vz¸ƒŠµ‘®ÄBg'¤5Ø€OB¥´ñþœêÅ%PKÐ6IçqôT‰½`vMw€‚ð4•á†>±+ *9wQò7"o0ø_µ¨¸\{’QˆyfvP”fXyB0m±u¨‰±õ>cÐGÀ‡’ñ_2p{“QŠOA E`=ŒÉ1 jÇbð‰Æ†ñ ¤@ ݈‹>ñêv»ÈW"Mw¼1xXb­3ñ–ˆ“Dd„¸7x€àoà ׯàÐ*4àþ‰ëx„3Åè tEà^p3ðÒ†ûh Ó¶7 Sw†††D×(5â–k0¿dyÀñÚ‚ >±x‹Ï& ±¹!†P7™X{pLø’IWskwN8ù@£à¶vç8 ŠÇ½p, ˜boó7½0X”ðx—±¹âÉMR9ž2“Cá…X ?40–ôÐ&p7[BŒ³÷6E ¾@Ð —±„…,ù0‰¯ðWpzÆ—8tÇTv?耰€@ ïÕxt8PEçä*7Àš”[ZÔ„h™©›aµ•…þ“S·@9PDa<ÊÅo©%& ”Ф°4ñU—°)dW”´e—»}‹4,b)ÛXE¢c0R R(5Pyt E4€C¹P 0NW(d‘?¶G¸™Úù¬‡C€@bÐãX‘±ˆª´–q pš6å5 `S· ,qŠ E' R 9P|Ã(, ië4›]A ú!Wêé¹I  q k”.+ГÓ~1¯xc»å~yqk(k+ÀjøaLÚ {‡Q×Z&*L Ð\j¢BPð¢þ[q `ø 06z£qr¶Kl@îù(˜ 23 Ž©.€HФ`)ù Yð¡a‡¡kî·ƒ¹ZVPt€ 5¥”pb*%Ê¥LP' 4 Ž †uéjoúV@EH Z%A`+ Ua )§ 0R<‡£wðwP48PB.Q·¢6Ç‹&' "ÀÕÚ BÄJjê’•9 7õ {^Çñ…Ò>Ô£‘B bÒ®\±& ÀR° Å­Và• q €žc`"¯àÖªŒq,v€ Q½qo`þt—‘¶°jªJ_ûE 80N3£ égZF¤0ð'¦ °wÀBÀ…W \,¡[ƒ¨¯p,]ЯòqÛ4ºn*IAP•;¸N¬z¤ xœj¢,9 ¦°³±dz•[©¯Pk*«ÉArïÓBë7é¯pÔço¨ &E¸}|æX+€¡Lk·* !˜¯W0k¤ kÊnö@…{)d‹—°&ccV`¶¢s&T€3P@Û¤ àŠñŠ´áP{·œ*0µÁpY‹AÊRm@²!¸æuÇq {þP`†`rŒk¸Ä:è>Ê¡0P„_ ¶Oq ^Ði¥ª›˜`p¬BÀÙK¦ÙÚ‘æ_[")À¤fb ö·µ{§x¦9вp&ðÒ{ >ˆ(º—A  K¡¸¸Z"£«%€pž¢\æëi‹ÀŸ›Û¨PôƒQ§˜ë;¸G0»·fyÃø©a‰¡B ªV@ 0Æ¿aŸ§ƒ<@“›—àYë|Û©À*w@¢L)j`­«KìûÁÄc"p¯§Ë¥fiú++L‹€ê¢.>жQyQ k{P4_õ‚9ü,5›þØÆÂµwŵ EìŸu«Äœª¡ ÅÑ~‹{?à?` Æ`L: @ëJÇXQ»7À–A{mÌ©9ð¬>aqü º¼ýèÇxRЛy†i§I«n2àÄ”G, ¿vK¦  @™`!É^á¬s3p¨—Ì1 1ìI‘c ™4p‚·Áë«/Éò¾—oP Ï Í`cðiu6Ë]q –'*ßÂkº,2 ë.²wt& ²[WP,à±C¬/PÈö±X§wÍÂ'VPÊ¡ÎñwÀÅÂ3 Ž"t—°LÀy‘{¶·þ¹Ÿ‘Єᜟ6Á_FŽ•«ciK¯`6£kŠK"8 ¼Fx¨^`mt¹%ê‚«q Q0NçÑS2®e4‹f³Nš{%©yÅ“ :GR*‹ÀW€Eà­.臔%b7›€`¯y«·€ pÀSq::È”JÑ6ÄP ±0ð¿áH‘»Ï%²o|Ý,5çqÐÎ Á"Ã>åô0`±lz˜r  t'ÒË"@—ñ®4ðp!cðùëVP´7¦(4UÑ_qÑg oÀ4r¨sP¸—\°ÙàK:Và¾Oðs*‡_`Á—ñð¡¹G”þa4°h%ʰ8Тڱ\A AP6Ý%1’mC— Üê¤p>°m ËÜ#ò ÙÆ{ƱP„0 Ú‘‘z{`Ù­ƒtx%du °­½õ J€²b* Äz¿9 —`×'ÓÝô.e‚Ó ±•âQð8Ú±P–íwðªæÍpÚq'gOðw­µ^á ¤àÏùüJ@~c? Ä—q y"˜`2ËÈúÆ¥S[:(Гêã_Ûqà9NWMŒ"þõëÒ.{Â'DÇàû÷"Q ¸~è.­BÎ’1á&Þ.øt5ØË´-[þ"`>¡{pš‹ÐN)Âà ã:Ç1í`9.ç³U®ã bu ˜ð }žæí¶S«½vzØÙ‘áãt€šýM"h;E@)À«@é€^Œ]Ð4à3`À­}LkÎ>Pqm€ãsþ8–âÍø!ÝÚ}k ๭¬8 ×ctŽ˜ÂîV‹ 6¶¼·j%µÅ.>°ë¾&7r®¾Õ­À×-âpÖ·ð €Àš.±ÀÚßË!R[XÏe+0œî-k=êBÆÞ<-‡OÔâ"€31ójÞ$êÈÝ@¢“Ž~.¡T3j`Õ þì ¼oá.ã íXI «ÁðlðAæíQй+䡲N)Üx/â‘pî¢3Çsüvxæ.‚~c_1·Ë—·°VÀ"C‘;;-~èXò QŠž'‡?P(ß (—¨%O ‡pæ ñµn6ÜÊjÐï7u †é+ õ8pÖrñ‹t {þ ‰|Aqj6¾µÅ`jïöo÷ ÷uo÷wŸ7l÷{Ï÷}ï÷ø/øƒOø…oø‡ø‰¯ø‹Ïøïøù‘/ù“Où•où—ù™¯ù›ÏùïùŸú¡/ú£Oú¥oú§ú©¯ú«Ïú­ïú¯û±þ/û³Oûµoû·û¹¯û»Ïû½ïû¿üÁ/üÃOüÅoüÇüɯü„¯FÁ «2K° m¿üÛOùÏ ɰ@÷ÝÄÆÌRi° ò  ý€}å¿°ú ÿû @›$XÐ É“·éA€ƒ%N¤XÑâEŒ5näØÑãG!'À³êC D ˆ”È@YŠ•—žX @ÀIËÄ„.9æ A‚K6ÅäÈ@[ <`ÊÒêU¬YµnåÚ5cO0 T°d™¼X) @‘Jž6:+{V©Ì̲þgÓXPél¸e›tHsÖX‰=ûH"Ýd‚É,x¬AˆêÔ}Ñv@Ì‹VmÄÈ9` €ðÙU¾–w€ÓHÚø¤¢…Áuay±g·íÕøqäÉ•‡ PO1"TjŽ–B‘c„Ц倂$xr’öá@Ô­cÀeŠ;E€Ã@‡Ëùr³“·$ÄêI ±€JêÎ’J'‰ÈX%õ®Ë."ŪҬ¼›r `¾úîs/ˆºI‹ ?È0 鳿åZtñE½&&£e‡zZc§”Ù!²M€5ˆtr'yôÑ¿—bÀpzþHÀ€üôsê'÷´I‚Ê-:0A3›Ï$ùZÒ ýÖàl§8YƒÊ {2Ê)ÅblX…$0Ú|3€8?˜SJ*c4ôPD­ˆÊX" XfÒIµ$©ªÆ#P ¾(­1÷šœ®€!Üò¦ˆzÜô?PÛü©"Í(©¥$õTˈÚÄt ý´ ’ ÕTµîÌsO2ÖXÀ§a‹=VžS«RôZl³M.ˆÝáÙŸb È5ê’Sp‡,è=¦½ik9Ul'Ç>àÒ§ŠørV¥2»5W|ãÕ/2YòÎLj䩤nÁ¹À†MhVG„íu—am3Öxãþp¢‡¨– /©XÌkd[Óñîä{·ˆd“AHU`5ˆ!‰;Hß%ª;‘M|aÝlV26Aè_˜Ï“ù „–¡© H*É@0›q.:eõT5¦®úê¬s.—c´ÓVXŒpKž©T ‰³²9Ä}mpK›50à\²è–Çnfµá,ËBx[¸ˆšKã™àêC2h‡î¬_[q`îºï–(€*aÈ!h3I,ÅŠÍ=<õŒLM ?=€Ôƒ“mmÜsçX¬Õ Ü)ª jµ•|/ˆ÷*[kíw±ªD“Ûæ9uþóü|çV\à§GS€ß¥gt¬æ €![ë{þé£×}}ö5¾}øµ‡~úë·ÿ~üó×þû÷ÿP€$` x@Ú/6G8À C!  l 4|“@ f°~HÀhAC à J–¢A¦PwX€' ¨€!È‚›PËŒp>D"-+™0’qˆ …;XÉ 0HC@Ø@XÓBªPŠSÌØR˜'Ã:6ˆ…lX f&±À€Ò¯˜S´ë¢p@OMÈ“ºHE<æ1QP€…ÕGOì`&K¡‚ ˆu«< 9†Å f@…,dQ:º‘™Ôc'=©ÈŠþ–¬d1v0€&0NB€$ÄBbÀ¤•IX@Z@80€j$P2p }R˜Ãåx"‰­„56Te!…HÿPÍÌÔ“Nf)€,B€ŽH#ÎFLp†#  ƒyl LÓ›0Y ¤fUà—^4Íø 0@‘ÃJÙ‡V‚±"bÄyP„RDpE8hñªÍ•*´€ 3©@fá&ng€C–§xH¤Æ*^°Ãˆd±p eiKã"O Ë{žú~—)äqK"œtiO}úS U¨C%jQzT¤&U©KÈVj•G2Â#Q=UYbQ¦ uþõH áC˜ÔðDX&"€Q~´£ !ˆVµ6.áP哲¡&™¤µP™I[¸Š™Ä3«@ÝꔺJ {$ =€$¿î£ ¡a¥çËdĦ»Á«FÞ‡À«0 RàÉÓÁ‚³°Uéჸ¹&ø0*Â! Tx“B•—ïð‚Hƒk€È> Á6£Ú$ˆq‘KG^šA ØÓZ q¶Ú%¢s“ ÃŽž¦3ÁggÖ™jÄb4 ¯{hÁYÐ!«¨xgxšÖn7µ\mëYÆ3^ôõ¡~žH‹¯J䑱ès’âìfTÂþêç0zA`Uxg¿ÌÀ&Œ°8¬aŒ0£`Ò¸ÆÜQ_§Ž ´¨.>•i£M¤±ƒÏÅ[ì"´8Ú͆gÇ0îb! Ìý°¿³¦"ñƒÑ 7±œœ[$±ŠÑÀ ŒM›IÚQ>|ÐJ¾ü¹_"± @ˆ¹£Û€CP¿t$$ý+X?Ê%g¥ñ¬Ö !Œæˆ·zÞb‘§M<²Oƒäh lÙ$pÉ}t%·K!É áX<˜àŠH¸\ŒA*ü¤¶&N)x@¤cÞ䢉Î6è© PiXÊ™µLp…G‰[Y˜’ŽäMP*Åò˦‚”Á.tþ#Q°‰4‚á×¥$" 7ÝéH+ÙÅ~s4‹$8mf¶¼H®ÓëéÀ“3¬Q^ñ–;%¦ö›À¢3·p¹†n»GšÔ,/BÔ-Yꔦ3.¯EyI&d ¼[´#6º†€+“)< w¶µ]”ÊšÖžgD‘R€³nŠ%@bì` !ÅHB°ñì„‹ÞÙr­µÀC7‘—ȽE»ñòꓟŸùJÂØNžK„Ø~nf mHßFÜÇh¸–Á Z¨7ÀXû¸¥jqÿ-ЯoÚhGo—ã&ƒËJ%‹„šFIc0à:´oCouVHšó~—u/þobœqt† é·‡3x¼H”¢26ÈÒa½dgÔ˜¼!ðlnös¼<Ó¾vÈ‚ý~ìÆ^ô–ް»z9½ ‘ŽÙµå ŸMχûëñ^¦YëÓ7û™J\}¯?’êÿ|èG_úÓ§~õ­}ìg_ûÛ÷ÈoƒKQæÎ]†Œxˆ÷…Ûš?²&»ç~ûUò<‰ÍDÁª FŒ« ñŸ¿“P‰U›7÷Àk9ŸÌ¨§ 5 X›»ž12¬ ¨ $@ T”¨úIˆ¹Té¹nZ «Ó§»@Ì@2úÞZ'}Ú:RÁtÒb€ê¨ÀÌþAåHAã’Jƒ`DD•V‚Á!ÄA\®HAr;ª„¡,ÊW„¤ªˆ@ TÈœÀ†$ɹéx56ºC4¡ªB‚Cìz-g4É» |À'YŠÖpȯDˆW;ˆ­¤@§äÊ ÄJ¨´Ù(œI4 °$Y†œ`¿þÁj%ÜA80ˆ` ®!ä Z‰¨Š8$“ÜFÁÂ%Ê,ŸËÌÌŠt™Î5ýM‚°LÀÌÁD8ð„>¸€à-X}r¼‚L/‚$ù¯{*ždËÉ”‚ÈÀ™HYð:ÀÛ”„Ü´Dï˜Ú,Áí@Ø”ÍÃ:„]*G4dIJë§êó#¼1éù¦€Ê€|€û[5è¢3‹pÂtzOƒˆOSœÏúìÆ‚˜%ø„Jt€$Ì @ƒ>€¹üˆMôìO›%MJ¤ß܉ä<žø&ý¤£€F€¥˜k¾ãÉ€¼ÁÍC‚úKŒ‚Ò’‹Jþ³1ƒÇÍÉ¥Õ >žøÁ BèŠÍZ´MΫb΂è6XA¤÷O[,Ò#m"×<!8I@ÂìÞlMc"<\µ‘1}Ò¦Vz¥Kƒ‰µÌO&ý@ÞQ).ƒøÒ×ÒÅ,=ϹSmœ+¥—p@„‘É5x€È€4|:m¼Ië2)0lJ‘O¢áD3H=É'|T ›I­“JÝ™GbÆæ’Í+l«»Â›nqˆŒJT`u)hÒ7{—ûEHuBÖÇF ®XU4a,\b5U8“ˆK3EÀ<€!È9?‚„Á.L=°KA1Ä¿Ù\Nð9!þ5¬° @…d•;Ä3JôBË"Mæ Šo]‰pU€q-W4AWoÅÌ3ÜSêra팉¤ …Hø#:З-ó›m-Í2üBè p ¨„äb€ù×–HC<äØY$?qÉW0cÉ%A{•$œcÊ>’ÇàO‘  Z¿ °­áBÑ0NjC zÃâÙÐæ’Ùj„‰šÕ;œµS­ZêFÕ¨&øI`IcüLA(²P<–®¢€YM²Ã’YY€Š˜0€Y„Ú=ˆ=ÅÓžƒƒRäŒPZ Ã)±"00Ïií«1Ë,ÆL­ïù¬‰P˜ðQ,QýA4>þ‹\›Rº˜DÜ+ˆB,Ÿé°D›œ@œ©ß3\,ž™ \Ô*Dšj>HÛä«=vÛ™Z\H@‚MýkýgÕ­Õ5ŽÈUCž W­]Þõˆàë]à ^á^â-^ã=^äM^å]^æm^ç}^è^é^ê­^ë½^ìÍ^íÝ^îí^ïý^ð _ñ_ò-_ó=_ôMß®öm_÷}ß0P_ô+úõ+ùÝ/:àƒüÝ_ýí_þˆú àp¸ßÁ ®ßé9`ú%àÁR`û=n+Ϊ¦ ®àžà¥Âà 6Öà Æà ®`V*}8aNaÖ‡ˆXaFáNþª~ážáŽa¤²á>Vaö$b!b"vîá>b$þáNBbæa'fa(vb&Ö£(–bƒ¸b%îá*Î#-žâ%c.îb*úâ,Žâ-Öa2.c4c5vc^ã)2ã‚ c‚°ãàDà=æã=îc@>Zä?.d?æcA–ãÁc`d`d¸bç‰ä(ŽÝì­T¨LÖäLÞdOî[äpdQnã3vâé‘äò¥d*Ö–Q^åRãWãX~ãòuäCieX®cRÖeWæå\_[6\–e_&æ;ÞåcîåñÝfnfg~æ}Ø hžæfŽj¦fk¾fhþÎfmvfnîffþfp>_pöf­(çj>tgu^gqîæwÖæx¾frvçs¶gƒXçhngtžglæçröçi®ç~>ˆ8h„Nh…nÑgÞf€爆牖犦góuè‹þç|Æç‚ÐèŽ.èèè’†èòéöh‚Pé–fép阆i隦iðµiÐižÆéžé•ê—jñýi’i‰FjŠVj‹fjŒvjŽê£&_£Nj©¶ê¡žê¬Æê™&ê›öêÆéï­ê¥¾ê²Þê³îj­Vk®þêµvë¶ÎiŸžk°&ë¦6ë»Fë¼fë´N^NøkÀlÁæþîj“6l”Æë§Vì¨ÖëÅvìÆæë½†ë¾>ÞÐçÂfìÃÖìĆìÍöìΖìÇíȦìÉk°6ÞË^çé±ëÑ6í×Fí·–í¸víÒ¦íÊ>^}nmºžmÛþlÒnؾíßíëëäNnå^î~ˆæ~îävnè~néžîå®nëŽîƒÈîëÞnîÖnƒønð.ñFnìæîóÎîö+ïæönñNoë†ïé–oè¦oêvïï¶oæÖïîïòæoå^ïÿÆoô&põ6ðøFðùVðúfðûöï÷vðý–ðþ&ïß>öðñ&ˆ §ð÷𠈇ðüqó6ñö&ñWñÇþð ·ðgñ—ñ§ñ·ñ‡ñÇñ çñ çðò×¾×ñ7òGòWògòwòò‡ò§òñ Ǿ"—ò#çò$÷ò%ó&ó''ó(Çò!7ó*Wó+ˆ-×ò,wó8€7—ó4Gó)gó÷ñ=·ò>×ó·ó<Çó.‡ó;ôBGô/'ôEWô0gôGwô1‡ôI—ô2§ôK·ô3?î9¯s:ïtP?ôOuO/õPtM_sCGõQguS'õSOôV—õWwõXotë«uZ¿õHŸu\×õ_çõJ÷õ^÷ôêöböL'öaGöfWöMgöe7vêsöiþ‡vUOõ6—öh·önÇöm§öéóölçör'÷p÷?Çôo‡uwgõêóyŸwz¯wˆ{Ï÷yÇw}Ïw~ï÷zÿw€ß÷ƒø€/xƒ'xƒHx…/†—w7øˆøö{ø{Gx†Ÿx€×ø~çx}÷xÇø„y{'ùƒ_ø‡7yz¯ø”y‰wyЇù—ùާù·ùGùŒÇù’çù“wø–ß>‹Wù†'ˆ¡÷ù•Gú¢ˆ£×ù‘Wzˆ‡ú‹wú—§ú˜ú ú·ú™çúš÷ú›ûœ×ú§ûž7ûŸ7ú¬Wû­'¦&hxû¸‡{¹¯ûé©{ºÏû¹ûéiz²¯úþ¿¿úÀïúÁÿúÂûÃ{¶/ûÄ?ûÆO{¦_{a²ø¾øÊoûÅüÌüÍ'üÎ7üÏGüÐWüÈÇüÒgüÑwüÔ‡üOòûÕOz´‡ýÇ—ý×_zpýÓ×üÜçüÝ÷üÞýßýà'ýÛ—üÖ7~Àýâ7ýåGýáWýçgýäG~åŸ~æ·~ço~Ý×~Þç~ß?þë¯~ñ§~òÿòÏ~ìßþôïþõÿþö~ï‡Oó¯ô§ÿû?õÇÿý×ö¯~€ p Á‚l¦H!Ã…&8ØpâC‡V¤ñ ¿Ž?‚ô‡0$I#K’<‰¤Ê•&9ºüØ2æL—5þWÞD™³äΔ0cvD(t(Ñ€¾,x©HƒK‘ö •åO SeV¥™ÕæVœ]u~åÖ§A¦MË2½š´(Û¶fÕ+U.U´PébµkïZ‚oùÆÕ«U0WÂ^ ƒE,V1Ù¶Ž‡þe¾ÿNü€aÜÈkëåÍœèà…º/ר¼qÞén£WÆ^ÝðõîàÃþ³”-¾¼yœäÏ«_Ͼ½{¨.Pˆ ?¾ùô)Ä×þ¾¿ù}(à€ûøgày& w6èàƒ†„ÄUüPÅH ÑD†Z8Äs†ˆYò£`"¦Ø—,¶èâ‹f€xb‚*ÖX׌"„ã€6öÈÖŽê$€ bä‘H&)ˆLÂ4$‘=‰¢AR6i%JUFù¤C2xå—eIå–H½æ™‰Yšy¹špRÄæ@sJäfœÕõ£çž|öÙB~º'ž„Ê$¨ €êg¡Œª¤è¢=Úg£”š$)Ÿ‰^úg¥œ†¤©ž™^Úi§MüÐÄN Qê©N°ªj¨þ’Âú訜~º©A¶Êª(­!¦áë¯À›†üý“k¤Ÿêz(¯`k±Æ&‹¬¦Ê"Êìnxd«í¶Üâ‘ÀsÇâ*­¸ÔN+굯nAëÔî@ï /ºÍkï¸ìâ뮾ôzvo¹äžp¬æܯhÿ œ/À +¼/Ã×˯¼G 1¼Ï1eþtìñÇ ûƒPÈ${ˆø€z´é5Oé¹µë‰T7`µCÛ¯sëN(›1m‹¸w ß|¸ñãÈ“+_μ¹óçi¹¾„N}¸tŽÕ³¿^\»÷ËÜ»þ8<ÂðâÉ«k¾vûõðËš™O¿¾}3_ ÞßO?üÿ( @ï h [¨ OèÄɃF(!',h!Oè÷›C^èáV ´aC#~h¢J!VTâ‰,¶èâ[ ¾(cO1ÎhcM5Þ¨#L9îè£PÜý(äJ=iäEE©¤DI.édCxD)å”Tâ‘€Ofé–ZvIPŠ^†yP†bî&g–é"™¥©¦\9Ƥ{M¾ ™œ×7§Aôé矀Rž–á)žyJž¡\!ô /˜ð褒B © (:£LiúbM𪨡ŽjêŠÿ˜Zêª¤ŠŠª§ >¾ú¬°!ù ÿ, z6þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ØÈ±£GCŠI²¤É“(Sª4ù±eÇ•0cÊœI³¦Í›8sêÜɳ§ÏŸ/D¸@A(Q£E‡RÊ´©Ó§PGÐGµªÕ«ú¢jÝʵëS¬`«zK¶¬Ù“aÞ]˶­ÛƒiÁ"téò­Ý»xWÆÅ:—îǼ€ ¦¸÷j_¿/+^Ìx`a«‡olL¹2ÞÇUA䃈gÏ?‹>`¹´i²š¦hb€Ó°c¿]=ñµìÛ¸sëÞÍ»·ïßÀƒ N¼¸ñãÈ“+_Îìàeþ#RåG•!H4IOÞüê½Ëß¾~éÞùÍÿ~p¿|û>dÆ€h ðýäŸw´ ´`w (!Ar7á…¾UH†榡v†È'$–h≜Ð߇'I摈0F„߇*ô!$¹øbŒ>@þ(d 4£†¶ÅÇbIéäBÉ#vGVˆÐIR¸$IY2á”Öõ#æ˜d–ÙBf¦9&šj¦ æ› Q çœtÖ¹ÔAmº‰gže²ÉçšpJПdúI¨¡"ʧ ¹=ƒ£¶ð¨¤-t)P¤“f ©¥ÿ ¨AžŠ©hž£¶Éèm„Ú¥©†Ê©ªî¥ª«ž †zêm°îéé¬fòÚ§®‡›è­±åZë®Â.š,©ËšÚ¬¬ÄÂflAÓjë³´R{í±ÁFkZµ+¸n«-²Üëmiä¶k®µèžÛ­¼N¶neîÆ ï¼ûªK¯²}yonùòî»ë›°Á W°¿ýü/³éJÜðp[¼0Äk<.—1Å;[1É£2pþ´ìòË0ûƒPÌ4»^xP–-^é%?n‰¤c¦aæ™h¦™†~é%—8Š©¤@m2T§›BÂy”œaâéç`cê™ÓŸ„N$hT|jYè¢ê蜌Fzãž`*ZP5˜fªé¦Õ(é§EBJ&¤w*ä)¨¨&UjB«¦ê*U­¾êf@!ù ÿ, a,þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹3.À±£Ç4ŠI²¤É“(Sª< ²eÇ•0cÊœI³¦ÍŠ.]ÞÜɳ§ÏŸ@æl´¨Ñ£H“B䃈S§MŸJ= ´ªÕ«Xg€ «×¯`%–`ײhÓV R­Û·pƒ. ¶¬.ݹvónË·¯ß¿€ L¸°áÈ+^̸±c’l?>žL9hä—•3k®‰dH•U† AÒd´gÐCön^ͺäY‚üZËžM»¶íÛ¸sëÞÍ»·ïßW/cNœ²ðáÅ“3ð ZóçΣCW­¼:â× CZßó8÷ï…½™ƒX<ùóqÍ£_ŸV=û÷3Ý”¿~JúÿðÛßOð8Gƒø¥!à€˜†Øñ§`~þD_‚ A¸ øé7á…U(†Z¤áe†(ч‘‰H!}$²e¢}þiWPŠc­¸ŒC9Ø ŒïјBôèã@Rà"Žè騑Hô /˜°ä“N2ɤ å“SbÙd•I"!ù ÿ, z5þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ hÜȱ#Œ CŠI²¤É“(Sª<è±%Ç•0cÊœI³¦Í›]èÜɳ§ œ@ƒ J´¨Ñ£H“*]Ê´©Ó§P£JJµªÕ«X³jÝʵ«×¯`ÊK¶¬Ù³hÓª]˶­Û·pãÊåê²nØeÁ–éÍ‹w¯_sc­ÛR¬¾Ãˆ+Ö'¸±ãÇ#KžL¹²å˘3kÞ̹³çÏ C‹Mº´éÓ¨S«^ͺµë×°cËžM»öTÂ…mëöŠÛãîßE{¿N+…ãÈ“+§P¼¹óçУKŸN½ºõëØ³‡ÞQ»÷•܇¦2üGòèKšOÏ^äúöð-¾O?bµûøóë¯& ¾ÿ†@Ôß²ÍWø‚b†à@ aD 6hÙ„†g¡Zæw`„r‡Ð 0¼`‰(žXb‰&$°!Z! ‡PŒ½½x¸Í¨áAM4ÁC?úäÚŽ„é(âEEùPF)å”df•XFieAYf¹¥“§}9‘˜‘ &m!ù ÿ, z:þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ØÈ±£GCŠI²¤É“(Sªìøòè³²†±¾} öï[ÈMOÿéü†÷ëëßÏ¿¿ÿÿ(à€hà&¨à‚ zö[ƒ2”›nVh…Z¨¡Enèᇠ>¢…çXŸˆ•hbz(¤âŠvµ(Œ¾½8#hÙ£Z4ö¨cšåˆãŽwýhäá!'J2¹d“Pß?P>i¥“LJId]GyŽ@¤%Aa>4æ–j5£ˆšl®éf› Ôæœoº§AtæiçESFÔ'šaýù Z’¡ ! (V!ù ÿ, b9þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ØÈ±£GCŠI²¤É“(Sª<ø±eÇ•0cÊœI³¦M‹.rêÜÉÓÅÍŸ@ƒ J´¨Ñ£H“*]Ê´©Ó§P£JJµêM—X­jÝʵ)V—]Êk“">fÑžMËö€É¯-ÉÊ»ÔÄ%áÆ¥Ë·¯T¼yõ¾ôK¸°a‚9^ÌXî²`Ë"C~,¹²ÝƘ3kÞ̹³çÏ2)\ a´h £KS ´ë×1÷ÉžM»ö>ظ·ØÍ»·ï—IÚ>;·qÂĉŸL>ü¸sºÌm/N!’!U~T‚I“îØµþ þ¼|PêÕM¢/n0€m~ÉÍË¿h¦¾ýûøÍ¸^öôõõ·Hó‘€ö÷z6(‚AX’„V¸ u ¨ž‚v8!‡Q8’ˆ–h‰¡˜‘Š&ø 4/ÆãŒ2î—"ˆ±x‘Ž-šçŽ©ÈcECöø\?H&©ä’ý Ää“I: å“RN¹¤‘®¦å–\®V¥•Qæ•bަAfž‰eƒi"ùå˜o‚§•sN¹¦f»Áçž-èÙg ­Ôf“e¦Y'”‡RY¨™Þ¹Xƒ:Р‰2Y)™h¶ÉD: U?41„C€*ª§–z©’«ª)¨¦þ‹Â«œž2Fé¬tâj§®ˆòªh¦†ÖºØ­ÀšÙª›¾Zš,¦+laξl±²RK«µ¹b»ë³Ðªm¯ßþÚ¬·ãN[®±Üvk®´è†«¬»Ì²[í¹ó¦ËW´“’+ïµôò»o¶ýl/]ø Tð?'¬o¾ë,— ¼°Ä#<±Å;Äí¼­Çà‚,î¿kL–?(§¬òÊþ ÄòË)» óË2ϼrÍ6Çl2n9ß|PÏ*ãܳÐ9móÎ<²Ñ33 ³Ó4ÿ¬tËH¿65Õ] 5Ë[ûœõÔU[ ¶ÔJw4Ù@›­sØŸiöÐo÷Ñs7ÍvÛc]v`ÝOóµÞißí™Û€Ã]¸Ü‡Ó¸Ý‚sFxAù@“ TyãŒ]®yÞO½Ñâ0Žyf›ï úßuÊ詾ë£gî¹ß,»þÏÕ¶szQî±ûÅ»C¿k4Sð«÷ÞX@!ù ÿ, äyYþÿ H° Áƒ*\Ȱ¡Ãƒl„›ø/ÉË3jÜȱ£Ç CŠI²¤BF698p É—0cÊœI³¦Í›#n2ÀΟ@ƒ J´¨C”Édy¢ã €@I‡ôùwÀ©Ñ«X³jÝzUC ->Mõ)ŽÀ\Óª]˶íEˆxÌ@0’uË·¯ß¿? Ä1^̸±ã z\p ¡#W¹|̹³gÎÌ '«Ì |^ͺ5Û˜1 vM»¶íÛ¸sëÞÍ»·ïßÀƒ N¼¸ñãÈ“+_μ¹óçУKŸN½ºõëØ³kßν»÷ïàÃþ‹O¾¼ùóèÓ«_Ͼ½û÷ðãËŸO¿¾ýûøóëßÏ¿¿ÿÿ(à€hà&¨à‚ 6èàƒF(á„Vhá…f¨á†vèᇠ†(âˆ$–hb}| ÂGŠ+ªÈâ‹hmõ¢‹4¶¸bŒ'úf€><öèãúX•ÕŽ@É£9öf¤‘Hbµd‘M&©Û“@¦EåRþv¥VnÉã^-Ì2dŽ)f™h¢–e_^òH[›ú¬ù×>tÖiçûÈé×`|öÙ§kxZ§ž}ùiè`´ *(¡ê¨IŽš¨¢x‚É(B‘úùR¦^ú§|n êlЍAH QÅU ÷M¼ª*«C¨é)W¦Z*R®¥òjPxò£è­1åªë?¾–dƲÌ6묶”lA”VJ¬¨£bjjZÓ^›U·ÒnË• ä–kx‹«¸«.wî"Ëî»ÞÅ/½ÀÛk¶û‚НqúöË©¶üþKܽSgï¼ c·pÁ çëîÃþF,ñÄ ‡ ±ÅQ<°À™"ÔÄM áÄ#—ì„Ê(s̘Ç!ƒ©Ë¿Á<³ÌÒÜ›Í9·›±Î·ñ|(Á­›Ð’â<´ÑGÿ,ïÆÍtÐN#­éÔ¸YÝ©AÕtíõ×`W£Ö¶iê¯MöxQ*ÔöÚî½ 7g!ù ÿ, y5þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ EB¤‘ãÆŽ `I²¤É“(Sª\ɲeAábÊœI3œ—8sêÜɳ§ÏŸ5ƒÊ¼ù³¨Ñ£H“*E)T(Ñ¥P£JJ§¾«X³jÕ÷´ª×¯`ÊÝJ«Ø³hÓªåY¶ìÚ·pãÊ…Ø–,BxóêÝ `®ß¿€}ÖÝz—¯a¼+^\r°V„H"KžL äʘ#3ÞÌ9®ã¬*?›íLº4XÑWC£Fˆ¦µë×°Ñ0M»vãÕ)QëC¨{¶íßÀ#êî‹røAÝÁ“+OHÜaó’ÏFˆ|¹õë§óÔ^»÷ïcqþƒO>ªñòèÓ«_Ͼ½û÷ðãËŸO¿¾×øóëßߢ«ýÿôí#à€¸€Ögà‚j—àƒèMw „ÆuØ…fá…|ièáxvxPˆ{}hâ_$êUXŠžè¢X,"v'4ÖhãœðâŽiŨoéÈã`=‘H©d’þýdCO)%TQ¶Tå”ôù¸bŠ[’ˆå—&i9bŒ]†æ™‰i𱉿›Ù‘9&‹áƒx¶p§ž-\ çŸer(†ù‰¡€jèæ@‹ Ôh¢2*çš“¶Yi¤˜:z©¤tÎÉe¦ þó訛† (©„Ǫ¬¶ê*q 4iꛨ~úÒ¬¸*ô('ÐШ'Ïpòk°Â.ɹ&ÛP£@„¨²Dú¨]£ÏT-´/J;m©×bK^­^Rú¨·C‚kæA¤«îºìRÐ"¹;š+(¼ð¾Ã &Ø«o¾÷Þk‚Hí«¯¿ã 0½Ø!ù ÿ, äz`þÿ H° Á‚4·€ƒ#JœH±¢Å‹3jÜȱ£Ç CŠYÀ#²:$ɲ¥Ë—0cÊœI³æA $µ(Öp¥ÍŸ@ƒ J´(I0 Öó¡Ñ§P£JJU£C xú¬Êµ«×¯`I^ °´iسhÓª{uÓ­kãÊK÷%€Ê&0P·¯ß¿€'.®0¸|+^Ì8íU‡HKžL¹²å˘3kÞ̹³çÏ C‹Mº´éÓ¨S«^ͺµë×°cËžM»¶íÛ¸sëÞÍ»·ïßÀƒ N¼¸ñãÈ“+_μ¹óçУKŸN½ºõëØ³kßν»÷ïàÃþ‹O¾¼yštø¤_¯¾=ûóð[(L¿¾ýÂâëø¸¿¸Ý' }ûé÷ßW4à€ˆà .x_ƒñ=¡ANHá†9U> †(âˆúphbE$¦â‰,F¤¢Š-Æøá‹$Êhã?4ÖxcŒ9ŽØ¡…í#A É"H&©ä’häg‘E Ÿ“A  ”€Yyå_| ÂG—_z æ˜`¹˜–[ö•˜ET𙥖¡µé¦_pÎYÝ2Á,£gžxîéçšõé§ |æ ¨Ðõ("¢Œ6êè£F*餔Vj饘fªé¦œ¶&ȧ †*ª j·Ï©¨¦ªê>¥f·ê«á¨zØêt²RT묹¡ù$F·âj›®õêë°Äkì±É‹ìrh »lqu>ˬ•Ò2mµÈ]‹­kÍ>Ií¶¯)[%”àb&î@ÚÄ 4œp¢'ϸ o»Š,ÉåVÖ­·äTæEræ{æ¹ü}[PÀ!,ð›KäA /Wº÷+qcÿ“1wìñÇ\,YÆ‹lÁSœñ 0¼`Ë0¿ÜrË&$`2U$7LPÉ7Ï•³ÅÙ³b??|ÏC¯EÆÒL7í4‡ ôôÔLGýÕT[ôZZ_ÔµE_oÍX@!ù ÿ, z3þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹32À±£Ç5ŠI²¤É“(Sª\I¤K,cÊœI³¦Í›,ù ⣓çΞ@|I§Ñ£H“*]jbƒD‹.JµªÕ«&Ÿ¬„ªW¯]¿Šmе¬Ù³hÓ $«¶­Û¶Q]ûñ­Ý»xíÒ…9w/€¼€ L¸°áÈ+^̸±ãÇ#KžL¹²å˘3kÞ̹³çÏ C‹Mº´éÓ¨S«^ͺµë×°cËžM»¶íÛ¸sëÞÍ»·ïßÀƒ N¼¸ñãÈ“+_μ¹óçУKŸN½ºõëØ³kßν»÷ï©ý¢†O^ ø¿}÷–/aûõÈÏ£/øªxøñï§§ë^?þâõÑç_AM4ÁCˆà‚Zý7\€A8QƒÂ%á?òíÇŸAdtèᇠ’ÁA!–èáˆ:–¡}´bA(JcŠ]ˆa‹á!':ò¸c@@!¼Ùx¡P IäoÍ(Òä“NF eAi¥”QRiÑKæÆeD_¢f—µ!ù ÿ, bCþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ØÈ±£GCŠI²¤É“(Sª<ø±¥Ç•0cÊœI³¦ÍŠ.]ÞÜɳ§ÏŸ@æl´¨Ñ£H“FD%‹©Ó¦PŸ*JµªÕš®jÝʵ«×¯`ÊK¶¬Ù³hÓª]˶­Û·pãÊK·®Ý»xóêÝË·¯ß¿€ ö;”èàÈ)6œ¸±ã…•PUŠê-À €h`ùé—^‚ –wFH €úGZ ‚WaA:hP‡m؈†"ƒ ¾×`‰Ôøa†*¦ÈâhiÔhã8¦a‰ÿ¸È!Œ(ê7£hfÈc@¾¸b÷ )šA9”Qé¤fVf™äK^é™–]r)£’ Òç%c`Ž)¦™ÉÒ™¥É&™säf›pr&g“LÂ÷fž@ù#è „êB†&:(¢Š&Êh£…> é¢M) MYJ¨¤–r:©§‚Ú¨¨Šb𩦂’êh¥¨Ê*ª·ª륦Õª«Ý:릯jº+¥¹¶Z+RºöÚ©±Ÿ"ª²£2[ê°F,¬Î®:­¯ÕÊš-­Ð*ì¶¼^{¬¸É’»¬¹Ívë-µè>Û®µIï·ïz§º2ÉK¾ñ+¿ÿø{/¾¶Ò;/»c[o¡-LhÃðÄ4°Å×],Ä{E1Âû¶ª±|Ju,qÅý¢üÏȱŒ§É‹œÝ­ÃüÛÈÙál³sóu×óÎÎ!ù ÿ, y/þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ hÜȱ#Œ CŠI²¤É“(Sª<è±åÆ•0cÊœI³¦Í›ÿÐèÜɳ'š8ƒ J´¨Ñ™@*]Ê´©Ó§Aù â#•êÔªX@Ýʵ«×¥ô‰K¶¬¾¤_Óª]Ëv¤Ù·cѶK·®ÝïêÝË·¨Ë¿} \óoKˆ+^̸1] (Dˆ YBäÉ Ëu̹óâ} C‹½Ï³éÓ)[¨^ͺu &I˵íÛgÏÆÍ»÷WݲkûNܯÁÒÅ“+/i¦¹óçÐÍÀ^N½ºJभkß>ûhîàÃþ‹O¾¼ùóèÓ«_Ͼ½û÷ã {<Ù¯¾ýûøûÁßT~GúùXåG ˜_ Rt ‚=¨Q‚ Þ'\ƒ*$áKJhÒ… ˜áˆmˆ‰$¦ÈŠ>¨â‹#m8a‰”Æ8æ¨clãÉ("‹ù¨‘@&™Q-¨ä“3I”PVù!“4ºhå–WjÙ¤\†#–Ucæ™h¦Yb¶y•ÿ ‰›n&'^dtçErÔg¨Ý)hž5ý ¨iƒz™¥“‡V™(£½Ã &Hji¥“NjBBù(˜†šPMð@ª©¥žªª‘ª¦ê*ª¦*ê¬ Éz$­¸þ!ù ÿ, I:þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ØÈ±£GCŠI²¤É“(SJ¼á–.a¾lIA¥Í›8sêÜÉ“`€ž@ƒ Jô¤‹£H“*uQ´©Ó§P£ÔGµªÕ«ú¤jÝʵëH¬`«zK¶¬ÙaÞ]˶íд`AºK·nP¹vóêÝË·¯ß¿€ L¸°áÈ+^̸±ãÇ#ßÜG¹²åËû$kÞܳçÊx9‹Mº´éÓ¨S«^=É*?ª A‚¤ í×±‡`Í{pÌü>gîM°pÏ¡‹+_μ¹óçУKŸN½ºõëØ³kßÎåÇï»ü‹Ç üøó)ËGϾd‹0ÞLJ/¿þÏöø/Þw¸?¿ÿÿ( Wê™7ข ¤`G F(ЃáIØ 'Ðp‰"œ<³a‡*² ' XˆàõgbtVèSŠ+N×¢‹©co34ãF7J—ãA)äDR\Ìýˆäs;êØâ’Ð)Y”PÒÕd‚TNøäAM4ÁC—_z æ˜6VÉÖ•Sfùš•iæYlƹå›ÉI!BlÒirp§Ÿ}þ)è6 è¡€úI¨A†&Ú(‹ê™ŠE:¥Y*¦ i*©hn>*Ÿš6jC§2”j©|!ù ÿ, a:þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹êÛȱ£G}CŠI²¤É“(Sª<ø±%Ç•0cÊœI³¦MŠnêÜɳ§ÏŸ‚ J4§IH“*]êâ§Ó§P£’,JUèI—.¥jÝʵ+ÁªU¯bý赬ٳ6ÁR;¶#Ú·pãŠDC·®Ý»h˜lëQ®ß¿€Eê-É×màÈ×,üR±ãÇC2Þ¹²åË 'ƒÄ̹seÍžC‹> z´éÓgK£^ͺµë×°cËžM»6ŸsëÞÝ€íßÀ%îN¼¸ñ}Á“+Wx¼9q„H†TùQe$M®K§>Ä÷òï¡þ;?à8?ñàÓwß!ûãêã[~oÜ=ýçòóCÄ¿¿ÿÿ‚Øwr 8œ~þd -(à} Fh'Vhá…œP Ò×á{†QyjØ ‡ø!{"¶Ø‰Vƒ)BX£‡.æ˜Ð‚2Dã‰*Þ¢ŽËõcä‘H&ÙBJ6y$“N6 e”Ii%AT*9e–FnÉ¥—Y^)&—H‚I¥™Q¢é¤˜W’ùäAnv gœj6i›9ƹäœnÖ©¥Aw6(ž"êég•|’I袚藆Éè¤ 9j¥aJé¦XÒ陟¦Éi„x”jꩨ⑀wöêšþ¯J9*‚h:­áú®³ŠÆ 4*ÂÉ3œKl±Ëp¢@¬^ꩳ®BKæ ½Šv€ž=Þúl¦Û¶ª( Q[-ZÕ”kî¹èVc"A`Ë,¢ÒB¯¤‰›½ãzÕnœë´o´Üì­¼[o¾˜àÏ 7ì°?Ù ôðÄ #D1Å_üpÆ7Œði;ñ?!{|PÉŸŒ²?—ü±i+³¬2Ê-‡\sÇ/§óÍó|±ÏçüÝÎ3»\´ÍGã,ôrDÔtAO3¾KËõ@WK¼2FTW [Ö$otÏ^_ öÙb—MäÔÔ²íôÊA©c×õJ÷Wv‡{·ÜVî}ïE~ó­cà®à"!ù ÿ, z,þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹32À±£Ç4ŠI²¤É“(Sª\Y¤ËŽ,cÊœI³¦Í”HrêÜÉÉÉ—/o J´¨Q•ú’*]ÊTßO  JJµjÕ¦X•†,‰¦«×¯`ѰJ¶¬Ù³7· ‹¶­Û·pã&„êR®Ý»xóR¤[W¯ß¿E+¡ª$˜ðàˆ .È÷ãâÇWÈš5²ÀÆ0-kÞüoY°e ?{MZ±AÊX5&½ZôgÓœcFÝ!m¦²sëÞÍ»·ïßÀƒ N¼¸ñ…˜3SMÎñ¸sßÌÉFN]÷tƒÌÕÌ~É*?ªþ ÉÙ$ç÷ðC`W_oùúv÷¹dKûû¼áÇOŽÐŒÿÿh†zø(œ~—ñgà‚*ˆƒ "á„V(ƒþ†à?fèa{nÈɈ$–h"'|¨âfÙi·fÑ×PŠ+ÖøX‹!(#C;Úè#^ò1!E=þhäLvxä’œ%9$“P6ùd‚0Fi¥”U>˜å•\Þ8%‡_v)¦]Nn9æ™d~©$šl–µapÆ)çœiPd›xV¥#Dwæé§F8„Ç „j( øç¢DI¢ŒFÊ’£/6&é¥C*¤™˜vê¥`†¦§¤nú$¨TZZêªs¡Êê«DQ 무ÖJ‹°æzS@!ù ÿ, z*þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹32D±£ÇH4ŠI²¤É“(Sª\YQŸË—0cêcI³¦Í›8I¢Ùɳ§O4þÊ3hÎ…2“¾p´©Ó§PU*Uj´àÔ¤U£Ndªµ«×¯` ^•‰pl̰hÓª]ûÕ,̲n]²K·®Ý‘qåÌ«ëÝ¿€ æ{ЯàÈÎkX±F#Kž Ù±å˘×RÞ9³çÏ C‹Mº´éÓ¨S«^ͺµë×°cËžÍÉ*?ª áØ„ãíÜC æÌ™¶ñã%ì[Μó硧Œ¼zu3سkßnF¸Aèà¡þ7Ƚ|vïÖÓÇÏ~Ÿú÷ðU¶¿¾ýûøó«œ¾Y¿ÿÿñ×€"ö 4&ˆà‚ ¢G‚2¸ ƒ H]þ¥Ü|д!‡Ì!d¡d–hS?(¦¨âŠý Äâ‹)š(#i0Âèb,Ψ#h8æxP+î($f@ªxc‘-©dbH¢xd‘KF)X“ID¥”XÞuåMfé%Ki„)æ˜d¦a@Vmie“•}é&IP‰fš]Ñxoº‰Çž|öé' PøšQÙfžˆZd¡1Za¢>Ä 4œp¢'ÏXŠi¥Š,É\"ù$‘–*Ñr† ¥ª¤šêjC,q69ç u®)꫸&ë­¶®Úk«¹[?Äkì±þ«lWÈ6[ì²Ð:!ù ÿ, a,þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹32tÁ±£Ç.4ŠI²¤É“(Sª<©¯¥Ë—0õ­œI³¦Í›8!ØÉ³§O,c m™³¨Ñ£H“êüÉt§Ò§P£JMÙ´ªÉªL§jÝʵkB¬YK‚õ鵬ٳHù ⣖íÚ¶p˜„û¶®[¶rÑêÝËw¤ˆJþ}¸¯áÈI6º8±ãÇ#KžL¹²å˘3kÞ̹³çÏ C‹Mº´éÓ¨S«^ͺµëב ©ò£Ê$Hšàž]{È`ØÀƒ3 °¯¸q~Æ“ÎñX²%•KW´¹u½Ïy^ßÎ\÷ïàÃÝ âN¾¼ùóèÓ«_Ͼ=ÁìÚ Âwê¾>×ùÕ â·Ïÿä|„ÿ`j4 ~ð!DÁ‚ 6è FˆÑï%(á…9Q8†vH•…òèáˆ(qø‰$¦8¡ˆf§â‹-²x¢Œ¡ã…øåãXÒxãu9†è¢€>âaä‘H&‰G¿ý¤‰PÒØ¤“°í'äs=i'Ðp‰"œ<óe˜^*² ' PÉ]WòH¤–ååPcj2Ç&‚pîÖAt.ÔgÀYÙæžobiП€®gc¢ŒZô࣠6*©H!ù ÿ, ä`^þÿ H° Á^D F«#JœH±¢Å‹3jÜȱ£Ç CŠy‘@•&Db‘¤Ë—0cÊœI³¦Í— `$V‚–7ƒ J´¨Ñ£(@ K¤P£JJµ*E(bajµ«×¯`ÃfÄJ+W±hÓª][k¬ØÊK·®G·$°Ë·¯_»…(ÍÁßÈS @γŠ#KžL¹²å˘3kÞ̹³çÏ C‹Mº´éÓ¨S«^ͺµë×°cËžM»¶íÛ¸sëÞÍ»·ïßÀƒ N¼¸ñãÈ“+_μ¹óçУKŸN½ºõëØ³kßν»÷ïàÃþ‹O¾¼ùóèÓ«_Ͼ½û÷ðãÛ½áúöñ߯OA~ùú(à€êã_y&(à 6èàƒFhVhá…h†V¨¡„«)¨à‡/‰˜ ‰ ¢fbqÁ´"-¦xZŒÑ8’á(ãl:ÕãŽ@)äDiä‘H&©äL-4éä“P¶Ù’Æícå•Xf¹•Ìiéå•?r Ž[Š©›h¦©æšfLifp_~ù&rqz9çxæ©çž|öé矀z¶Ô „(o…&ºÔ¡»)ª(£º9š(¤¹IZ(¥¸YJ(¦·¥áé§ †š†(rúZ©¡jªeš^º*ll­úªf±.jP­aΚ®J„«®˜ñªã¯À²Jì­µ[Ó±1«¬Wέ@Ó>KUµØ&kíWÙÆQµÛFÕm«ßj®UM4ÁCºëªË¾ëî¼í®/½òÖÛ„ªçªÅ/Fÿ^p¿D!ù ÿ, z7þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ ]hÜȱ£ Œ CŠI²¤É“(SªD¨¯¥Ë—0õ­œI³¦Í›8o"Ùɳ§O$'c u™³¨Ñ£H“Þ:4(Ó˜J£JJµjÁ§1˜Ä S«Õ¯`ÊÍéÕhÙ±hÓª]˶­Û·pãÊK·®Ý»xóêÝË·¯ß¿€Á. ¶¬0áÁ†̸±ã…\_>þº¯²å˘÷!ÌÌÙòäÏ Óvî¼ytæÐ¨SS5}ú k̪cË.úúréÚšgëÞwåÛµy ÒwnƒÆOXμ¹óåÄ£ßMîÚ·ÉçØ›KßN—:rßgEþšO¾¼y3‹¹«wë}½û÷ÛßO_¡üúøSsÚÏ¿¿NT‡[~¦€q~7` Nv o\ƒ6f\„ NXá†~õãᇠ†Ø‡$'≖¨¢n(¢¸â‹ªµx"Œ4‚&£ˆ5æèØ!êè#`<‚øã|ù!tD&IWx 1©ä“PF)å”TViå•Xf©å–\vYevÙ!&v^–h¦©æš Äæ›iš)çœtÖiçxÞ5&™íùÜA~:—g’j×g¡H„h¢ƒú¸¨˜ˆBZh£D> h¤—NJéxÈÁI§Ÿz ê¨`HШ¢¦ê§¥„êª*¯âÑê¦5ѬÙú®é꯴Ni*EÃNTl°R‘²1‹ì†!ù ÿ, äadþÿ H° Áƒ*\Ȱ¡C„¼ˆD-™–3jÜȱ£Ç CŠI²¤É‡2ÐÒҀ‡'cÊœI³¦Í›8sHÓ€s J´¨Ñ£H &R²M)’JJµªU©T:öAÁ’c®ŠK¶¬Ù…Y+Ñ1`€±¨gãÊKgpúüÀ ZTu L¸¡\ ¸°ãÇå&P0DµUp#kÞÌ)€€íLº´éÓ¨S«^ͺµë×°cËžM»¶íÛ¸sëÞÍ»·ïßÀƒ N¼¸ñãÈ“+_μ¹óçУKŸN½ºõëØ³kßν»÷ïàÃþ‹O¾¼ùóèÓ«_Ͼ½û÷ðãËŸO¿¾ýûøóëßÏ¿¿ÿÿ8—Oh OîwAP°`ƒ:È h!‚ ‚”Gv˜a~Zø!~!8â}H¤¨âŠ,"qâ‹0Æ(ãŒ4Öhã8æ¨ãŽ<öèã‰%Xaþ¸‘I¤‘Ì!YdAN~ƤrQ"Ô•Xf©e AYå”°}y’˜`º†ÄUüPÅ)6‘"šjÑeId–¹Ú†ñ3TvjgÆŸ€*¨söi衈&ª(nQJ¹(u:ú¨t‘NZ]£–fªé¦œvêé§ †*ꨤ–jê© á¡êª¬¶ŠGz…¢Ú›ýÔjë­¸ö#+q¹öjë®Ãùê+°ªUcì±È&[ Û+±¨àl®Ì4-®Ðž€?Üvëí·þà9¸äv›íiå–+®@é’{î»ðÆ+ï¼ôÖkï½øÒVi¾¯E*)¿¬5£ˆÀlpÁ üÚº ý«pj!ù ÿ, z=þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹êÛȱ£G}CŠI²¤É“(Sª<ù±%Ç•0cÊœI³¦Í›ÿàÜɳ§ÏŸ@%J´¨Q&](]Ê´©‹ P£Jzó¨U¢,]¶¤Êµ«×“W¯žDB¶¬Ù³H²jõøµ­Û·ÃZ…K·®Ý»"ÑèÝË·/šåÎÅK¸°aº€ >z¸±ãÇ]ù â#™òäʘ@Þ̹sM{Mº4MÑŽ+¡ª¤šõêÖ°A›žM»vBk?ÚÞÍ»vnH{ N¼¸ñãÈ“+_μ¹óçÐ /6½ºu“Ó±^ß~=»ö‚Þƒþsß<¼x‚áÉ«/ï¡™÷ðãË7#{½}ãéï뿟¿ÿñýý'`uœhàr"À€ >‡C 6(¡r.Tá„Vwa†Â`‡ Ööaˆ$’6b‰(nÖB 0¬Ø"‹.ƸaŠ46ãA7Ö¨ãŽ<öèã@)äDÞeÞyE&‰“yJ6éÓ‰NF”RV™“еgå– ˜–\†™%•b–9h¦©æš iæ›pÆ)çœtÖiçxæ©çž|öÉ£—àaé'GžWè  JP3Š0êh£>šÀAV)¤“"* ¢åžAž¢§é€Ö\Sꩦ¦Šª¡Žêd«Eèê›`ë­¸æ*ë¬f!ù ÿ,!J=þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ hÜȱ#Œ CŠI²¤É“(I^ˆpÂÊ–/]²¤²¦Í›8sêÜÙ0ÏŸ@ƒ J4§Ç£‹*]Ê´©Ó‚H‘>Jµ*D$X³jÝŠdjÔ£VÊ«¯¬Ù³hõyýÚq¬Û·MÓÊ5;Ý»xó¢ñ ·¯_žsçþ%Èw°áÃ$Ëýˆ¸±ãÇ;” ¹²åË)cÞ̹³çÏ C‹Mº´éÓ¨S«^ͺµë×°cËžM»¶íÛ¸sëÞÍ»÷@¶Mo뻸ðáO"On¼ùHAУKŸ.HùrçØ/ל½÷vîE9ô‰O¾<'Ý‹?´pCôé½wêžaýø¹ç¾¿¿ÿÿ(à€w]ê´^‚ â´]ƒÖô`„–4a…‚ta†R´a‡ >ôaˆ$*4b‰(Bu`Š,TÍ‹0Æ(c5ðµØ" Õh#€'âˆ;¶vâ?CBd« Y$’Ù)¹"“ý9‰”ÿ ÙD<\™%–Zvy$•¾-éЗ`òFÆ™h¦©&” b›nÆ)çœtÒ¦ŸŠOÖÉÛñ©goÖ\è ‚J¨÷ª¨¡…"úgod¤£@‘ 4飸 À¦œvêé¥ÿhú騛bÚ[@!ù ÿ, z<þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ØÈ±£GCŠI²¤É“(Sª4ù±eÇ•0cÊœI³¦M‹.rêÜÉÓž@uÞJ´¨Ñ£Hê[Ê´©S}I£JJµjD—.>ÝÊԪׯ`ÃÚÄÚR¬Ù³hÓJ$[Ö š·pãÊE€*Ûjóê {×ãźSû¾ÜK¸pR>ˆø Vœx±ã† Þ¹²å™ ¾<äåÏ ‹n¶ÜagÏ¡S«^°´d׬cËN »à²`Ërã¾­»wæÙÀƒ®-¼¸ñãÈ“+_μ¹óçУKŸN½ºõëØ³kßν»wЧ¿þ‹8ùóÆÍ T¾½löìÝ˧=!§ûøóëç$ |üù&¥Þh õWÐi¨¨ TÏ@ÓàƒFáo¸….¨aR¦Ôax†xÑ"–¸‰&¦øŠ*¶hX -À£Œ1Îhc‡.æXŽñ¨ã@)äDiä‘H&©ä’L6éä“PF)%” Ni%KU^©eH 2Ôå–`Âôe˜d–iæ™h¦©flY®éæz,¾d›p &g“tÖÙ×xÖwrp¨ jè>ò ¤z=„¨¢I6£ˆ¤”Nji¥ ü)’‰’Ôé¦yex ¢‚Ú^©¡ª§¦º—ç«q¶š]@!ù ÿ,Q2<þÿ H° Áƒ*\Ȱ¡Ã‡#JœHñà…(\̸Q#F CŠI²¤É“(Sª@ŸË—0cê[I³¦Í›8sê4(³çË@ƒ J´¨Ñ£H“*]Ê´©Ó§P£JJµêBX³j݊ժׯ`Ur«5¬Ù³hòAÄgm[¶nãHK·.]ØÝË·/K¿ÿ*¡ª$˜ðàˆñ^ÌØ¨Ÿ>Kž rO”3kÞ̹³çÏ C‹Mº´éÓ¨S«^ͺµë×°cËžM»¶íÛ¸sëÞÍ»·ïßÀù’¼¸Íád+_μ¹óçЛ#½úÃéÔ­k¿Š=ëöï »{Ož`šóèÓ«Oc@oyðîÆO¿¾ýûøóëßÏ¿¿ÿÿЉ×€Ì ˆËˆàr/Àð‚ F¡ƒšÀ‚f¨á†vèᇠngà€!Ö6"‰%r6âAÍ(Òâ‹.Æã…)v¶¢Aó1t`œYs@þ(däÈ£kF*$À‘³ À“PF)å’LÊ!ù ÿ,J9Eþÿ H° Áƒ*\Ȱ¡Ã‡#JœHÑ¡¾‹3jÔW±£Ç CŠI²¤É…SbDx! -_Æ„é’ÂÉ›8sêÜÉS$TªìI´¨Ñ£H“šR©Ó§P£JJµªÕ«X³jÝʵ«×¯`ÊK¶¬Ù³hÓFÀ¶­Û·lÕÊ+®Ý·tóêÕz·ïÞŽ}ïþ¼00\ F̸à²`Ë"C~,¹²Æ)WÖ<òeÌ C‹Mº´éÓ¨S«^ͺµë×°cËžM»¶íÛ¸sëÞÍ»·ïßÀƒ G¬ïðãI‹GΜ(€gРKN}úçæØu€ø3»÷ïàó‹O¾¼ùóèÓ«_ϾýhånÝ»‡ßV¾|úöïÃÏïž4Nœ(ÂÉ3 "Ëp¢ëÑv ¦¡CFh^…f¨á†vèáqôu÷!xø^Mð€¢Š)®è"†&®cŒ4Öhã8æ¨ãŽ…Ècp>þœ5×id‘H)ÀŒBÞÆ$B4é[Tiå•XF)å–\véå—`†)昅cæ™f’ÙšhªÉ›gº¹[@!ù ÿ,Q2=þÿ H° Áƒ*\Ȱ¡Ã‡#JœHñ €‹3jP±£Ç CŠI²¤I‚"\ reK–*) ÜHãÉ›8sêÜÉ“b5ƒöJ´¨Ñ£DƒÒDÊ´©Ó§P *ݵªÕ«'‘hÝʵ+’“S5bK¶,D}hÓª]«lX›fãʕ˶nÚ“hòêÝËÍÏ’}ëý;·°aví:%<2q]Ƈ#—uÌ–£ä…”×^Þ<Ö²CÏœ fVº´dЦF›ºµëëY¿žMkl}¨këÞÍ»·ïÐoáþN\$K 1YJX™üäâУKŸN½ºõëØ³kßν»÷ïàì‹O¾¼ùóèÓ«_Ͼ½û÷Iƒ_„OߨüùõóC¼¯¿ÿNþþèVpH€&”| 6XQ?41„C@(¡Vèà†vèᇠ†(âˆ$–ˆÝ}¹™¨ Š)ªh ÕÄ(ãŒ4V#€‹8tcŽ >§<ŽdDiä‘H&éŠJúÇd“ú‘!å”TVI†Pæ‡e–\véå—`†)æ‡,Ž™^™f¢÷dšç!ù ÿ,iÑ=þÿ H° Áƒ*\Ȱ¡Ã‡#Jœ8PŸÅ‹3ê£È±£Ç CŠ,¨±äÅ‘(Sª\É2!€–0cÊœYÐ…Í›8sº É³§O•&MþJ´èà%*]Ê´©Ó§P£JJµªÕ«X³jÝʵ«×¯`ÊË€Ù³hÓš%Ëö§Ú·gÛÊK·®Ý»xóêÝË·¯ß¿€ L¸°áÈ+^̸±ãÇ=ŸA“Ly²åÊ ç °¯³çÏ ÷iMº)…Ó¨S«¦Pún¿×°cËî×ÚîìÛ°k×Å[7]Þ·} N¼¸ñãÈ“+_μ¹óçУKŸN½ºõëØ³kßήwîK½.ÃÏNæÑŸOÏþ@òE@tëûúWïãŸ(~üþžý½õŸO¦5 O!ù ÿ, äzfþÿ H° A‚þ}úGíPŠƒ#JœH±¢Å‹3jÜȱ£Ç CŠiQ…¤‰•ÀÉ—0cÊœI³¦Í›8!ø§ Éªœ@ƒ J´¨Ñ™ €Q[…açѧP£JJ•£4b䴪ׯ`Ê@€€ Ç>t˶­Û·`ÈÀÜ—póêÝËw&$)Là³ÊAßÈ+–H IªÉa0`±å˘ÝØ,W`åÌ C‹Mº´éÓ¨S«^ͺµë×°cËžM»¶íÛ¸sëÞÍ»·ïßÀƒ N¼¸ñãÈ“+_μ¹óçУKŸN½ºõëØ³kßν»÷ïàÃþ‹O¾¼ùóèÓ«_Ͼ½û÷ðãËŸO¿¾ýûøóëßÏ¿¿ÿÿ×\8—€dà‚&è ƒ :Ø„ BäÂ…f¨¡ nèá…RG¡2écâ‰(¦¨OˆÓ!áâ‹0ƈD‰*Öh"‹Úh#Ž`¡áã@‰ÆZ³éXãg/H"©"‘Â!I’’2Ù¤“(BÄ"|lÙ%—^†yÀsa‚iæ—]Ži%AXfimž%rxY4§|• RIž{êÉçŸu²çŠ ªÏ×!úœƒBd¨£®ÉÜ£…Fzeœ-Ì2œnªi§ *ék”^Ú&¤˜Všê¨±íãê«þ°ÆºD²Öú*­¶ÖʪH\@A¾ö*¯ÀRÐ뜽þìÃ*k슕«®M++®ÖÞz•¶š­¶} .U⺊í·çf‹·y+nºÖ~Uî¬Õ– ï´¸µ ï¾üöÛ‚¨¬Í{o®òÚ[ﻣ»+H'¬îAH QÅU áb.JLñÿñÄ_œ1È;¯Éø¢LðÂ5®Á +?+Çù÷óo_?ö÷so¾ú ·_ýþçËõX¾ôµ€ÒCàú˜–ÿ)Ð}„_å7AúUÐ~Ä7âÀ êÏ€üóàEXÀ‚t„Ü FNhÂ’0/\à@XHÎÐ…*Ĉ ²Ãôð‡þ8laQ(Á^ˆCb ‰HA&ZЉ„¢h$.Q‰EÄbµøD.FÑ‹S¤"E¬˜ÅÑŒIDãÕXƦQŒ!ãÙ8G7®ÑŽmäáñXG8Æ‚Rü IÇ.ò‹‡ c" éÇ?þ/#„d ÉAV’’“ä£!)IvR†z|c(ï8Ê<úp¥ ó8©ID¶R‘¯dd*79KWÖ–ÿXeDtÉJøò—À ¦? "Ìbþ’˜Æ,&2“Ìe2ó˜yf3 Â˃T³‘Ò¦3¥¹Ígv“™ßLf8 ’kú1›Ð4:}9NeFsíf<§Éʘ¬s˜ïÌDç<µ™Ïlî3¹g=e"Ð~rÓ ÞD(8*N†’s 0)¨:áéPwNTŸ•gFé Ñ‘H4 ½¨?7ÊO‘Ô¤ í¨GC RŒ¢t¡/mhL:S‹ª4$%HN²Sôô?ýéM7T–êÔ¨°Ç.ûìúøæÂí¸ç®» ØàûïÀF´»oÆß»ðÌûîÐG/ýôHŸ<íQ¯}ô¨5ß8<º\öÓàõ8ø?²ð&IéÂÂÎå…þ"E„H“Zäƒïë`TvÈC$º0†d`x#Q‰q¢i´øC(Â0ƒSÜ N¸H‘ech<£ÓÈÆ^±†6ÄâbÈ"JÄŽÿÀ#Jè8>þÈ¡ã>IÈBr9¤" ™ÈE*²‘Ž4$$#ÉÈ‹PR’–¼d%Ý#HMr’—%%EIRÄ“ƒ4åDP‰ÈLz>|¥+5IJG‚…•µ\$,åø\Î2”¿åW|ibVÇŠi2—ÉÌf¶À«De.ÌHÞRšÕ´åuYcž›ÅgWX©J‰s›¼D‰7£)Ëp¶“+ål¥Gân’dæ@ç7ßI#{ŽŸ½Ô';iy#Š ¨ŒÁІ:ô¡‚¸IBù™§‰j²œwÂ'F3²Ñ:3‹ éèœDZE<!ù ÿ, yþÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹êÛȱ£G}CŠI²¤É“(Sª4ù±%Ç•0cÊœI³¦Í›nê܀ϟ@ƒæäI´¨Ñ£H“*Êô§Ò§P£JÚ´)ªL¯b :µ«×¯`µnuzp,P±fê]Ëö¨Y²ßúD;¶­Ý‡rçÞÝ+0/Ý­±ò\Ö/a»†ãÊ \õpÒ¼C òAÄgreÊ–3pÜ6qAÏAsÖ 9ò@Œ^+ºïâÂoW…|Qµl°­ÿåÎ}[&ïÞ£w¿Vû)m¥¿;^œxZãÃGWžùsçua_ÿ<} õìØþkϽ9uªÝ]›^wûïâà ¿ … "à¿/¿~ ÷ÙfŸû]Ð_~ xtëy÷Þƒ ª·{ºWar fHß|ò5¶¡‡Zõ¡ˆ!f5¢‰% ¥á‚¥A8¡ƒºH…/J8#Œ5®¸š_ðqX^Ž=‚XCTñCC DJyä¨D¤‘H*Éä”OFùã:î¸X$nY\ZX£‚ñ“šAjB„a—|ñ(ã˜8– &ŠI™¡çž|öi†–pzÛ*žXhŠ\šh Œ¢7èœ>’I§@‚Tj饘 ‚P¦œZÚè§&Ê•¨g)Z*¨¨FªS«ê…è©@©ÆÚY{œÔjë­¸r"@«¦I©¬À~Õš í ©¾"ì²R=³Ð>+m´€ D¬BðÊX˜Ìv«Ôµ(!ù ÿ, z3þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹"Š„hcGŽC&ÈH²¤É“(Sª\ɲ%JábÊœI3œ—8sêÜɳ§ÏŸÿj •y¨Ñ£H“*]ŠqèТL£JJµ*R¨V³jÝʵ«×¯`ÊK¶¬Ù³hÓª]˶­Û·pãÊK·®Ý»xóêÝË·¯ß¿€o@¸°áÄ+^\±ãÃŒ#KNZ U¥Ê—-cÞŒu²çÏ-;ƒMº´éÓ¨S«^ͺ5Îǰ]Ë^ ÛñìÛ¨k#ÆÍ›´nȽƒ³ü ¼ qÃ)'Îü¤òåÆŸ«”Þ¼:Iê±GW~ÐŒ÷ïàÃþ›m½ê²¢n»¶y«¸‰Ëí¹Ù:ä­¹±‹î»8Æ ï¼ !ù ÿ, y3þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹ØÈ±£ÇCŠI²¤É“(Sª$ù±¥Ç•0cÊœI³¦Í›.sÞÜɳ§ÏŸ@Mæt´¨Ñ£H“" ´©Ó§P£JJµªÕ«X³jÝʵ«×¯`ÊK¶¬Ù³hÓª]˶­[„hâÊKM€·xó’ÕÇ·¯ß¿úîêLX+àÃ} +^<1bÆ#›¬„ªeË•/k6pÐña¦’C‹žhÀ3àƒ G«^=pY°e°_»ŽM›³AÓYëÞM·_„¾óN¼¸ñãÈ“+_μ¹óçУKŸN½ºõëØ½m™½»Úí/½ç‹7 ¾ãøócËsDÏþ+§÷ðãËç$ ½ý¬‚Ö¿Ï_¢úõýRAù5T`*$`j-ˆÑ Z'`„\M˜œ…fEÁ†vè!Êa˜áˆA‰Hâ‰<™ˆâŠ©(‹,ƸŒ0ÊØÞ‚ ¾øB5Úxލí¤z>&H£"Yä}G9dyK6ç`’NR åAœ@óž"œ<à —^~¹ ' Dâ”5yeA@¡™V¡Ù Š@Z JôfB{Â)UsÒ©æOJ ŸßÉ©£‹="j݇r˜£@‘F:é?•Bêèy!ù ÿ, ê&þÿ H° Áƒ*\Ȱ¡Ã‡#JœHñ €‹3j¼X±£Ç CŠYq£IŒ$Sª\ɲ%É“']ÊœI³&K˜&mêÜɳ'B$Cªü¨2 ’&FƒbÀ§Ó§PS0È/ªÕ«X³jÝʵ«×¯`ÊK¶¬Ù³3qæD˶-BµkÝÊu à4»xïêÍÛt®_´SøKX"ÜŒ…¥À¸±ãÇKžL¹²å˘3kÞ̹³çÏ C‹Mº´éÓ¨Sƒ<ŒXµkƒ¬[¿žý/6JÚ³mÆ ÖöÛØ¼ú¶œ8ëßǾp8ìâÏ“G_®.s…Î f'¸}à /LM|/À€ž’èJ´¨Ñ£H“îä)ÀF,J£JJµªUƒ-è ‚ >è`ÿ\fáeÂI8áA.a‡xAP0b‰'šH"‚¢A.¾XPŒ¸Wt42È!ºH¡t9ö¢þˆ#;ÆH¤†FBd’>B9d“Î= #’W*y\véå—hÜXÜ…dò”•3b™¦–Yº(&qe’y¦š¡Y'Ù9\œj§§@þh Àñ‰áœl®e›S2Zäk†ZP%¨TB©¥•^ª©ˆ.ªh£Ÿ>ê(“®EjØAœ¦7(ž€²*¨«þ€™V²Ê·j¢wâš'¬ÉŠYA¾j·Ï°Äkì>«,±É.«l³Î m´ÌRI–‰¬h¢%j{Á›Q+íAâ;­¸çR›n´Ö’Unµ½;ìºÎÒ»¬½Ï’+o»5µàï¿ÜBªÉ‹¬¾ïâ{¬ÂãÆ»/ÂåòK“Á › 1º«›1»×Ûñ½ËDñÇù:œ0É £ÜpÁ›±U¾þZÝÈ.c\³Æ7sœ³Ç;ƒÜsÉSÅ,lË,ŸüsÊG¯LÍE¿œ´ÅU™!õÔTWmÁÖ1½4Ñ[Ý´Í_ã¶Îcó²IZ‡ËµÚ^wítÙ>à ´Û`ŸMRÚáýþÞ|¯·ß{n÷G}·ÍöÛt‹8Ù‹›Ýx܃‹T8â‡×]¹â—3ž¹ã›C9H“[þ·á£S^ºè“žºé! _蘟ûê¨Ã®¹ì·ƒä:|ýôîûïÀ÷ƒPðÄû>|ñÄ<ðÊ/oüAÎ3}ôÏ.õÕ„}ïÍGß½óß/>òão}BÁþ³½ðÓc_~òíSÿ~ðóK¾Ié¯_ÿïûgOþñó^Áw?“ø L` ¸À쀠ö¶×?î P|$_I®…<‚4_áA–Ð}#¤ß"Áÿ™p‚(<¡üRh?Îp…Eiá@t(ªï…ù.Œ¡ ˆC¢øðˆ@Üa{¸Ä ±ˆ9Aâ•8E&Vщ7"¡¨)f1ˆ_¤b­8F,‘‹8ñâÁ¸F1¶‘Œo4ãÑx5Αwtcá¸G9bŽul¢ÿ¨EBâÑzD$ e2H Ê0Žá#û¸H“øã’˜Ì¤&ýMz“ü¤'C)JM’²” ¬äXPiʃ°2“§de,Q9ËRªr•¯¼d-E¹ËOör”®Ì%'oaÓ Æüå&•©I™ ĘÎ$¦T’Ì\2–Ò„ 5‘)Ìkb2šÙDÍ6 2NŽ€3œ¦f5_yNtî¦ §;“!ù ÿ, b%þÿ H° Áƒ*\Ȱ¡Ã‡ÿHœH±"ˆ3jÜȱ£Ç CŠI²£Å“Kª\ɲ¥Ë—0M¢<‰p&̓6-ÆÜ)RŸÏŸ@ƒêãI´¨Q‚9+ÖL:q)Ó‹G£:Jõ§T…O%^Ý:0+Tƒ^2åJ¶`Õªe‘†Mu-جb“²-{–ê\.òêÝË×Å]£n VûôïÖºB +¾;¸+\œkDB¹²åËH"*¹3ׯAGŒìùáf šOû,Í0iÂc!–=›­êÕomÍ;¦è߯çª;µêÞÈ]¯-xøhæi‹ç¾¼ºÊå±ßBžÝ6õéÇ­þ‹‰].móÚ»GÿnPzI¯_Ç·u^>g\ûwÝ›e?¾ü­õÙtŸ€çá7—~!(Ò2Á,ã`ƒ >(¡ÿIàLbX ò— ‡¶v!J’bHû¤¨âŠ,îs"rþ¥‡^sÛ DÁD€ãà¨#7p×9îxAEyù#>ÉdA-V©â‹½x“Œ†h¥•}Ye˜b²ˆeQ1Ò¨w3Ô›pÆ)g žX¦™ݹ"™zºxfGs '…Zê´¡†nÔgŠ|êÙè‰r´¨Ÿ…*u¨‰‘f4é£er*f¦múÜš¡Ñç¨ ‰jªT.Š*F¬¢VšÒ¥[¾Ú«á:ФñÙšª«£¶Éf—° ë«§_" æ±/ÅjjÁËlžÀ®Zí´+9 ­¬Ma›®ë-ŠÀr«­†ŽkmŸÊŽ©nIÚ’-‡j«®¸â¾ÛQ¼öšÛ«¾ø^«/HüJ[ê¶§z0»‹T0½ÅÜðÄÏ‹hA@f¬ñÆ@Vì±Ç¢},òÈ ½ ƒÉ(·p²Ê-LIòË0ä2C3ÇlóÇ!ù ÿ, z/þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢B3jÜÀ¢Ç CŠI²¤É“(O^ˆpÂÊ–/]²¤²¦Í›8sêÜÉsb€ž@ƒ J´¨Ñ£H“*]Ê´©Ó§P£J…ʱjÆ©X³jÝzÓªU®`ÊËÙ³hÓ¢ùI¶­Û·pÿ±K·®Ý»xóêÝË·¯ß¿€ L¸°áÈ+^̸q[¯UBæè¸2ãÉ” bÖ(y3ÆÎž;j-Ú²i¢¤AoV™õd×OËöHº4ÁÔqö Û+ÂÀƒ ÞÂÀlˆµ ë.¸üvèÞ_s??ž³¹`ë±ÿÓŽdH•U†þ AÒd¼wðCŒï^Mg÷ïáÇ—^=aíø§œ;_Díí‘ÄŸ@þU–o™¡à‚ 6h†}#9(á‚haI²wᆊe؇ æák!–8؈±™¨â_(ú¶â‹{µŒ4Ú%cd5樀:ö¨Õ™ù($X@n4ä‘=%·Þ‡K’ˆä“Õ&¥t6™"”X:4e‘œQ©a–`*ÔÄM áÄc–鄚hf§so:We˜t~ÄåU^2YçžÝùYžNò)(rqä§mƒ&ºÐ¡Ðá¨è£ iÇ 4œp¢'ÏXŠi¥Š,ÉMÊ馗fj©§ Bª*œstD6þóêC±®úh5¸æªë®Õ`P­ Å ,BÃÚÊg±ùºŸGÈëì@Íí³Ô5mµØz!ù ÿ, z.þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢B3jÜÀ¢Ç CŠI²¤É“(Mr\©1¥Ë—0cÊœI³&E–,mêÜɳ§ÏŸ3q®J´¨Ñ£H{"YÊ´©S$I£JJµªÕ«X³jÝ)tèÖ¯`Ê}Ø•ãØ³hÓn-ÛR­Û·p‰²Í·®Ý»/+¡ª¤—ïÞ¾€ ü·°_¾‚ñ*^|21Xú"KžLYã˘Ñ. ¶¬3çÍžC;&X¹´äŽ™S«¾jÚôê×°cËžM»¶íÛ¸sëÞ­r.FÞÀƒÃôZ¸ñÔÄ‹D2¤Ê*C–6YÚüùÑÇ&ÏŽt{Éùþq·è}<Éò‰#Do¾÷Üöç}¯—P=|”öïƒÌ_zúú•ä_€7ØŸÿ½G`| .XÑ€Aø„RÄÉ…f¨!'TX`ƒ‚˜ [‚ÞC–‘„,"¨¢D':ã‹ µ(â@ÒHÒŒ«ÙHb}.ê($WANX$H&©ä’ édO>–õä”k™#•¶µÐ Zr¹e—`òøU”]a\ý¤©æšlö#f•7FX¤™¶µi§šh‘)|F¥'N}jÔŸ9 j¨O„zuè¢6%j£|²'gœ@s¡"œ<à ¦šnº ' T饙nZª§ BjfrÊMêâ@‰ ëCoª œ¤¬”kAµ"$f¯k+n»Žøcˆq ˲öW¬±eg³Ô2im’­ tíµmk-µà!ù ÿ,fDþH° Áƒ*\hðŸÃ‡#JœH±¢Å‹3jÜȱ£Ç CЉ‘¡É“(O’\ɲ¥Ë—0cÊœI1¥Í› iêÜɳ§ÏŸ?q µ ´¨Ñ£H“*ý7tèÒ§P£JJµªÕ«X³jÝʵ«×¯`ÊK¶¬Ù³hÓª]˶­Û·pãÊK·®Ý»xóêÝË·¯ß¿€ L¸°áÈ+^̸±ãÇ#KžL¹²å˘3kÞ̹³çÏ C‹Mº´éÓ¨S«^ͺµë×°cËžM»¶íÛ¸sëÞÍ»·ïßÀƒ N¼¸ñãÈ“+_μ¹óçУKŸN½ºõëØ³kßν»÷ïàÃþ‹O¾¼ùóèÓ«_Ͼ½û÷ðãËŸO¿¾ýûøóëßÏ¿¿ÿÿ(à€hà&¨à‚ 6èàƒF(á„Vhá…f¨á†vèᇠ†(âˆ$–hâ‰(¦¨âŠ,¶èâ‹0Æ(ãŒ4Öhã8æ¨ãŽ<öèã@)äDiä‘H&©ä’L6éä“PF)å”TViå•Xf©å–\véå—`†)æ˜d–iæ™h¦©æšl¶éæ›pÆ)çœtÖiçxæ©çž|öé矀*è „j衈&ªè¢Œ6êè£F*餔Vj饘fªé¦œvêé§ †*ꨤ–jꩨ¦ªêª¬¶êê«þ°Æ*무Öjë­¸æªë®¼öêë¯À+ì°Äkì±È&«ì²Ì6ëì³ÐF+í´ÔVkíµØf«í¶Üvëí·à†+î¸ä–kî¹è¦«îºì¶ëî»ðÆ+ï¼ôÖkï½øæ«ï¾üöëï¿,ðÀlðÁ'¬ð 7ìðÃG,ñÄWlñÅg¬ñÆwìñÇ ‡,òÈ$—lòÉ(§¬òÊ,·ìòË0Ç,óÌ4×lóÍ8ç¬óÎ<÷ìóÏ@-ôÐDmôÑH'­ôÒL7íôÓPG-õÔTWmõÕXg­õÖ\wíõ×`‡-öØd—möÙh§è@¤ŽŸ €B$Ò¤ÀŸ, ‰þàØ Ÿ˜u'Àg¬âѤ`øžH²Œ;þwž0Îæ›g³ÆåyTùãz0Ðèx ¤öꬷîúë°Ç.ûì´×nûí¸ç®{͈D‚Hï¿ûüð¤¿içÈ'¼s*ï<çÌËùüóÑÇ©ÏõØg¯½>Õùý÷ØÓ >øâ¿ýî#¥D§ús²/§ûqòòÓ?ýøþ÷÷o?ýú R÷0˜À|Û+ Šˆ"F¤¨¨%Á Zp€A öx"¾ä€ ŸD@—#ø¹$„Ø#áLˆ¾ºð…0Œ¡ EãÁ7Ýä}5´! [³C¦ „H®=¤€‡HÄ"R€"FLâøÃö5‡*qb¡h’õ=1N=dS×´EåÐ!_ôátȔьW\áÏ‘j¸ñpŒc5 ¤0†‘ƒ¡cÓÈ<^Äõé¡ ùè%*q)€¤Ï Ç‘.–b¬âD‰£EJr„ä‘%B‘&4žå'CIÊDîh“9‰)uD†Vºò•°$ÿËZºr–SÂåFt饀!ù ÿ, J<þÿ H° Áƒ*\Ȱ¡Ã‡#JœH±¢Å‹Òá³±#Ç3ŠI²¤É“(Sª” œË—0c† °²¦Í›8sêÜYP¦Ï—x J´¨QœA&=Ê´©Ó§P%.JµªÕ«:§bÝÊÕ¤¾¯`ÊÕ×5¥Ö²hÓFˬڷpãælÛV®Ý»x1Òe›·¯ß¿÷ŽL¸°@$ˆ+^Œ„ª`±†#û}Ö1寒3Eù³çÏhh¼Œ9*i²šS =-ºàiË—UËÖùú`mƒ|ñɽ[7ïßþöM¼÷îà£ISÀ¼¹óçÌg›Vn›zA¬ b'Ýšàm§ÐÃþ?—õ»w뮹W¿Ü} ù‘â㬄ª}ûõïë7@þé{ÿýà€èÁ_xñ×ß[ƶeèž„"-Ì2^ha†*¸à] Bøàc:˜œ‰ڵϊ,¶èâ>½(#‹1Î(c6ºˆcŽ4Ä£Ž)ùc‹;þX$G昤KΤC®Øä>F c•QNù¢–@>¹ •W&—Db9$™=Ši¥—Ži¦‘o"§’s2Y§“lö禚YÞI%Ÿgú¹¥ .ž•§f{”(A‹Ô¨@žæ¡O>ú¥˜®Ih™”všP¦} §¨r’J§§¨*ª©©v²Š§«þªjª´‚(¬ƒâÚ¥¬·Òšª­£ò,£« [ª¯¨{,±³2Û«³Ã" Ø3ÐPkmµØ^ë!´Ë:Z,·§.¸­J XÊŽëm³ë>Ûn´~knIýÔkï½øöƒP¾üÚ»o¿üþ 0¾ìïAŒpÂÄpÃóšôp½'\±Áœ1À÷ÛqÀ ?1Jë2Ã盲ÂO¼ò½#VòË4óÉ㌱ÎÇì×Í-‹Ì3ÇC{\4ÈA£ìs_@Ô´Í.­²Ô,;õÒx==Öqý×`_µ]a tÎgïœvÏk=6ÙbCm¶ÕsË­tÛFãôÔÛq•}7ÝÛ6àƒ ®6ßpù]øÖq3^·ãC¾8âh)~¸ál~¹ä›wÝ8åeYž9ænk>:ç§{þ8è]‰^:éy›þ:곫ΰ¡¬cåzì°ïÝûÔzOîŸ{Uþ$¯üòÌûƒPóÐ+ÿ|ôÐOO=ó'oöÇCu}óÖŸ|øâ“ÿ½QÜwï”øË›½ûÔ«/AìKPýãßÿüüÿƒ¿óú«ü¢×¿ùýo€Õ ûÒW@ŸPå»8²ÿÊ‚Ì`C&X< zP$!ù ÿ, z'þÿ H° Áƒ*\Ȱ¡Ã‡#JœHÑ €‹3jP±£Ç CŠI2â…(œL¹R%J [Ê|Ù²¤Í‰sb¼É³§ÏŸ@{ÐG´¨Ñ£úEÊ”hЧuê„JµªÕ«]hÝʵ« „M›‚ ‹«O©9ͪ]Ëv$Ù²ß+×i[’h7ÞÝË·/ݺå~‹0¯F¿ÿ gD̸±ÚºE“•¶°â‹ˆ/cv̹3PÈv ‚N4G‹š3§ö̺µ[ДÅz<]P3m¾¶]ëÞMqtl¦7s´};ªð‚|ñI¾\9óçxKŸ®ÐwiÈÁW×&Žúø@þPOþŸuѰmz'Ƚûeâû¶//ý|Aû#×?\;ýÄúý×~Ò2Á,£`‚.è xîfòØ ƒ2˜ „êf yéUHáv#î`‡@í£âŠ,¶¸B.Ƹ"Š4ÖXžJ¼¤’)éxA|É(#ŒBºhã‘HºV¤‘-Ù"‘Nª˜ä”T"åŒM^ùb–WVéå—ji¹¥AbB%˜h¦T™\žÙ¦“jÆ)'Il’©¥™pΩçžÕäo.Éç „.ä'A‡”h¡Œºè?>Úè¤qF ¨]Rª©ž–fŠ©››†šf§ þ驨¨VIjžŸ:Y\ªA°Ö¸ª Åjë­¸æªë®¼öêë¯À+l¯ó k¬žÄ½zì²hžÈì³T: í´6¦aíµØf›†@RëmÝ2$^@;kew/images/kew.png000066400000000000000000001105451507107350600144170ustar00rootroot00000000000000‰PNG  IHDR+ƒsRGBÙÉ,gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿÿÿ ½§“ pHYs  šœtIMEé 4øÔϰ IDATxÚìÝkx^åy'ú[–,[¶d[ر%0¸>¥6d†rH“Ʊ›BÚ)Óº©K;3ɸIÃLL¡»v3W3´fÆ_{'“]¦™Ý«®›Í´³§Û™Â$(¤L¡4;[1ôZ ȯÐÁ²dÉÞÌÙ'õ¾ëùý>¥¤M³îwéY÷ý_ÏZ«$('•(p%Jà˜=ñ#öÌ£(¸À|Šú̪(ªÀìŠ"ú̱ ‡ÁÀ<«`†~̶ŠdðÀŒ«8̺ŠbèÀܫ̿ `ðÀìÀ ÿBmðÀLì` þ˜¤Á3²ƒ3ü`Nv`ÌËÈð€™ÙÁü0;g<0ü`~Îx`øÀ áÿòÌÒ ÿ˜§3þ0Sg<0ü`®ÎðQƒ?æëŒ†„ ÿ2þd<0ü Èx`ø@ñÀð€ ã€á!@ÆÃ?B€Œ†§š† QY½tJþ³ûòq´÷H 9¬Ð|¦Ã?Éi¸ü£¾»ÛÅüš‹¢ziCAÇš5+bVÉÉ<Í/¶Egnÿ9Âu«Îz,-Ï~Ó‰€ Àဤ‡|F¯/ß]-{!@†2c*·ás~v (ÜÀð@Qq7?[¤ÀП´ó½»† ùd˜÷µ¹|:Ã?†z2ÅãÓl>]€á€IW±xy,©_£H`ø`\Ît7¿3·ßÐOÑï>§L÷Œ.`F•–WŲµ×(ɇPì€á€ÓÔ4lˆÊê¥ Ñúü“12Ô«Lùœ>•€á q^Èãgw€ XÃ?€˜$>I `؇„B€w?:c‡€ Ã?€a˜B!Àt†ƒ?  çv~È ÖçŸ|Ï'6…€É ÿ~ HÒ &+0üüA û€à=k…P@`ø(‹—Ç’ú5 LŠÖ矌‘¡^…È@ Èwú©Ô—–½ p`ø0ð‰…5 ÞþgB€â € óÞ€l†C?Ài¼7 0C€ñ†C?À¨ØP!€Àà þ ý€,„€PÓ°!*«—*€0 `Ã?À”–WŲµ×(€ `ÚCÀ³½@ 0ø (¸Àð`èi 0ð†Ã?@áéËwDWË^…Ã|/0ô=»&'0ü†~E ÿ‚€0ð H!0ü†€¡A@â€á0ô ÈH ’QÓ°!*«—*B@ ²È€àì€á0ø ÈP 2ÇV„ ÃÜñ@ þ0a¹ý1pä°B™ ÿ€ÁÎÁn C`ðA 0øÀ¹x,(ÖÀðü`œì 9©´¼*–­½F!€¬rÇ!€0ø@QÉw´DO[³Bþƒ?L!»€™ €Á„€Àà‚@`ð‡Q¹øuqÁ5q|h(úûûbxh0^nï:ïÿ]¾£%Ž¡¾îê¶ÿ¾¥åUQ^¹(æU-ŽÊê¥úÏêËwDDDWËÞÓÖ„‹.X‹-Šyó+cþüª˜3wN|óÛë„A ŠQÅâ層~BœÖ矜֡=K†¤¦/ßqZH0U€áÐÀÃ8¹{7}j6LxGXS€TC`ø‡³ðé.k€¦›"g»mºÔ­Šê¥ ŒUiyU,[{B ‘¦()¸Üxí‡â±'~ 8X¿À¹gÐcë 4Å ÆÚÖ?@h€IÝÜÒÑüýGßsžùœŲ&¾u®Z'…þƒ?Z°nbݲž ,X°Žb€¦UФ‚5ë+ 4¥pv¹ý1pä°B€õ—i”ïh‰ž¶f…€Æ“©ç.X±@³IÆôå;¢«e¯B€uk4 4”dí¥`í¦øØ@óG°–c=€† "PLJË«bÙÚkk< ƒ¿ÁŸ³óÖ~ÈŽu«¢ziƒB `ø‡ˆÖ矌‘¡^…ׄ€Ðô¡Ñ²Æî\@dLMƨ¬^ªhì€3ãz@S‡HŒÐ׆4l€k ®)€Ð¬¡I²Ê»p`øG3¸ÖàºÍ0Àµ×!@hÀÐl®E­¾|GtµìU. ý€ë®U€Ð\qNS ¡Ò@¸vá@E±éÌí#‡HBÅâ層~B`ð×$¸®áú@“„¦Àu×;@š"4B®y¸þÐ1íò-ÑÓÖ¬®B@h|4:¸&º6Óg–€F €5=dŸ࢈&Àõ×KšΤ3·?ŽV×O 4.\Kq-hXЬàºêº @“Bað9?€âP±xy,©_£B@4%®¹¸æ‚@4 ™bG€k0 Ã?gä­þ®Å@ŒYMƨ¬^ªš ¸>ƒ4h,p­Æµ ™@C€ë¶k¶"€4h"p wý q@ã€ë¹k9 Í‚fÞ¯´¼*–­½F!\ÛAþ5¸Îã:Р!À5×|@v¹åeqlhX14pí¸øã“£vËŽ(_¸ôŒÿÞ… k£dî‚*›?#ÿÝ»rq¼?ý¯ÇÐÑþÐí~0ôè@.ø¸à“Žò•#"b~ýúˆˆ˜SSó–­‹Ùó«ßó¿7§¦>óµè‰ÞCOGï¡§#߸ÓÉž}À…xŠè_|Å qÑ.Ž¥‹«cΠãÄìÊxyp®â¼Ï’9'¢zÎÉX8·$jæ‹á£]ñúÁ'ãOÿÝvÅA€àâî¢î¢Îô ñoݵ·þܾ˜_¿þí;ùÕ‹—Äìy‹âè¬ E›F#=1|´;Ž÷ç#¿÷a; Ð+ _¸˜»˜Ãø‡ÿ%×nMbk~V‚ÞCOGîÁ›}ƒÞA@.â.Þ¤¬vËŽÓþÙ¼eë¢âÂK¢´beL_n¯è!ô€\¸]°Iqø_¸ê'ÝÝOXuéñ˜{b ºûâGÿE0À”XP·*ª—6(„ž`øÇEš™ü—\»U!8£¾ÜÞ8ÚÚƒ]¹èÏí‹¡ƒ) ú ýÀEg }Я¹â[ù™°ºx#þç­«=†>àÂÌdj}þÉêUÆmÝ=͆~¦ÅÜ®æxöë_´K½†à‚Œ 1S©zÓöX~ã AÁèËíC÷_§è9ô €Bæ=.Àò•cõ-»‚‚7»·-¾ׇA€à‹ /ç²â¶G¢âÂKN ý¥³¢²¢<æÎý'‡¢ÔÝÔ¹oV ô"Æ#ˆÀƒ?Ó zÓö·ÿuÕŠ+£jÅ•žá'ÓfïWžþëhèvÅ@_R@:sûcàÈa…@.²ü™ >ÉïìÊEóÝW)„þD„ WÃ?ÅÏúáüº›£ý;âëzEЯ€\P]H)õÛvEEíʈˆ(›·ÈàãÔ—Û­{î$ÈKŠõ. ÿ (E1ü/Z·I!` våâx>޶6y€þ= Àų0ä;Z¢§­Y!P½i{,¿ñ…€02н‡žöu} B€‹¦‹%†Hž[ë!ã*/%õkB_0ü»H2}ÊWnŒÕ·ìV(@#=ñê£_‰|ãNÅÈ ÒòªX¶ö…Ð߀ ÿ.ŒL½kþè¥è/™«Pà|ZPŸƒ^¸(âbȘ­ M ýØ‚‘({åÅ×ïúÅÐó ï.„¸r:[ü!›|VPïƒþ¸âÂGDD\ñ»ßŽ‘%^0YvÕ’ˆ®ç¾ßø¯ÿ· @„^¸èá‚—’·=•õÕÝÔè3‚ú!ôDÀÅ:?’Ã{îõõ}z#à"—²ÎÜþ8rX!2ÀËü€Ñ°+@„¸°¹¨QDÊWnŒK>yoÌ©©W `Ü|NP¯„~ ¸ ¹˜QÀƒÿÊO}5J+(0©AÀ@ûA;ôL蛀 ™ …`Õ]O¹ãLyðFóßÅ`WÎûôOè¡€‹— Ó©|åÆX}Ën…¦]wSc ½ÑíÝ®ú(ÞäÝIÀEËàÏ”Xû;ߊ²‹>¨ÀŒëËíC÷_§z*„À…ÊðÏd©Ý²#–\»U!€‚ä¥z+ôWÀÊʼn ªÞ´=~âg-ÚJ? @Á³#@…> ¸0ˆ|GKô´5+D‘ؼstž\¤@Ñ艦;W)„^KpAš žK+Õ›¶ÇòïP < ç€\ˆ\€0ø‰Ùwk"è½ô`  žtùœš_ÞCS}˜^ ¸èà‚“†Uw=sjêH–ú1=p±ÁE&Ó|Îྠ/K…1#™)Ñúü“12Ô«Ä6€³ìÊEç»#߸S1ôgBàâ‚á¿x­»§9J+(À(t>±;Úº]!ôi™e—&\XpAɨõ´)€ @¿†ž .&¸dQý¶]±hÝ&…2«ôäÉ))èÛл!4ÕnÙ~èÆ(™[¥B=z8àÂᢑE¶úL…1¯äDëžûbèàc ¢—ÓÏ  ¦WùÊqÉ'ï95õŠ0 戗¿÷ÿÚ ŸÓ×  ¦Oí–±äÚ­ 0Nä[ã¹/^©ú:ýŒÃ,%ÀEÂÅÑ[u×S†€™l^«—Åõ;å+7*†ž#;0ü»Ð2 å+7Æõ¿ý_â¥9ŠP~lÁ¬XÜ·7þøóú }ž^¸(¸ 0 VÜöHTÖoP€ÖÝÔ¹oVýžž¸¸0võÛvÅ¢u› ˆì»µNô}z?à"àÀè¹ëP¼îû³ø__û…ÐÿéA€ €EŸ³[u×S>ë}¹½qèþëB¨}‹=§”¯Ü«oÙ­5Ø•‹æ»¯Rý žXì-ô†Ã?@ ï¹7ò;B_¨7D  Xä-ð)Zÿ@›"$¦ó‰ÝÑþÐí ¡?Ô#"‹»…=×Þ·/úæ\¨‰艃_ûl |L1ô‰zE’2K °¨[ÐSòÉ?o7ü$®´bA¬üÔW£|åFÅÐëè·.ˆYó©ÿð§ñk_5žͦ'N…«oÙµ[v(†ž§(Ô4øD1ç&DéBXèê·íŠEë6)gå½zGý#°€[¼‹œ—ü0ZKòûâÑ/þŒBè!õ‘ÀÂ=9ZŸ2F†zÂð@Z6ûh<úÀ¿òr@½¤X´-ÔÅàýÆÿ×|âŸÄã­¥ŠA¦ våÞþ×íßþ×Cot¼ýïõçöMøÿÏX¡lÝ‹j?U‹/Œá…kãÀÈj?Egd 'ºžù†Çô“zK`±¶@*wý)$œèŽ®×Ú"ÿú«oåùÆÉÕ¡zÓö¨ZqeTÔ®Œ95õN ŠN_noºÿ:…ÐWê/`‘Æâ\(ÆòïPfÔÈ@Oôz:z=ä ?µ[vÄÂU?yÚ??ÞŸˆˆÙó«…ŒÃ{îõ·­¿Ôg"ÀâŒEy&­ºë)S®2cVÇsñ‹Ͷãwã¢õŸˆ'‡W)Ófß­uŠ ÏÔo" ÛJË«bÙÚkÂb\P.ÿâ?ĉ…P¦Äüü ñä?¢EäŠÿB4üÓ?ˆC'+B!€¾þ-ÂYñ³ÿç¢u¸J!˜žù-~µ[vDÍ7DiÅÅ`J vå¢ùî«B ÷D€…ØÌTñ¼?SÕÈw>±Ûó½U¾rcÔ~ìÓQµâJ“®ó‰Ý Ò{êA`¶ð2Ù ü5¿þ‘¯Z¡LH_no ÷¾íßùßûNPõ¦íQ½áú¨¬ß Lè?õ¢°øZp™¤áÿ¦»vÇóGÔ‚ñénjŒÜƒ7+§©ß¶Ë΄úP=),ºŒ^¾£%zÚšb ÜðÛÿ)rËþ©B0&#=qðkŸuwŸ1©Ý²#""Ê.Eë6)ãâÝúQ,¸†ÆaÓWZ㵡R…`T ÷̓]¹èÏí3ø3)ª7m%×nõ©Q„zR!,´X\§ÊMŸ¿3ÞXó¯ãµÁYŠÁyÞs¯—÷1-aÀE›?çQÆÄ#zS}*,°UÎá÷v=/žX‡ºgűe ÂY¹ÃÆL©Ý²#–\»U!èQõª°¸ZPõ´)g52н‡žö"? Öª»žò¨B}ªžU )†Æb°+íßþŸ ý«·=âSƒ¼gm{éëwxG‰~Ui±h}þÉêUˆIR½i{,¿ñ…àmOìŽ#Ï|CƒLf”¯ÜËnüBT\x‰÷¤ÞtöFî›_ö¢R}«ÑbЗ–½ 1I>ô[à VˆÔÿ®r{ãÐý×)IYwÏ;_Ž ¤Ë#úW, Î̳%–·ÿüÞ‡½½Ÿ¤•¯Ü—|ò^ï ‡ÕË"ÀÂiÁÌÏúóÖðï®?¼—/ ¤©ó‰ÝÑþÐí ¡—ÕÓ"À‚i¡ÌÏú§«»©1†ÞèxûÖèÂùÕoÛ‹ÖmRˆ„Ø  §ÕÛ"ÀBi‘ÌŸÆJsè÷æ~˜› ·ÕÛ"À"i,xå+7Æê[v+DB|â ¬©fJMƨ¬^ªz\þ-ŒU¦rèo¾û*…€iä±*!z\½.,ŽÄ‚ñSðÝÈW®PˆŒ²Å ƒ«fÛ/oµ£JŸ«ßeÒÍRxÇš•—Y 'è¦?Éþ3ìðž{ ÿP öÝZçNq†­¾ewToÚ®†Ú)#,I“‰¹á“Ûâ‡ZÂEcÒykuv ôD×3ßð(`^˜m‚íþA `Á° 5›úr{£uÏ}¶ B‘©ß¶+ʪ.ˆµ—ƉòJèÑ#°øñnùŽ–èikVÃ?áÈŠêMÛã¢ÍŸ‹ÒŠŠ!Ð# ""J#â÷•!ÛÕ^ªçYðûºÂðŸ¼Î'vÇáÿïž8ò­û2àØKOÅp”FiEU”/¬U XzÝoGþÅÄÈ‘—C<éuz£ýE…H€'õ<ÿðÏØxÖ?[¿çú!­õ»¬êï È_ÐO…¾|GtµìU<§xÉTvØF Ör¬ãzbôÆ,t94Œ6Ø•‹æ»¯R VÝõTÌ©©Wˆ"f'—ÞXŒ œÅmÒÙö¯A¬óXãõÈzdXÜ,lšB ”gDë}ìîÒ'ë•­YJ`Q³ q6+n{D3X„Fz ÿÀ˜å¼9º›¢Í©©÷eÌŒŠS±xy,©_£g‘ïh‰ž¶f…Dñ± ˜,õÛvEDDÕE«bÉÒD×P‰¢ /4àN7ÍXÌ,d† Ðá=÷F¾q§BST­¸2J+(†@߬w&<`³€ñ«îzJŠHç» ÿÀ”É=xs¼úèWbd G1Šˆ tò-Š`¾éZwO³OBÙðoË?0åCRãÎhºsUt75Æ`W.Fz¢ºl$̶‰TPÜ<*H‘Güa&Àóÿ¶x¾¾ÜÞhÝsŸü3æ­Çªæ”GEÅÜ9}Ãî)*œÛ‚ºUQ½´A!ôÑ ÿ®t¬ºë)wþ ÜÈ@O¼úèWl÷ Bõ¦í±äÚ­§®!Ëëb¨¤4^í„zi,XßÄ-¿ñ…(0”ŸŒ¾cÑ?t<~í³îøkÅmÄÚ†ºX¸¤6úŽDÿàp:Ã'"FN–Å`I©" ôÓúiX¬,T…À3‚…©ó‰ÝžñŠÂ[Ÿ üØÕÿ(fWVGïÑ¡ˆCÑ?x"ú—ÆÀÉÙ1e1>'(ÐWë­`¡²@þÑ ™ñÙþk|àòÅ+¯÷Å˯ E~°,cNœ»\côÖzlf’‡µ,P&Ã?3€IõÕ[)ÜúÏ¢vaIÔ/©Œ…óÊ¢ÔÍ×=$0ü[¸]ü9õvÃ?%ÛŸ}<¾ô¹ݹ渰jNÌ)+‰9³FF@‘«X¼\Š˜G†}fØá=÷z»?Yå+7Æâ+nˆ×þ³(Ÿ3'GNÄñ‘“1ròdœŒ’(‰’qOjFžõÚzn$ Q†ùÜ_aìÊEç» ÿ@j·ìˆš+nˆ «æÇÉY³bpøD?Qzr(ŽÇ!€  ø4 Þ[@A¨iØ•ÕKÂdø/r¾¼Õ§ý€dƒ€ˆˆ­7ÿjtæÆ‘¾Áè>^ÇbN {I  €¸é¦`!*@­Ï?#C½ q¶ý޾ÜÞ8tÿu 7þÆïÅ•¿ð›±¿íX¼ÚW½'Êc8ÊF ÷ Àdá1ü7wýÎìKÿóPtöÇ+GãÈш¾q²dV/ñh€@®g2YQ±àþ™bo½áßðpf¿û3+¢õ»?~É¢¸ ª<ÊgŒYq"Jc$Jã„éfTgn¿"œƒ€D€?,\ÄySwS£-ÿ£ð—ô»ñozyỔ% æÆüòÒ˜Ur2fÅHÌŽá˜eÓ*3dàÈaE@€á¦¹ûv+n{D døÏ=x³BŒÁ¾û–8Ùù|,®,вYQVr2"NL=7ôæX„‹Hí–QY¿A!fxðßwká`†>}ëoâå^ˆêŠÙQ9»4ÊKNz`­»§YΠõù'¢ç5«NšÆXøÔßÌ;¼çÞÈ7îT€ È7îŒ"¢'¶ÆÅ,Žš¹ó¢ïøH «Ít(­X gà«SçŸ[ܤ+|vPt,,g¶þ6ÿá S!ÀK_¿#^~ýHlmEsKešû ô¡cµ n•"8Ÿ,`îþ[x]¤‹ÃÈ@O¼úèW ÿ„ù‹ëcÁâ‹¢¢²zÒÿ³‡ŽõG߯E_×+1<Ô£ØL›Ú-;bÞ²uq›f}¹½^d«O׫gŒG,*” 4EhŒ(~+.¿¾àÿ;–Ï‹çÎÅK/÷Æ‘Ž—£»Í'µ›ö‡nò•cÙ_L£Êú Q¾r£ÏØB†Ø 0ü±êMÛcùw(Ä è|bw´?t»B0nó×ÇÒúuI×`døx¼ò£¿·›×¾·ïÖ:EЯëÛXL,"3©|åÆX}Ën…ÐQ ŠáŽ~¡…¥e³Oû燞}Xq8Mí–±äÚ­ áÚ§o׿#°X<Òà¹ÿé×ÝÔèÓ~œæÂ†+¢ªºV!¦1(°k€ˆˆúm»bѺM !лëáX@²"ßÑ=m¾EkøŸy¶üónËV4ÊçÎWˆ`‡€@0}Fz¢éNoy׿ X@,†ÿ sç?M‹êÖLèew̬Þ|{¼ÖòŒB$À;\õðzy †ácf;ö ëÖ+D¼`úا—×Ó/ŸÄBQ$ê·íRƒ?“Ì›ø³¯|îüÓ^ÈØ‘kŠþ#9Åɘ·R!ÀÔ[ríÖìÊE¾q§bp^ —Bo_@ì( ? ÿçþ=ãhðgb¼•Ÿóñ>lðHÀôñR@=½þ^€…Â1ÉliÔÈ`ðGÀØylεSo¯ÇçtÀÂPà ÿÓãÀ—ÕÙЧŸ?Þ!P܃©tSoýmB("vÌ0 ¡áÿœMèmDeý…˜b^fdà‡±°C ¸x$`zôøúýâ0K  0­ºë)ÿáŸQ þ†f⼫»ìj…(ùƱïÖºˆéZè IDATèQ º3L02óìð`¡,ÐáNM½BL!w*Š{ø‚BcW@qXwOs”V,P×V½¾ž_€ÁBP<¯8µ»rÑ|÷U Qdê.»:**«‚‚72|ÿ¤"¼iñ7þ§Hç» ÿ†˜ñóû†+¢€4ß}Ut>±[!¦Àê[ÔÕ K¡²`šI-ˆgãÅDS7ü{ËaZT·&/½X!HRG®)ú؆^j·ìˆ%×nUˆ)à}æ³€ i5 ¢²z©Bø£?ÍŠÛñÉ?Ã: Ð7GiÙl…€7y< °üúúa<;xBLß Ì…Ç#ÓÈðïþLj·ì0üOÃ{î5ü —_oø‡3ü]P8¾óƒ½Š0Y½oý†¨Þ´]!ô¾g%™~v8Á§¿Ë–Æãßø3…Û§ÂÈ@OüÚg½|ÈpEËŽ€™ç“Ë£ï(-¯Šek¯QˆwŒ ÿþГáS9“?ü7ݹJ! þ `Bª7m%×nÌfƒÌñ€?rà†ÿÌ0ü†e«?jø‡IàïhæäwF绣/çqôÂ`Ü:sû!ÂópSàÀ—=J1“ÊÊĊ˯—_åsç+LbÐðÁÍQwÙÕŠ1!@ëžû„“ÄÎÅ S€“9ƒúò1pä°BDÄòïP„IÔÝÔè™ÿ´lõG£~퇦HiÙ쨨¬¶#` |,޶6)„`ÒÙ€€Ìëj‘ GD¬»§Y&yøÏ=x³BÌ€ºË®vǦي˯ ®PˆiÔþÐíÑÝÔ¨“¤|åFEœ‘§,p™S½i{”V,PˆIÒùÄnÃÿ +.¿>**«f@UuíÛ‡L܃7 &Éê[v+²IŠÅû]´ùsŠ0Iúr{£ý¡Ûb†Àßdª!À¾[뢻©1._ñÙÊ2Nx‡›dæ'€“×–Qëîiv÷‡ÿC÷_§ÓásAÓ1 ”ϯQo}6É—oÜéó€`®**p’NºÖ矌‘¡^Ã?6Ø•‹æ»¯Rˆ)üôx,`òy!àäð(€™âýÜTœ|v0éRþËWntþ ÚüÅõ†H˜¿ÿÉ—{ðf;&(¼Ÿ0Dàä¤à-»ñ Š0A}¹½†ÿ)²¨nM,­_§  óܼí‘äkà®7,XE¤²~ƒaï¹×Kÿ¦°Ù_¼ôbÅÞ³60¹!ÀÈ@B裘Dn´ ΂ºUŠ`øˆˆUw=åD˜àðŸoÜ©|ÀQ´šîÔê§ôÕ2­ziƒ"µ[vÄœšz…˜Ãÿä7õ{Àz1ý¼ÌnbôSB€÷³ @àd´@œ%×nu"h– ²ÕÕÈã˜>c;1^ T_¾C0üPó^>w¾BB€ÖþÐí1Ø•Sˆ XwOsò5p“ §«e¯ ” ”á¿vM;`M),Íw_%˜€ÒŠŠÀÛì¼8 €T2¢zÓv¨qò¦äÉkÔ¬/…¸ÞŸGôÛæ/@A¨X¼Übd1Šˆˆå7Þ¡ãäMÉšsÀ:“ÊõN Ðw#(bKê×X„pAš[ÿ'ÞkÊkNñ…‚€ñ©Þ´]`”FÄï+ÃØ-ª½ÔðŸ¸u÷4ǬÙsbŒ»rñüï®Vˆ 6â32„Õ®Œ|û 1NÃQeUDùÂZÅ£…«~2ò/þ F޼œl ÊæTFyE¥9¬öÒx£ýEãdÀ8xö„úm»<÷?NÍw_¥ü ÿ@!¬E6\¡ãoÜù½+Ä8­¾%íO+zñ6˜!‹ÖmR„q°íb 7@¡¨ª®µ.M 8¼ç^…§úm»’>~;qOqCVÓ¦vËE0üþ¬O:ŸØ?6oDAÆÀMÞRÓ°ASOÚdø_ríV…#ß@ŸºË®Ö\B€ ‡?8ðœbŒÑº{š“>~»N©¬^ªÿgjþÇÇsÿãk¨+*«dXûC·Çpïë¾ 0F¥ ’@€áŠÙú?>¾,4ÑH©¬]^8v¹oŽ×^Rˆq„úrÜ À”¨Þ´ÝÝÿqènjŒ¡ƒ)ÄšgÃ?P̼p|Z÷ÜgÀ8¬ M@05JË«’>þ|GKÒÇoø»Á®\ä¼Y!Æ0üXÓÒ4tð±è=ô´€1± à”u«A0ù–­½&éãïiKû9«95õþÆ`d ÇsÿeÀÚeå b”rÞ½‡žŽ¾œo½…]T/mPLžÔ¿7;žá¿éNI¬á ¢~퇭sc ޶6 Æ(åÚpŠ]€Iååió½ÙÑìÊþ ÿÖ» hèvE£Ô_ˆ]&UÊÉ¢meccÛÿèaÍ0âÚç+£sèþë¼`ŒRÞ±i€I”úÝÿ””Uw=å` :ŸØ­£l€RUU]+¥®g¾¡cúŽM!€¹m´Ê”ÎÌ‹ÿÆ6üÛ²høgú ôå£çÈ«Ñ$7µMBù‚¸°~mTTV+:“í­Ÿò󷨵?t{,\õ“ú‘1XwO³Gá,&Ä€3p÷?MRõÑq÷ÿk‡íËIp—²@Öl@¦ ήzÓöX~ã ¡o1ϘkÆÅKáM†ÿós÷ßðŸú@rèÙ‡ ÿö{³é†+á,ò;cd G!F)Õ~9;ÎÔÈ'š˜¥¼PÔoÛ‹ÖmròŸ‡»ÿgVV¾ ê×~X!2:dR\ì ð7˜Šu÷4GiÅ…п˜kÌ7cë[•aøŸü;³ºË®ŽŠÊj…È€‘áãÑòÜ£ ‘¡qÙêFùÜùŠRļà캞ù†wÒº{š£éÎU €Óy^&=>©s~}¹½>ùgøOfh$;Þz\î€â¶¨nÍÛ/ƒäíÝ^`<vJÀ;<`øˆtwx‘ÎèØúf† C?þn™^¾yŽsÚKŒõ3fsÎØ@Ò†ÿóìòi3C„¡ŸlžþŽ‹GùÜù±lõG…g:§ï¿NŒš¯¬úm»ašï¾J ÿ™ú ÿœ+  8BÎp~©>öé8€3ðæÿôxñßùÙúoø/fCÇú þŒ*pžXƒ³ /·WÀœ'€3q÷ÿü:ŸØ­Ï¢èlf¼çÖâ¢<ï¿.úr{cÎðѨ*Q³° $8ü»ûnƒ]¹·ß.Œ†Óð†s‰B³¨n"œ%xú·/K*¼ãÞmAÏ@ H·i0üŸ—»ÿ†Ã¼snéxY1 Ìâ¥ÇüÅõ q÷_ÎÁ.€ôT/mpâ ÞáùÿtÔnÙá„?î¦ÆÈ7îTÃÁèËü™žu±m¿s­-­_'8[÷Ê‹±0ÿ¢B@ð~¥åU΂„Ô\qƒ"œÃÈ@O´çOÂð_ÐÞº+ÛöÂ÷ƒ9÷(¬€Óµ?t{ÜÿT¬«Ž…%CQvr$Êâ„€ek¯q$¤´b"œÃÁ¯}6†>¦†Ã8­×城‹æ ż“ r¦sç¶G’;æ”ð5@ÒúòŠÀ{ vå ÿšÉ‚¸Ày‰u{lþÓÿvS\zAIT”•ݶÿ4•õièjIï[±«îzÊo4ÿ"h" rÀ2dáÅú=~ó—»bqE™BœEŠïðI@@²RÝ’ïhIî˜k·ìˆ95^t6}¹½>û§y4TsÖ:žÅaïµ|tw½¦H>HUO[srǼäÚ­~ø³艣­MšFMcA RàüÅz>9úsûâ`g—Bœƒ/ ’ÑõÌ7Üý§`'ÃÎe„“Ëû}8€ vå ÿÅ‚˜Ày St.Þ]Œ ô(Ä9ØŽÔ˜å‡'ëê·íR„³h¾û*ÿáÆ$CY?ÇsÏW!¬ï3®éÎUŠÀ{Ø Àz&-Z·Éƒ]9Í¡æpÆ#HÁðPóÝ:O° @F¹ûŸŽêMÛá,^úúIÿ¢º5NÃ?8ï…IénjTŠ?ÿ·üÆ;üðgiR)Ðâ¥;fh2áoÀ߀`fä¼9úr{âRÛêc)ß$$µÏÿ­»§Ù~Ž@#ÈL >À;½ùv…°öOÿ¹wÿuBH˜€L*_¹1J+(Ä컵Nˆá Àk-ÏDG®I!˜‘R—ê.€Y~è4¤¶½gÙ_°ªA þ†8»þ#¹8ôìÃq¤ãeŘBàwt>±[ÎÂc ÈTÖoP„3H}ËŸÆoú ë7øÃt·í÷7ãZ0­Úº]ÎÁ‹¤€»¨ vå’Þò§á›>‡ž}8Z<®0ο\¦‹wœ]j/’ö2@€ØtѺhóç\µÎ ùî«à Éß‘¿%¦CëžûdjaþÉäŽÙËÿNgë¿;=†ð7…kÃû |,»rN†³Hí]È€‘¡Þ¤Ž×§ÿÎÒD&¼õ¿î²«SìHÇËŠ”‚¼ÅËPT|úïÌRNöë.»:**«S<œt·íW˜â¿3A€€™áÝRÙ—Úcâ³ü°Ù•ZŠwÉ'﵂aøO9Ù7üOýPø›£øù$àÙ¥ö2@P4æÔÔ+‚ úÛÜÑ1ˆ@Vÿöüý¹fL6Ÿ<·”vø€ŠÂª»žR„÷ìÊE¾q§B`ø‡Îkß­uN‚³° /µôÎÝÿÓ¥¼õßÝÿ©12|ÜÐB€L[¶ú£Iê_ ┿"&ȘŸÿO‰—³`øŸžA£å¹G„™V>w~Ò!@ëžûœg‘Ò'SûŠ˜€¢—ÚÝÿ%×nõ£¿Ï/«  HéotèX¿BLbª¡ƒ9@PÜJË«üÊV½i»íÿïÓÝÔ˜ìÜÝÃ?¤ªõÀãBדIá&Â9΋ÛQ„ Ke×xæ€ek¯q6g˜—²œ.÷àÍš5 ÿhЛoW&dèàc12УgPY¿!™cMõkü±RTRýìŸáßðœòZË3B×– kºs•@¡ó_&ª7ßnø‡ „þŽ…eÀ™¥ô2Ào,¦ð€€â½({ë=Üýg¢=ûp¼ÖòŒB@†þ¦q¯WýŠÅÅçÿ²-¥ç°F#Å»ÿ†ƒào›©‘oܯ>ú…€Œ)‰ˆ“€lHm›NJ[°Îçðž{#߸3©cž¿¸>–Ö¯óã…ߪ^?Qµ8jÊÊcñìò3þï9>]ÃCïùgíCÇ"""7x4þ(ߪEL`:1#ÃÇ£å¹G“ÜpQœ(+‹Ò7z¢ôõî ýÿ¨Ÿ3ï¬ÿÞðâê¸óHþŒÿÞï­Xþ÷Ý~¤בk²nN@iÙìhøàæ$C€‘ž(­Xà$5v´¤lp'ã-ÝMÉó¢º5~xÃfý‡Ï!¶æ:bö¡Ö8¾feÌzýH”žaøˆ(kyuzš…7‡ÿª…qtQuŒÌŸK4Å‘õ?¸ï‡ñ‡ë7GDÄ‰Ú cèøP<Ü$>ó÷ù1 Hÿ‘\t„ðt¢!@Ššî\eÀ¬¸í‘8tÿu AÑÉì#¶ÿg› Ñ;RÚ‚ööE×VVƒüEýúØ´hIÑÇÀ¼ùqtÞüˆYÇ**âXEE¼QsAÄ¢ñ¡ÿñ7‘¿ä±ê¯ÿÔ^Õ­‰ÅK/Vˆñžï}ùh{á{Éwý¶]±hÝ&'@¢=XMƨ¬^j¶Ê;(:ëîiV„7¥x÷ßðoø/Vôoîˆ_ýÛÈÜqU튣ýçüß©<Ò/ÿü§bÞK¹·ÿÙÿÔÕñ¿ÿàĘîëFÛ~ÀDÎ÷Êê$;÷àÍQvÛ#^Àœ¨®–½QYíåêY‘ɯÔ4Xœ²Ìshï½ ƒá¿pýÇó…è\¿9Zù3ñÉŒáªùIÖaö=ïþûj–ÄÖ_‰Îõ›£sýæøãþy'‹µ h¤Dmmòã¿Oí–Ékjgy7y&ð¦Êl³ýÿ©mÿw÷_Ó_èþ¢~}¬ºòCQwt(J_éTq™77~õßêU kª5µÐλ’îÅ”†nNò¸Gzüø$!«/>@j¾ûª¤Ž×6UÍ}!ù£Ú{{(½ôéæ(Ýû#E™Ÿ˜õvÝ«z™‚X'f\ŠŸ:øXüÚgýøï’Ò£©©}u,«E.¥r¬¸í?xDt>±[`<økŸŽÎõ›ãW—xe¦Ý¹|µ @P½I‚õÐÁÇüð (‹—'õö´¥óIÕ ’a ^y9J»»ã¿ù£í'Qà3ÝmûaŒRÛàæ`R-¾âEˆôÞ¶[¿öÃ~tÃÿ”è\¿9ý×ÿ[¿õý\vADD”õ (L~â»ÄñáñúêõBë “$¥›£±þ6EÈ ¬=f>ËSœRy` "ÒzÛ®gÿ5çSåàM7ÇÈå—Åì?ûˈˆ˜Ó꥚©™{´'.8°/æU ¬3SfÙê&u¼©}¡è|j·ø —úm»lÿwaES>a?ÿ‘ŸŠöñ¹XøB{”>ûBÌy¹SQWq´/"Ní9ð/~'~î¿ (Ö›IS>w~RÇ{´µÉþ.K®Mc×fJ_#‹ÈÖÍf€?¸‚µhÝ&?vD´î¹OÐŒÓã_üR|µ~uÌú·ûsf‹ðƒøÏí½v0©RÚÍÖþÐí~pÀÄToÚ®12Ðcû?ŒSçúͱú¯¾¥?|Q1õ9ÃÙ 0%æÔÔ+BDüÚgMø8†8ƒãuøc¿äü±þLŠ”Bí‘?8¦Oiy•_’Ìénjt÷Í÷8†ÿ‘ ¼;„ñ›s$ÿö¹„uˆÑyõѯ(»¤ò5€ÔÞ ( ËÖ^“Ì–ÊÚÂU?™üðŸ{ðædŽwQÝ«±¦{BþŸ[¶Ç+7&†ÍÒ×݉br´nþUE8‹¡cýŠ0 ©„ÛùÆvy5 ~¦JÊÔ—$5üGD,^z±“žq{åÿ}|ôG¯DÉËmQÖm(aò”wvx¤ä,Z<®¼‡]d]eõR€‚)i¸VnLúøŸÉÛÊ9»ÿ§ûxyUt®ß%ßünœì9e=G…©û¼éóŠ`]—”v våüàoJå…ÖÀ„Õ~ìÓÉ{Íì“1Üûº“MöyüùÏÞ__}Ut­®²|” ) Sªò…ç£sýæø¥Oܤï2ЗWÞöÒ×ïP„7-¹v«" €Ñ¨Zqe²Ç¾¨t8z=Ô1{ùŸá¬:×oŽŸn=õ¬iÍw›˜^_nïñHÀ»´½ð=EÍõ=‘wݤôòâó)›·H2( /Ÿ‘T¶Ø”V¤ûE¾q§“Ãÿ9†ÿˆˆe._̬Ã?ûiE°VZJïºéËíõƒ“YYxù¼Š‚R»eG²Ç¾vÁPìþ´î*¹û¯¡ëðÿÆå«N]¼†O(3jNkK¼ðóŸµÀšåš÷>ù½Î…ˆS7´VÜöˆB ˜L —Â/˜1©>/5¯d8VVzŽô™üñÕ£sýæè^i,|¶YA( ^z!z–_xû—Þ ìd|[e½¯•!`œRØþŸòÛÿæ îþ×I³»ÿŒÆŸÿìMñO–F_Cm,Ú÷¢‚Px!Àá—cÛ·¾m'@/]ûÞ12ÐãÇ6§d·o/ò›Ð Æê[v'yÜ c0.š7ÛøwN4Ðïò׿¶-6+‰Á%ÕQÙÒ® ²ÿ•芊׺„¢rè’K“ìp Œ° à-Õ›¶'qœ]-¾þ ÎDITÌ.‹ÿüï¶)Ä{ïž.j{]A(¾áî¥ã©u?ž|0t¬ßÉ8/<%Õ\#ÎDÌ)+M«9v÷ÿŒÜ1³ušì¸ªé‡ñƒúåIŸÓ­w"¸sjê“Ù€`J¥ô ÀÎÜþÌcª ciIIÌ+9f%Âðoø'c>”;ûV¯LúÜlÒ—³-<"bùw(BÆó,j@8rØÂ˜Õ`VIt·îw’'.õ&ùãåUN2iýƒñÃÄC¿¾Ý"$$µÏ €1[:'âÅgMæxmyäLvßø‹ÑwI­BI?~à`4ýøêdC»\9eýmŠ€`¼RÚþO¶UΙßü‹"a©7Çí¿ôé8:«$*_jw2Yë~x ~øÓßþ—¿™äñ{! E¥EºƒÑ€×úü“ŠeÃGÕ€dø·Ä±òÙQÙìó‡dßëñ¸aICš½ŒžUYù‚ÌãÈ@šLZ¶öS°hõfþWÜöH²¿ï÷ŸLçY)[O7ЗOöØÿnãQÙÓ%‡m‰$uO<•ì£vœYýÚgþ›î\凼£²~C²ÇÞòR“ am/|/Éãþã«7ÆŠºÚ8ñFoTt HGõ§BýC»õÛv)BÜý?]îùï&yÜ/¯Š.[ïåcîk¶…’®ßÿÌo+É\#÷ÝZ燎ˆò•3Œ¾ €sZ´nSÒÇŸoÜé$HÔðPzÃïÇË«bׯüzŒtuÇÜŽn'IûÜ“ÏÆ/~âç’:f_ u«oÙ­8; h‚³d÷OýL”=Ós^yÝI±³d¾õ ˆÕ4ߣÌ`JÙþOÄ©gžgumí4` IDATtÇH™Ë¼evÛkɾôø@:Rº‰YY½TÀÙéx9éã/>á$€ÄÙpºÂòƒ_û¬:¼ÿ $«/·7óǘÂ÷Ǫ»mrÇì'œÛá­·(™7tð1EˆˆªW*€±(-¯ò«QôFzâÐý×eþ8Sø¾ñX¤x׫ýšüðpsžkŽWnúTRÇÜ‘óÜ÷óÈ\J+ÜA0&ËÖ^ãWˈêMÛ“=öÞCO;ȼÎõ›ãè¥ £0û…\üùÏýJ2ÇÛ$çG'Y@@’RÝUyrP ïþ½lYTîköãÃ(ýô˯YÉ´î¦FEŸÁFÀ¤ðö̲ª ’üm»ŽtD¾qgæÓVÆ´u®ßó^hUëPüËŸQ„„eýÚ™{ðf_©ª¬ßäqïÏûñSkè»Ëõè¿ýR W{_ ŒëÚ¸ÿÅøW¿ðI… ³^{IÐúü“Ék±½£NÀŒ(_¹1ÙcÏïµåÑðŸmëÿû·£,ßëLJqúÒ‹¯[#Éî`¸ç>EHÀÈP:}@±½£NPh'P]mtæ²ÿ™°Ú}:Ý íÿ¤Ë'ÿÀßÒX ë÷ƒ'dèàc1Øå% "">ò±ÅÀ‘Ù?N/?ɶ ®P„pg ˜˜_¿æ§’8ÎÖû±ß%…wèx£² nUæo=ò7ÎJŠ^Uumò5èÍ·'wÌîþÃ亷z™5 4\þ ÀT¨^šýïIWÍÉþHí–V‰ ++_ ñZË3†`bëikG2[©­™ç“õ]G[›üÈ "â¥öì¥æŠ’ý}SøôMýÚûC˜$C‹ÄÇË}UƒlièöäkP½i{æ1ßÑâd@Úz=­dNçúÍѵñ)Lò#=±ëã7&q¬9w…S’ÂM‘sY±b]æ±§­Ù‰.€ˆÒŠt·ˆç¼Ù €_þWóØ÷ýð0EÊ^팿øÍ;2œýG¼>%ÃG»“>þ¥—ýc'€s)¦+¤¢sýæ8Ö°T!`Šmúî?$qœ¾ž’ŽöƒIÿÑŠ ˆhyö›ŠQ}¹½™?Ɔz\Šæ¶t(L/ÛLKÖ_˜ú®È#'Ê’xHVëžû2Œ¥e³“ÿúÒù¶qçúÍ1²ØWŸnŽœKõ†ë ‹»r1tð1…H@Û ßKæX‡?pa”éñ£Ã4jýÄ/gþ=ŽC÷_—ôñWÖoÈü1ÚÝ,€$ïÏ+B LBMkçúÍ1T½ÐÓ¬¼½K ЂºUà”ü^w3ÈžyϽ 0Rx@JSKÖßYR½´AïW¾rcš@ãN?~Æ 7|L’”§Bo 2kå§¾ª•ú]Š¡c}N`Ú¤ÄyÀ)‹êÖdúøFÒ~—Ìâ+np’# »J+¼1ÜðŸM©Ü­zeëoDîãW9é¦k@\zq¦¯ë™o$ýû–/\ê$GY’z²M¶Ì~îPÔû)…€ð‡Ÿ¹](zí¥}W­¸ÒIùŽ1ðÚKŠ@&xö ˶'³ÿ õ#/û¡É4»c³£§­Y0YŠå“ å;™Ùt´µI2ÎsªÀLxcå¥ñÜ'?—écìnÛï‡H-(–O*Àû ô$¿µløÔ/~2ÞØ°J! €,<øb,yþGÙ¿–&ô•È"79$ªzÓö䎹÷ÐÓ~øŒKåîÿ½?z=îmöƒÓ?<<÷hò5hø`¶ÁêËíu¢ƒ€¬Iñ%'¦ÒñÊùÞÏ‘€Ò²Ù™>¾áÞ×ýȽŠÅËðneU$wÌùÆ™?ƬŸ˜ˆ­×Ý=ëmÿ‡B4»¯?‰ãô®•lz£#éãOq—lýÔǯ+Š@P Rx6¦âÂKüДõïñÀ«±`ŸíÿP¨†çÍÿæ… hù`öwÉ®X¶4óǸÿ…–XR_ø7Æ >Hå )ð™²Æ) t—ÍŽ_ûÛPˆŒ«»ìêLß`W.Ùß6…]²Gûºý FÇ(V#=Š@&ô®Z¡PÀ.èéKë"ã**«3}|Oìö#gØSßÙ£Þ²¼n‰"dqhòÀL:–Æs·u寍j>ä‡B_Ìeþe€v]e[ ïM:ë߯Çd¤¥ni"dPûwþD2¬õÀãIçGKýØ0…<&‹ 1ÿì~EÈ ¡ƒ)Eoä‚EŠEâøÂªÌ£]€¢×™¸6­ß}5Õ Ebö½™ ²Ê§³£áòO8·bø\œIÖßHLÄÂæ— @¬¸üzEÈj°Áo‹(pY#ñÙØ‚ ªáùóì h¥ü)@0&eýG3ŒBØì:ÞŸW0ݾô›¿¥L«C÷_§ €â32УµõÝ*±¯ÿÜVE@Óaàµì¿8í†+üÐêã/w*‘Š /Q©Èw´dþ}Ú${ªªk!£¼@ ŠßÐÊŠ”–g¿©""zÚš³?,®¸ÒM&éx9óÇø?~QÅ®üà¡LßÈðq?2@Ö€ŠÅËýB((Ýmû3Œ×ýýs~hÈ€oÿbf­å¹GýÀY –Ô¯ñ eDiÅ‚¤Ž÷hk“âý{͵+dÀÏë)è•¡x(VíݮԛÏþ`üÕß¿× …¡cýIwÖ_ÂëKJ (¯µ<“ùc¼é¿ÙV ‡Ö'yÜóªj2}|)|I !’k ÅøÌo)BÆ”–ÍÎôñ ÷¾îGÞCO+áW›)ú(@S¥ý;¢¥_ù™2bhVIô_ü(ëéW 0eM×ÁÇ¢´£ç¤"@ Ÿ<Cq2fõô)E'߸S@ÀT+ë?¦ %'OÆÉá¥È¨¬ 0ºñ?JNFœ<>¬•õ/¦Ð_]¹Q K!ÀÉ“qâøq…Ȩ¬ @Q(ò²¥¬èËgúø~ðÁÕ~dÈRqℵ@!éËw(B†{í¥$Žsþâúä~Û¶¾—é㻸ö"À©àÔ{¬ÍÓ¯bñr$Œ¤ñÂ¥ê%û±3æã?zI 3ÃDIœŒ8y2º×^ª ÓlIýgv|È[·)>åsç+BÆ”–•)dÆ©;ÿ%'#Ê{=šEÓ_­ô>XÌ€i0oÿ!E€ y{À‰“ѹ~³‚@˜_¿^d]׫3`òš»’Y§†ÿ812O|ì'ŠÀœšzE@u#C½ŠL¾“qâDüÄs?R ŠJwS£"€€Ñx÷‹Ë^ïVŠJîÁ›ŒÅPùlE€"Q¾p©" `¸®2¿ü2r KÂ×jôÒm_5ްÝ!и;ÙxÄ2°i’ÌTÅM{Ùšd©ê ™2/µ3PSÌL-[e²[æewI:EuRÙ¢]]LÍlv¡W—…» mp7‰mÙ"]kÑ‹‘®d_ûZ‚ýÃØ#Ûz¹Ò½ç<ŸOÕ¤ SÕ“ó»WÏùý¾÷9Ï©éz'Õ×75yÚ‡  R{v+°@€]RïTa\H…%W/W$v;€˜*Œ)€´›<1ª €Ê–iߪ°ˆ®®kPiWµä*EXK²Š0O1¾Ϲ"PèQ$Õ×4)‰3tãáø¯Ú½@娪©W°N-Y¢%0”;$ 25vî ¯¯´{Òjäº•Š°ˆ #Gƒ%u_Véo;÷‡3×(¤Ð+{÷((µšk¯Sé¯òRUn F/ô+$@ñ£AE@IâÀ’ìãE€zµ8žÚk»¶õf0©1²ïeE@ÀâXòÎE¥.Ó¬¤F±g·" ¨+oø-Eçä ×+€JQßÒ¦Õ_Z¦@â|é𻊀 RdšZ¨?øõ5Š#ƒï§þO ç|Ð `¡ü»¿úsEH€ÑþCŠ˜ŸÉ//Wi÷ñ—½ ÒbêË™ðïV7*‰´äj4€ŠP˜È+BZo ÕÕŠ)qÅUW…üÍ… q2í[CUM½B€*ßê‡w¦þûß}ÓRK:»Râtƒ_PIh°ánE$Cmvƒ"X?ûÝMŠ)ñѤúúZ¿~G”Ÿkñäq½ ÐlÎßü/ÿÖ )ñGޤúúª–\åçšzß—h6Kãã• ƒ4øÅøˆ"¤Ðñ‘œ"JtcèRH§ó}©½¶e+²>`T†ìý/(BJ­¾)ý½zûz4PÑš²Ö©´ª^ã,@ÂÔ4¯QëÞ¿xV(‹ëî}R@Àbú¸åZE`Ñ-¹Úë+I¶Þý?pi­7Ý©@eÝú)$ØØÔiE ‘ªj꣼î©Â˜$ÕÒ‡‘l»ì—ãyE€){/õ×XßÒæƒPJ§nüÕð½\gj¯ïÚÖ›}ȤΉ¾®Ô_c¦©Õ-’jjÒöÒÔÆ_mRH ýUŸ¤úúê2Í>dRçÔpNTú ³Þ·ãÝ"ëCa°öK¾è@÷üõŸ+$ÌñïÚAÀ…ƒ” ¨@»ÿöoæÀ­ë!Ås]Š eÚ·*‰öÒH¿"@Âü×?üß!ÅŽ¤{›xöþ¢ýl‹=»}Á@’5nÜ’úkt@º½ZµÕ TŒ–ë¿©)VÓ¼&Êëö @‘ù—ßÿŸ!…–\½<õ×øáïDûùÆrÀ_ê §[Wúƒ*c@¬Í(BŠÅúå^BùÁÞT__KSC"®QP^}ó€"¤PUM}ê¯1í[ áé|_¸ª·/Œ]wb@…Û?1ª@1¼0„þùïýNª¯oýÚ¶0Öß-àòînß”úkôjH®ÝÿÍ¡þ½÷*Üðd1Õ×ËÎ+â3²ïå8ú‰7ßNõõ}ÐÛ“ˆÿ;à•ŽôúQèñACB}÷ÉG*ÜЊÚð‹ñ…Hó8ø¾"¤”S2Óýß?0s­7Ý™êëøé}ȤŽ_£€JñÃo…§ó} ‘b£ý‡ĪY±*üÚ7nP±ˆ5Ùô*@Òâ߯nT¨PÿðñIÃ?z&*Zcvmøû8¬HùÍlCú!Ïø #ðç³C Â¾|M8¹~MhûõÔ_ë²YxÊÅúÒ£fÅ*Ä®6»!¬~xgª¯ñXï>t$þî«+*©‰«¯ ' '¢¸Ö¦ìú¨?ë#û_Iý5^½r½?j­1»VÀì¤ý€‰\œ¯;¬Ínðå&î~åEE€ RU·,|ÿ¿üg… j®õºY*±¼ã”¸8(—܇Ç«ÅñÔ_çµ­7û°S.Ó¾5TÕÔ+H“SÃ9E€„kìÜ>nºV! ¼±/Žuu™fvÚ?ãÕ·F{íS…1_çËöú”€ŠÛ.€+ùСÌ~ò•úðÐÀ; á&OŒ¦þÓþ˜³ ÄÆú»}J)‘ïxV îÛdzÿ³ŠCÌÛÿOÏû ’%†Ó‰ùLcç.E€2Ê_÷UEp%%ú^úSE@iS½f“"¤\l福:íC‡2éý‡êBZ{v+BZÖæý?g\wï“©¿ÆØ¡ˆíÕG ÇýaCýøt¸óÈ[Q\ë’j§Â ¾ïKÄô|Ã'E¢-mȆLûV… 5b@ ÒüúÛ?‹æZ³ë¾ýç=Ú(õרöø^Ø ø¼©âxØxë¯Eñ¤ý”ÌÑ®ŽhÿؾrÇ[qH• yµ',¦õ5Ñ\«Ãÿâ±´!« ø¢•¯ýħ•?ýa´×ó ·Ötzhà°bÔ‹äk¯¿¤"§†é.êëþ{ŸV 8è„´‹í0@Àâ°ã&>ƒ¹ôöؼy{ÔŸqa Ç\^Ußa€X¹S'¢¹VÛÿÏ8>’þµµáæïDý?Lý5¦ýñf€/\ÒÏô+©óÐÀ;BX`OçûÔ‰ùñÈ©ÂXxñ_±ÊRH¸_]õÐÀ;!¿ªÉ‡ ÀY¤QìoG*{Ï—!Üwß¿P­xÒûác}uÕöüÌ,€˜ÎÚ°ýÿŒ©ÉÓ©¿ÆU÷<õg<9þ¡/:BøÉk¿P­ïðkŠ)[”¡ô;wEs­+oø-ø§zßÞ¥)7~ä—©¿Æ˜áîÝŸœ7Ö *PUu]ª¯o"wÀ‡LêÅú+–³ tb:ø/„ª¿´Ì‡N4òÏ*€KIRª2_+×Ýžêë;Ñ×õ] ϼy@¼x'º¡Ê-Ý{!Bî¡€€T95÷/„±?ó“XwZ`þbÚúóz õòÏFT¯Ùä‹@ªyæîÅÉ1E Õbø‚´šQè‰úó½á¾ä‘ˆõP«‡ÞÀýÉÁ¸öëÿgb8ý?»!I—¤½Æ]@YÄpòiì<ÃxḞZ `öbÛúÏç9ý?£]Š"…‘£€[#:0íœ|JLbþuë¡wBaÕW| Àðo}äÌgþðÎèk{þ¾Ô_cL¯L;(›ØÏÈÞÿB꯱0‘÷E't:&.aÛÑÊ@j®½N@@Œb?`ùúöÔ_cÿ»oú¢jÙŠl´×~ç‘·Â? aJõt¾/ºkn¹þ›>øóÄòè\UM½¤Ùo¾öÿ†¿½/Le–+\ Æ­ÿKªëCMmƇ™æÍÛ£¯Aì;`p·Ü¸N"°~[wê¯q0×åƒ!4e×G_ƒ‡Þ '–×…Éküúgźõ?»î[>üóÄòëãÆ->l0·:*BbØw|DÒ}–îBø•ÿç¯Â•5K} „Ð1:åÖL#í§güô‡>d„êëíõE z¹S'Â÷rqö7_¿Ã€hu?q[꯱¡uƒZP:^˜ŞݞƒŠå³>y\8ç–î=!wê„BõðK÷žh¯¿jÉU¾bµ™&EÀôÞûñ£Ñ×àÆgúS}‡_óeç !À'+¿ªDgÿÄhÔÿ]P_Ëóÿ„0´g‡"¤LœPVŞ݊€8RGÿ±GˆÎGÞ²ö¥>¾œIý5¦ýæ4T°›×þŠ"~áàB?òq¹÷ð^E Ú{c ƒ€yÙwè¥h‘ð^Üøø%,„§ó}Ѿøl;z8¼Z·æççÿðNEœ1‘;} by/®Ã5ÄÓ…~%†á?æ×ý-oYëK¹šk¯SÀ}/ý©"ÄòY; i¼Z÷8©õ£¡\ÔÃ!¬húš/Â4lÿ‡d›È Xi ÀA€gÄð6¾È.€3xG@*‡ÿ‡Þ±Æ÷wÀöÿB¶(BÊ ÷&ss"€$¾nK;5¬ñÅx~@¸hàq ÿ.¶ÿÇÃÙe˜±ÓÇóЉc½ûá~!ûÌ«Åq!‰·bÔðom»¤‘Á÷£¹VÛÿýÐ… $ì by À+5Ê— ;w)‰uç‘·¬iÖ´Kí?ÅuzËÑu¿®)Ówð  ç7n\§@tœ @ ¯¸œ˜BðXÞrt9#û^V„”™Jðk]$õäÅÙú0ý7ç=¿˜}žƒ1ü[ËH®LûVEøT ^{þ_°(j3MQ|H¹!TY¿­;Šëô€ÆY@jîÓ§Nþ­a0­‰ÜE@Óq@ÊË!Éñ£¡\¸¥{B„Z®ÿ¦"\FLá÷ª{õ‡8v¹ú¡RsâMŸ‰eÓó Úô!Àþ‰Q… ¢Ü{x¯ÓþÏSS›Q¥"]‹G,»< ˜;¼r§N(3ü¿šàC ¬Yîy É X^˜²£ƒ£45yZ4Ô3vK÷!e·íèaÃ?\‚ÇI³¤Ï vPYÃ`aL>Ëé¹½o;8‹Ù‡ œÃÿÓù>…8°òòbúõ¿zÍ&8`fºkS„O9<õÅÝyä­Ð1:¤,šý£¡±s—áßÅe¬¼çß(BDê[âêÝÓðzg´«C"“;øsEÐ`ÏÚ÷r¡±s—GXpcS§ÃGÞRæ$¶ónj³|èÉ4µÆ5§ô°¸b8°øÑ úìà÷ðÎ(®s²èÑ!ÀÜy$€…þ¯ïú™BX—æ$¶€ÛöÿÏ;üÜEH™©œÿ’øÀA€é3²ïeEøTL)º70wy+4vî cS•¤t;wþ ÿsV˜ÈGp¯ùÃøà?õq¾/{v+¸‹åçÝøL¿" áž¡ë»~ælæ­ct(4v: ÔZ4?ýï¾ÕõV¯ÙäôÿóœNý5zE¹ lú¾ÍÖþíÛ}kI-»4Þ¥ð½\§G˜×ðÿ½\§BÀ,ÝðÀE8Oþ€žÀ‚™Šè]¼¯¿ZÓ¿-ÜëA0wyKÀ¬Ý{x¯áßúS±Úë·uûÐÏsj8òÏ*DʤåÑs$Pm¦)õרó—ß÷AGjjÒ3Ü3±¤Ú6Ë™„»Âþ‰Qo à’Înù5¢ ÿ†ÿR²õÿó =©¿FÛÿ,öB[]—êësÀçÅt@ïÛž»‰ìºo)Â,‚€[º÷˜Vcç.¿ú%sÕéã!÷ü} €ÒZ¹.ýgx ^Θ¿ÆÍÎ-Ý{ìÆ9?Êù>XoÜ¿(¹““SŠ€`1x`út=Ö¦çñ64å¥Ñع+l;zX!"ÿ<4ðŽBXgП@t³¦T´Ñ®EˆTîàÏAs¾`žÎ÷…{ïUˆÈìŸõ«¿õeÁøõŸB({OÀœ‡@ÏP}NL)ûdÑ# šô…õjq<4vî2F¢ct(Üyä-…˜…k[oVÃÿ%5oÞîÿð»ðÔ]©¿F *Fløã#ís]ŠÀ¢8û¶Òåìsþú››ºL³"pIW¯\¯ç™ÈP„JÛŒiïèKO*B¤Žäa옟ó_H²Möœ¿õdÑļõ¿6»Áàüï‚_ÿÀüå;žU„óÄvØŽg*5íå~4$|Jª{ï ×wýL!¬#‹¢0‘öÚ3í[}ÎsjØ}@Yx À¢ šwÍû|=4ðι­ã L†³¿ú¿ZW ëÇ¢9–;íµ¯ºçQ_€ó íÙ¡ T =Šp»ÐÄ/ž³‡RyΞêߨ¹Ë¯þÖ²p`-!„0U‹bÇjŒÛÿÓøã² ªªëÐÌ/0»*ïópªé,©®W„YŠ9œ^¿­Ûà<^ý‡€EµrÝíQ\§“U?/¶gï옛–ë¿©%tþ«s§N(È";ÿJ+»î[ŠÀŒUÕŒÎw¢Ï[‹@Iåž¿/Lލç‰ñÙ»‘Á÷}ð³TS›,[º÷œF½9`ál;zø\ýâ¿0ìš½˜CéÕïô¸ÀÀ‹(€rŠí À˜Bâ6ÚHæhðÖÙW6vî ÛŽV¸÷ðÞÐØ¹+<ïS ÃEÏD{í™ö­^ý©†Öø>÷´Î”v¤¥Á_±Jblܤðhô+ÎÓù¾Ðع+ŒMVŒ98¤8ÍßšP©Žõî‹öÚ—6d}.Ðù`K×Y›iòa *[l»³k£¸Î©‚Óv?·G˜Â; @ß×wý,4vî ?òÓ™ üží·¸U¾†›¿ãK¡Oÿï;ø†*Á»þL.Û+5`óç´ïÅõÐÀ;ç†Û{ïux`áGC9¿áŸ„iÞ¼ÝáðÃTŠ?ÛïB[âãMÖ›îLý·ã¹N4ÌÓÙÓ¾)‹ïÕâx¸¥{Ï™` ³2Üi7Õ.âÚs§Nœ»v ÿIûšY}-àêz¬MÀB*öìS…1 ôn|¦?šgÐÎoÄ4²ó„åót¾ï²Ü=Ýü«¡¹úK¡aIu¸vù5á«“WTüuM/ ÷‡‡Þñ!þ ÿ)²|}»/ÂyF»:@¥éÝÿ“(ŸY˜©Ö¯ßzß¶ »RM7D?”!W,| IDATY~£nE!„ªºeáê–ÆP½lY8¾lÙ™û’%aÉ’«Â²Úºpý¾®°ä£‰B7.S+›ÂUß}æß³×~úß8M¨pÅ™ÿñIý²NO†ªÃŸc°ÿ77„÷Þ Ãÿ¶!ßðO,߇ÎTa, üô‡qô f©Ô¹"„ð‰/mº¤ý1€õÛºí¸ÈÍ(Æ­hšÚÒðëVúü“꺰aÙ5!»ôêé×Ò«ëÃêšeÓþïF'ϼÁ`ät1 OÃ/ÆG¼ŽÏ:i}ŒT¦}kXuÏ£¾‘ö\f©ôñ‰ÓõX›`±Ö㥄éòjqÜ«ô°>þ ÿÀçx ‰ ø¢¶Ç÷*Àß´á¿$ ÿ_ëŽK‰‘ö-p¾¥ YMÀßò<OW¦U8ö^4×jû¿€„¨Y±JbnúÔƒÁü 3¯á¿ïðkÑסzÍ&_†iäøÑ@Å‹m@cvm×yj8ç/xµÙ !{ÿ Ñ]·]¥ ,­_¿Ãßm þϸáŠp©ÂXÈw<Çzâôþh+M÷·ù /¢¦yM”×-X˜ HÆð_µä*… $œ)4=Ïþ§[,? …b= !Äú7jø/ Aò™á_¨HS…1E¸Ä \ó†üm2Þbò´"?"BUu" 2ÙŠå.0h€¿I`¡MäDs­+×Ýî$_Œ¯txÍ›·G{í…‰¼/€Rûwèo±´¦&O‡Þ·wùny“ÐôßÂX8òÔ] *Açƒ-Šp·D{íýï¾é ̈áÿÌð_›ÝàË0˜vœÆúbL? H…¡=^Us1ë·uG{탹._€B "`ø'= ÿÓóÚi æ1€ôxñÍQUSmp|$Æó¾Hö½üëwø[[@ÎñÚ¿KñÚi3¢HdißåµëÝç €Dÿ^ñÇBsòÿôF»:¢ºÞú‡k HGóà0@B«îy4Úk÷ëÎâ„ËVh ¡ÔW†÷‡…óÁ—3ðÓFu½™¦Vº }b| ¼¼Ÿé×ä±`š²ëí€ÿ¸/,†˜ †¾ƒo ɦ cŠ€fÏàþ†`<ûq‡ŸÛŠ=»£¹Þªêº8g‡â¸€tŠå1€˜^Ó2W±žÀâ0†ðwS©Âgxöÿ"Caa,ªá?„V®»Ý/€d:úÒ“Šp 1Ÿ é+Ï@ø[q¨<Ùû_P„‹Þ÷rT×[³b•]€?îäÊw<ëþŒX_ x–Wl R,©®÷7bø/‹åëÛá"b{½tcvm”ŸóPî ±ë7_TUSõõ{5`yBC|ñï"»î[ aø_t~ý¿¸Ø^ý³ÂÈQ¤…7\^ÌoЖwàü-XóË˯ÿ—{þ>Eˆ@Ìo†»Ò‡—X ÁfÔ€>¼SCHYþÿXëË#ö.åÔpÎl€’Ê.¯6»Ac¨1,ë ´l…¨1øc§2t?q›" €$‹1É­æÍÛ²iÊ®7Åðá¿"¾‹‘ïü»”Ÿý÷ë¿ :ùÁ(¯;¦?vIîå5nÜ2í[5‰”}@2$á{ Ëο‹øé!1?ÿ}0Ü{À_„VÝóhô5TÎÀ¬é¥çäÿK+öìŽêzýú/ 21ýÑ{ËÌ´=¾Wèa4ó ˆ–7ÌLUM}¨^³Iлϗ¡Â/ÃÎòøÖô ÿ—ùÞ¬¾énÃ×`ÃÿÅeÚ·*‚áÿ jV¬ŠöóÊò¥¿€Óˆ5!Ëö†±þxÞßöøÞ°´!ë ï†9'v¤g˜Àß.þ^Ó2ü{íŸ~Ælóy~ýÿ";øìÆÑÔÕõv?q›}¼Nè‹èÔäi…HÁ0xöÿµ\ÿM‰àsÆðŸV†ÿË›ÈÅw|Uu×Ð×sñ§†m#œ©Æ[l-¼@ïÛ»!E˜ÞÁÃÚyîÿò¦ cáÈSwEwݱ=æËåyà"l•qÓdz˜~ÈÀðAÜ»½¾/u¼ÒSS*£]!÷ü}f3Mô–(±›ÈðFæ= ¶\ÿÍPS›QŒ:?àTöçƒáßðÏt¦ c†ÿÈä{}ñ/Â#sbÛbÑ÷ÒŸúÂÏBÛã{aýï¾ y…ˆ`Ø4pú,X¼u•‹s6ÏÌõüå÷!21l>[v0­ú–¶hþpŠ=»í˜oN¸t³º¤º>d×}K1">/dwÀ±µ?>þž.­zͦиq‹BÌÀhWG(öìŽoÝŒø×.Í—P³bUhÌÆû¾hgp)θ¸å-kʦ¯)D¤l[öwÄÜ ¾Fû½·ûrÖoëU5õ ¡_˜cfÍ€K5q#GCˆ8€K¹ñ™~!ÀEœm^ /qºðmãùp¬wŸÂø¹ ¿úÏÌê‡wþghª0fø7ü#`6‹GL@¶Ø0K™ö­!ßñ¬B\$í?äeB]¦9Ôe¦ÿ æºÂñ‘x^GzmëÍ¡.ÓìKóà‘Å™ëz¬M@”ʪ{ !!À%ÙÿŠ€‹jÊ®!»>„ÞÞÁL×J.oý6›ÍÔ©á\”×ÝÐ* âÒœ01o£™È†áÞQ]³]³çQ€Ë0Å“ÇCßá×.ùKy¥—:˜ïì0gøÇð¯GÑ›˜[“íÿHþÜ`Ýh…èlà…Þ¤Ô†öì/>bn‰L~°×ëÿfàJ%0[H`qø• Àðoø_x†ÿ8þ0gC{v(Â,µ=¾W„†ÿóÜ? (ƒ‰ü`Ô×[¢kr<K²š”Y„#ƒï+åúgøŸ¹LûV¯ü›%ÏþÇi(wÈ—_PZ±„‡gÚ碪¦^0C£ý‡ì¢þ™³oÛaf?·%Êë®oñºÃÂÈQp#)WP½f“Bhˆ¬uóà¹ÿÙ™ÈÅžÝQ^{¦©ÕÀBp`|[‹b½‘Ì× 8Ca¶ñÔäi… ÿþçú={ê.E0£!@0Ô¼,ÊÍëí]B U¦&OþÝ?ÅD.ÞGu½®Üð/ð%£Ä^|$LÆB³(!€fHƒÜÁŸ‡Þ·w)Ä,­~x§"Ì_ÿA%ÕõX›`Ž 8‡FÆ[€„¯a“E÷̹ ÿµÙ 1K1Úìð?‹$ö×Lĺըë1‹ì\x3ÀÜŒö‰þ™Ãÿìżõ?‡ÿÙ™-X4^3¯SÃ9E˜cмy»BÌ!ÐLI0ž°^ÍCÛã{abÞúïÙÿ`‘Ùç¢ÓýÄmB€9jܸE0×GS T°ÜÁŸ‡c½ûb–6da–bÿõ‹Ì.€xC€¡=;„óéZ›<ï??ÌãwϯÿÑòë¿À—E•ïx6zB£S–FûÈþWBñäqÅ*bMb~œú?7‡Ÿóƒ`åž¿/Œvu(„ ,ú¿&Êfjò´á¿Dÿƒÿfo"w {vG{ý~ý÷¬‹PCÊhÀÅT˜È‡#û_ ½oïRŒyª^³Éð?G1oýÝD~På'…Š;è~â6”·ÚÿŠ×‹¢ÿÝ7¡DÃÿ ìPˆ9è|°Eϱá^? *DìoˆÝÑ—žT„9²  4Fû €eÇQéþgïÔpÎsÿ ¨Þw"™ïxÖ£B€Š4èÀB þÖ÷¼rû¨ûõ¨Ÿû½×Á®k@òLJÜ<  !ª¤f}ØõgŠì¦5O °x%ÒhW‡/A Ôf7h¨À<ÿÎõîSˆ–ißêÀ¿y8»í߯ÿ H$©¹çïS„†,ü€øÛfîVÝó¨"ÌcøÏwû¿æ?÷¡Ä9{è Hü`oô5j†Ðù`‹?ÍW*† @åü=âþ“DS…1þé“§åÇÓ…ç@Øþ¨5 ‰'\)ŸÕ7Ý­`ðwgVügF0'üûãÖ< H°–ë¿jj3 †þÔÊÞÿBX¾¾]!æi´«Ã«’Íf„2ò”éæGi UÊ«ÿÝ7 (`ø7üsIçûÂø‘_*„áÿ ú¾¡Òª¾¥-ú䞿ϛ„©TŽì%Œçæajò´Ã7+ˆá¿4><ôšgÿ?U³b•"œ¿æÇa‘x`‘IúΰÅÇÀº<P9–­È†¦ìz…€Êüy˜,Ž)„{uê íÙBaàÅGÃL`.(#;{ñËR‹žAU³ã#¹pdÿ+¡xò¸bÀeÙÿŠáßýÄð¯†e€?ú²’ø…°úá¡6»Á—a}éI[ +ñ;ïÍ𹡟ÊS½fS¸á QS…±Ðõ˜Ç?ÍfJa”»ù{ê®0‘;  `Õ=úõ¦BCþüþãÐó—ßWÃ?Äüe'ù;ະÓ/¨P£ý‡Âhÿ!a†~ʪíñ½aiCV!Jµ¶wuþ¡ÙPFRÀÏH??¨²p„ÉàœÒ`dðýsáî½î·ú~ôþ,H8P ¥0‘ýᄅôüzþŠä :«îy4,mÈÚš˜g·O/©®ÙußR*NîàϽ¾Ïðϧllø§²Ù`Q¨(AMÉbó+E2Ù@%ðl²­ßÖªjê¢Ä?·%{v+„>_¯/ÀÂ`a˜--§'›ø™ 'ýþõùz|‹ƒ»RÀÎJ­xòxè;üšB¤„_ýÝCõ÷z|€Àa À9ãùp¬wŸB¤ˆ_ý–Ãuõöz{ E‰ùÕB€@€…akÊÿöÞj³bLä„üWúz}½ …Å¢Ô (@€ÁÃ%qŽŽž^?/À‚aÁHŒ–ë¿jj3 aØ'e²÷¿–¯oWÿ~^?À¢aÑ @0Vƒ¹®p|$§‘ýu«o !Í-°‰Üpä©»B¯`á°x„¤‹Ç’Á¯ü‘ÿÚî¿(¦ c¡pì=ÿ^/ÀbñXL™ö­aÕ=*Ä"íê¹çïS}*„­þ‹K®׿ °€XD„ ø”GJ«xòxè;üšB0-oÈY\¶ýëÝgj(w(FŽ*„ ÉÜõ|#LÇbPÁkù×ïUK®RˆYð ?†÷=={²ùáN€ÅÄb²€š7o·(DœÎ…î'nSæíÚÖ›C]¦9õ×Y˜È‡þwßô3oÂoÿ~]¿ŽÀ¢bQ 1"5–·¬ µ×\ª¿´,‘ƒþ±ÜÁ0YóARRmï K² ±HÝzõÙÈö†±þn…`Q,Û!ËËûŽÃþŸƒoõézt L…³%²¼üRPZ^ïWv¶éÏõæét¥`áuæt–6dÃúm¶¾”ÂÏôþË`"w@ô †þ 0U5õaý¶îiߪs°úáv´•‘Wý1S_i¨5«, ìÅ íÙ¡e2îÍ›·+À eÚ·úÕ¿Ìüú¯÷œ7:þ³"$­G !üeH†åÍ¿¢©ËGÿ¨ÞÀ»~®¨ÿJX¶êŠQ&Õ×4‡/}¹5T5\&º„x—Ò¼y{øÊÿ£B”yø÷ë¿á6ôàÉã@ OjØ%q‘!tͦ°æàíeæ± €‹sÐ_ù~nK(öìV=¸þ;å<àŒ”+öì=ùýpj8§eäYV€/²åßðŸUÕuŠ`.I ;Hi!š«¶Ç÷†¥ Y…(“–%ÇÃûƒÃŸûϼ2ˆQöþÂòõí QfS…±ÐõX›Bè½õÝ,D£´òH@e8G†ˆ‰Q†=·~›òñêÈ{v‡®ÇÚœò[fçïÄÈÞÿ‚‚†ÍhW‡á_Oiøà˜yê®pô¥'¢Ì!ÀÒ†lXR÷eÅR­yóvÃ…85œ ¹çïSÃ?‘ò€Å)µ„$—W½fS¸á QAF»:4f@j¬ßÖ푳 þ=vvy5+V…ÆìZ…Ðc ¨< ­Bm¦I!,Psæ`ÀÊ B‚ ±Ì•ÇëhgÎlzk) UªÙ–YÙa€ H‚LûÖ°´!7nQŒ ãÀ?}µ¾€Å*C¹C¡0rT!„‰æ— ’ÿU÷<ª•Ü íÙ^|D!ôÓ†è2ý³kCÍŠU 1ƒsª0¦êÆgúÃê‡w*`øgVF»: ÿ†ÿy›È*‚’py^XÙj³ÂúmÝ^T„æÍÛ ÿ•>´åxŒÌð_ýúôð€,*vJÌŒ_t’Á–N`±Ý‰TsíuN÷OÀðä©»Bï¬FfÞ `+•ê5›Âš?ü/œ,ÖðoðOÈ J¥ïàaª8®)—±=‡R)öìvbpBÜøLhÞ¼]!€Ó¼y{¨Ín0üW¸©ÂX8üÜÿá¿tß)ÃêØ`A‹’]³0I[>Rª^³)ÜðÀ…H;ÂôÊúe5,j%ÑöøÞ°´!« aû'0_Ùû_Ë×·+„á_Ÿ¬OF€ÅÍâ£Õï µÙ ‘   þéfç×ìÔ·´…LS«Bè,rÒiª0Æü2„ì ¬ï)rj8ºŸ¸M! ÿ%3”; #GB€@Àgü:”l¶‰geÚ·†¯ÜñÇ÷3üë‰Ñ °ØYì¸8&ÛTaÌ[ â¡?³án¿ö'Øáç¶„bÏn…Ðë‡`ѳè ˜¿"AúU¯ÙBaÅÍß ·(ˆá_Œ>X€Å‹ßìyfTT®ìý/„ºÕ·Úâoø×ÿ¢ÿE`Ä"Xº „ HIpúxÞ‰Ò`]¦ÂÖæ÷~ü¨á_ï[r}ßSÅq…`!0;Õk6…•÷üÍfÊx $‹CZÓ9üÛ¥ïÕó"`N¼þÄ‚¸XaÀÚÿá/Â'Kë#%F»:¼B*˜Ç±Òg"w œèëÂþõ»° Z+_Ûã{ÃÒ†¬B¤È©á\!ø% ÿ,°£/=òÏ*„^WŸKÉ]©þØa!t?qÛ¹‘tXÚ K²¡íñ½¡yóös§ŠåÓöø^ÿáÃ?̘H.C`2?™ö­¡nõ­žIM)ϦBy4oÞîu~)Ôù`‹"èmõ·°HZ$“oý¶n¯¡óäñªô™Èðæ}­¾K‹¥€ä˜*Œ…výY8žëôš*(±ìý/„ºÕ·Z? ÿègçeíš_ ;ÿãÿ©€EÓ¢)XÜFÖãéçÀ@˜O¥Ÿ×¬êcõ±°pV¸ü`oëïV!³4U ]µ)\‚ÓüãáY=¬P‹§fADÀãPÖ@ô®úWXH-¢Ñ†5×^§ŽÐDîÀ¹žÿ0äž¿OQHÝúB°ÆBð«¿žUߊ ªÅ”‚GøŒ÷_“¦áßàOúÓ«êW`aµ¨"à¢N çÂGݯŸ;HP @†}3q±õÌa¨zÔré;øF˜*Ž+,°B€Êuã3ýŠ€¦šŠÕöøÞ°´!«\–çýõ¦zTXd-°Ì_Ô`=Âð¾ToŠ‹­…VÓ BLöþBÝê[=¿Á_?šH^[‹®@€F¦‘ißz6dÃÕ+×;¼9F–^Cë†P›iRý(„ݘT¯Ù®»÷IÏÝRÞ4À…ÃfÃÝ‚Fæe´«Ã«LõŸúP¤ƒôÕâ[)H)~nK(öìVˆU¯ÙVÜü½©=œ¼Úý k‰á_Š„`!Ñ™*Œ…ž¿ü¾f>e<ÓÏBé|°EzOXŒ±/$¯á¢\<ã[ùª×l Í¿ýG¡¦yu‚QN†\w8òÔ]Š¡ßÔs"À¢Œy1dï!,_ß®”ÅОaàÅG¢‚4oÞ7nQÔéž×¡羫úLý&,ÌX”5ýÄèÔp.zBñ£ÁsÿBÇs%(±ÕïtB?ec»ÿÂñ¨©>B‹33–iß7n±Ý—Š7U …cï}î?³øóš7o!„P}MSXR÷åpÕ²LXrõrC?eSÜ÷ŸÂá¿þ…0üë/`±Æ"]I!Àª{Ua"wàsÿ~¢¯+ÚG 2í[CÝê[=³OEò«ÿÂóÃ’¾l‹5sæ-$ÝTaìÜ?Ož §çÃäø‡aüÈ/ûXA¦}ë¹¶[Ã?úH=%,Þl„P¢à`º¡øÑà¢ì2¨^³)¬¸ù;áš¶oôI¤‰Üæèõ’`:UÕuaåºÛÂÂ]‘V?¼3Ôf7(ÂOÃÉ£ç,<{XálL7Ð_½r½ƒùH…«?øExóý=…è!€EÜžT^ÀåØî¯gÔ;‹+• ‘ašåž¿/ íÙ¡|ÁÑ—ž4üëyôÜDÅ,ìôhxSgüõˆzE`·°§\õšMá†ì0ü£7Ô#€dTŠ=»5~†ô6†¢d{ }´<‡©ÂXø`ן…|Ç³Š¡Ô"P,ú•¥ïàaª8®‹¤íñ½ÞWR‡ŸÛŠ=»B(~ ?Ÿ¹ñ™~EH‰Ñ®Ž{þ>…ÐêA€€·úá¡6»A!hª0ºkS½ŸÞ¸¸03Þ<¶ûëùô|piÞ€EÌM•iœ}[ÀDî€bT¸SùÐù`‹á_Ÿ\†¸1$ŒP¥¼²÷¿2×ÿFødibT€¡=;ÂÀ‹(„OÜ Ü X˜ ¦y7”É©á\è~â6…ÐÛéí@€…‹£yóöpMÛ·†=z:à†á†‹ê5›Â]ßûWáêÕÃÁQË*€Á?ê[ÚŽ WßyIDATB¦©U!ôr@°Øòƒ½a¬¿[!*HóæíaÃÆß ýáÅ0øëß0ü#774Ë´o u«o ¿ÚòåpÃ7¾z>œ ùâ•áä'UŠ0CS…±ÐõX›BèÛôn ªªëÂÊu·+„Iê=øg/…ª–_ ï› ÿ_¡*†ÿž pSI§æÍÛÃwÿ`sšø$;ÂÐé«à§†sá½?Š=»Ãð¯On,¸¹$;¨¾¦)Üñ[ÿU8qEè+Ø pÖОaàÅGB¦?n2¸É¤CõšMaYöÆðû›ÿ(|¼ìÚ0^œ B8>yE8íÑ 2øÓ—éË@€› n8©—ißšoØ~û[·…©+«ÃûùÃÀ)‡Ñ®Ž{þ>…¨05+V…ÆìZ…Ћ!!@’õ|#LÇ¢Bƒ€Bøg¿ÿÝ0¾ìºÐ=¦&€á=XZxe3p0mÐôÕëÃ]w¶‡âUׄ÷òŸ„|1„Û¤ÃTa,Œù¥á_ïeø¸-ÛÐ’á_ÿ¸3L.mý£Å0RabòÊ0þqµÂ‰tô¥'C¾ãY…Ðsé¹@nHnHL'Ó¾5üÁïÞ²_¿%ôçO…ÃÇ>ÇN-Q âMä„#OÝ¥z-½ÀɉÙø/ÿû°ôÚ¯†Ñ‰Saxb*|0~UøhRTÞà?9þ¡mþ®¡uC¨Í4)„ Ü ¨Tßû×nØøOC¡¦5ŒŸ(†±‰!÷Ñ•!?éÑ ¹& ž¼BQÿÁ½psÃ-íaÀ†uׇoýÎï‡ì7~#üì`1¼7î¼àŒ©ÂX˜<1ºŸ¸M1ôGè‘€œiòÝGŸ _»í÷Âß Ô„¡Sv@¬Cÿð¾—=Û¯7Bo„Üèp³‹Aóæí¡áæï„ªšzŀȆÿ®ÇÚB?„~¸ñᦛìý/„šæ5aÉÕË…B§†sçþÙ6=z!àæ‡!„3»7nQHÁÐÿQ÷ë¶øëÐ7Aæ&?ØÆú»"™ö­á+wü±] ¡hÏŽïxV1ô<þA€"n†Ì>hܸ%,mÈ*Tøà<×é×~½úàÆˆ›"¥Ñöø^a”qÐ/ ôœû÷Üó÷)Šþ}p“dqLäÃpï… 8ôÔýzxñÅÐ×`ø¸YºQR^ë·u;'JäʱBïÏþ¯p<×yî?³­_?ƒžà¦YQìà¬ê5›Â ìP˜ÃÏm1à£1üƒÜ@…¤‡Çà [ú¹”ªêº°rÝí aøÌEÍŠU¡1»V!ÜD©0Í›·‡Æ[‚Ô›*Œ…á}/ø¹,?Zè[@B€ÄÊ …‘£ ÁŒ¬~xg¨ÍnPíKÃï„\÷[adß˶õcð7üƒÜ`ÝXa¦œ%@RŒvux%z= À7XJ!Ó¾54nÜâ*ÊTa,Œù¥áŸY³;QoX$ ­Bm¦I!ÜlI0 °Ø&rBþÀ+!ßñ¬b0/~ŒÐ€Ü€ÝtaŽª×l Í¿ýG¡¦yMXrõòPUS¯(ÌÙ©á\ڳàÞCp#Æ ˜¤ÈÞÿBX¾¾]!˜Ïñ£ßÐ{€Ü”Y@ÞÀbË´o u«o u«oµSÀÀoàgÁÕ·´…LS«BüABT‚æÍÛÃ5mßvÀ`JMÆÂð¾—ÃÀ‹(z Ã?7j7jø¢ê5›Â²ìv $Ä©á\xïdžbÏnÅ@?žà¦í† s“iß2îW-Ë8h°ÌξzoüÈ/Ô‡>½ÀÍÛ *K¦}kXÚ Õ×4…šæ57¸Äp_8ö^8Ñ×eË>úô 7óMäÃpï… õª×l +nþNjÏ$˜*Œ…B({/LŽèW|­ªº.¬\w»BþAn츩CùÝyp1§†sáx®ó¢ÿû³ÏÓÏä¿çBgÿ{=“Où¡@Ÿà&€Á}À 7{ôè@nüÌÉPîP(ŒUÜÿ þ€4pÏwÏ )Ѐû¼{=Íæ÷vÊÏc ÂÜËÝËK¸R ÀÍHs€ûú-H?;@¥¾ƒo„©â¸B¸oãž @C¿&¸Oã> @s ¸7cø ÑÐpà~Œ{1M‡æ€JTU]V®»]!"?ØÆú» @C€{nZ ÝA4(¸Ïº·Р QÀ½Õý€FÍ €{)4.hdÜ?qÏ ‘aÖ&òƒa¸÷€B¸_bð ©Ñè05+V…ÆìZ…pO @Ãà¾H¥ò*]—QU]V®»]!„î¸ïС)H‡†Ö ¡6Ó¤îs€âfû£æÀ}÷6@š#4Lîi¸—H“ú–¶ijU €¡÷.@š(4RîY¸_ÐP¡¹pÂý €& €{îI 4]Tˆ‰ü`î= €û@š04cî7¸ßД‘:}ßSÅq…Üg(¹ü`oëïV4*‘_i÷ÜOhÚм¸àþ@#‡FˆQCë†P›iRÜ/@Ðàî ¸/@£‡¦p/À}š?’ÁÛÀšÁB4ˆ@…ªª® +×Ý®XÛ  4&òƒa¸÷€B€5ƒ? 4‘h"k5Öl@h,Ñd—eK?Öe@ÐpBŠÕ·´…LS«B`€ Í(¤ECë†P›i:÷ïÿäÛ¿^}ýïk- „¤S~°7Œõw+©V³bUhÌ®U ÿ€€Æk'X?P!ü‚…æ.?èç{ÃÚolC£ ƒõéitAÃKŒÖ‡u‚XÌï`óòðÚΧý>:[€R¯sg-Ã?@@#M‚TU×…•ënW¬Y€@€ÆëTޡܡP9ª€ÐXC©üöo~#üõÿ±}Úïµ€ 9ëÏ…ŸÕ?ÿoÿEø»®~…"‘¬=€€&ÝÚÖ€AhØAã^i¶‡µ ´ó¿ÀºåÖÃÒ¥ÕáÖo~3üJïûá_ýÇ—Ã'O„;Þ~}Ö3I.wÀÞtײëëßÿ韶‡ÿÒñ†/† ƒ?€@‚LäÃpï…@z5´nµ™&… Z~ñ@tìÀà `ð(a `ðR:üŸû@@ ƒ? €ÁH©¾ƒo„©â¸B‰ „@bÔ·´…LS«BP6~ñ’4ü €T°#€Å2”; #GH›ü`oëïV@  ­lóÒa†~€” ÿ@@ Ààæ@Hµ¾ƒo„©â¸BÑ ÿA@T¼Æ)f›?  ýQB z ­Bm¦I! ÿ‰þ3dG€¡@™š«Bcv­BúR.Á®€òñ?€Ù ÿa@bøµ`a!€0Ààðá_ 0ø¥TU]V®»]! üJÌ΃?Àbÿa@Y8Å r!€0`^üÒP¾á_ 0ø„•ÀကRÌõ€ihÝj3M TÏôXv•¯ýé „ʹ€aÀð/ˆD}K[È4µ*`ð”$$„]Àlå{ÃX·B¤løDÈ.à,÷„°;âeK?@|Ãÿ|!@ÊÀ°@:‡3"Ã>!€ (“‰ü`î= †‹C0 ϯûTB @†~*xø° !ÀùÃË…á@ïþŸ„ªêº°rÝíŠF”¾tUU8yzÊÀ@â!óf±0èPŽÙýŠÿ%`ÞZ7„ÚL“B`è€Îí§fŪИ]«ò”1Pvõ-m!ÓÔª,ˆü`oëïV5ü/T  ¢8[€ùÊ …‘£ @¢‡|ʹññK>!|ŽG ’i"?Fû„©â¸býð¿Ð€€ÔóxÁâ íÖz"?†{(†ÿ „p±=vàYz(ïŒ.€«Y±*\]·b^AÂD~0œ.žüÂîùx aóù‹x1B(Ól~Å"_”Ê0—_Q†‹À"ÏäWª7¤ßeúÿ×.ÌãB ÿ‘B ÿ‘B ÿ‘B ÿ‘B ÿ‘B ÿ‘B ÿ‘B ÿ‘B ÿ‘B ÿ‘B ÿ‘B ÿ‘‚ÌÕBÌÔ‘BÌÓ‘BÌÒ‘‚ÌÐBÌÏ‘BÌΑ‚ÌÌBÌË‘‚ÌÉBÌÈ‘‚¢Ÿ¯ˆðƒÝL|EÄ´ Àà˜ƒ]¸ ó¯0÷*D2̺Š"ÀŒ«8‚Ì¶Š”xÂó¬‚ 0Ç*\Z Ì®Š!€YUQ## 0Ÿ*p„€y”P0{âCˆœp0_2/ÿ?›á£{5Û:IEND®B`‚kew/images/logo.png000066400000000000000000000120251507107350600145630ustar00rootroot00000000000000‰PNG  IHDR "²:ÕsRGBÙÉ,gAMA± üa cHRMz&€„ú€èu0ê`:˜pœºQ< pHYs.#.#x¥?vtIMEé *›JùŠkIDATxÚíÝ{˜Õ™Çñï r÷ ñh4>ÅŠŒ²FâíA¨˜ˆ‚Ê£1ºÞ×c²O5â5ºj¢qEŒqVQñrÄ€Šh”ˆ®"AÁK*‚.¸(7¹_ffÿ8gÚ¡¦«»¡»æ÷yžyº§»ç:§NÕÛ§êÔ))Y]%ƒ'Q\ öNbðJxþ-`Ï´‹bœ}U«[D¤|¶«püzà`xÊ¿ŸLq.â”qÆJ ""5”@š€)À¢”?3Ähž>Ng†VµˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆÔ¸¢¯DO¢¸Ð+ü:ß8»2 ‘Dñ@ðë<ãìZ5‘ÂêSüM?àýðs\†ê"eš¨¦!"Rþ’«)ƒuÒ¤f!"Rù"""J """J ""¢"""J ""¢"""J """J ""¢"""J ""’5Û© ÒK¢xW`Hõ¸xÔ8ÛÐ"N`Ю•q–g+T®½ñóœµv²ÍOIÆÙ¦qöŽ)"Î'À”eˆS.“ÙxÏ—R–çñÐS*5΃À¸2Ĺø}aš ôîî,Cœr¹¸ª˜å)ðú€Ë˧\Î*òó…–gh™â”cŸÑDñÀR—Ç8»6‰â£¶õ>#Í ¥]!€“³Oe!$Q|fØQ5ý³³”"ED6ß=)šaeX©ñ]üIÁÿ4ÎÎWÍTíúÚ8ø:þäýXãìª ”k7`$~°È3Àãìú ”«O(×nÀxüÂÆ ”ë à ð_øÑvMJ mOŒ?,×|rþ°$ŠgaãͨkËÂó3€nIßTáó'•ÞÕ÷†/2àGŒµ¨ñl×4¾^ |˜Vãåê‰?wÙ|n÷;ø¡øsò}^‡°²ûm¶8‰/ìêì­Ú©ÚÞÇq9/Õᇠw®ñ¢íŽ†Ý¬=0$$–Z¶pHÎïÛ'd )öËIÍëo@¡+dÛB¾<‚c5~h¤TŸµl:<{Pë½Ål:Œ¹e»¬EË€5-^û¿ ´ÃElz=×gJ mL8ì1˜Žÿ½¸¾å­R5ëkþ°èà¦Z?Ühœ]\,;¦×;kù°\0¸-Æzü…´ã2ÐÿŠ¿8zEHÄ_”«ÒwJã¯ä=¬öÕJU¯¯gñ‡{úGg_ËHÑÆG„r˜oº‘\W Àà  Ë—3ãìZà§À¡øCtg ^«“èÙß)}8ÕDͬ¯áÛzÖzÃó2¸®C1kåZŸn‹Ô‘T”@DDD DDD”@DD¤Ê¥9‰ÞˆŸºžMÇ ×² ¡\ Tî>""m:¼‰Ÿû Ï¿_‹žÏרiˆˆˆˆˆˆˆˆˆˆˆˆˆˆ´=ini»~Ò·zàã웪Æê•Dñ)ÀiÀJà§ÆÙŪ•ª^_£€ýñÓÏÜœ‘/ÕwÝ€—€1˜L±ùž £ñ7^zÄ8;1#m°pKØÇßiœ^è³i®éœ öÔ&_õ¢°¾N¡öï-ÑÖ×1dç:­:ü‰NÇOêY—‘ruNÅß$«o†ÚàN¡LÃØÂýƒt!¡ˆˆ¤¢"""J ""¢"""UN7”ªAI·v2ÎNRˆˆˆl)qôÄf9{Ðÿ”@DD D ö6ŽFC€ÝÙxè±I5$"J ’›4ê€=ð×nœ¿–£Kx{0 xx@µ%"J BÅ€ÀY¡·Ñ=ô6À“À8`¦qv¥jLD”@ÚvÒ¨zâ¯h=+ô6:âO­ÆOý0˜ ,6ÎêfW"¢¢äwnÆŸßØ9ô6ÖãçA<$Y˜©Äzj´/âOÚz‰(d_`~’9€ç€_/k²0á\™ ®¥õ×-MI¢ø‡ê­‰(dY_¾}îüa«€ç’(^¢ ;_-"ÌV•‰(dÝüyø)×÷Ä_ç1˜<•DñƒÀlãìŠ6\O.¥õ³¸þ qQɲpˆj0+‰â€“€ïã¯2ß øWàBà­$Š'¹ÆÙum¬žÞÞU‹Q‘ü;É¥À¸$ŠÂ_e><ôJöú‡Ÿ+iá3/ užDD”@$_¯äfà„Ð+9؈ÃÏ<àF`ŒjMD¶ÍÆ[å½ãì#øs"Çào%<hz‡¨–DD=ÙR¯d60;‰âÛÀ9ø“ð""J Òº^ ðxÅOâ‡ûŠˆ(HQ‰¤Q=Ù–tDDD”@DDdëÑ!,ɼ$Š»à¯ðo—âÏ›€iÆÙIoâÔ¥ˆÓL5Î.Ô%‘ÚÑ ø~šü4 dþ~,»cS&¢ À±€ˆ(ˆÔÀ=7%|nÏa^x¾ ¸;eiÀÏo&¢"R+ÂÐçËÊçsü¤Ž"‚N¢‹ˆÈVì|\ƒ?‘øžª°ê½Ö×`¹ª£êÝ<$øÃ^YÐÜŽ¿ðõ-²3Õþjàü¡Ñ—2Ô£Â>þmm’"""""""""""ÒÖ}EmÅ€^á×ùÆÙ•Y¨ˆ$Šwz„_çg×f¤\Ý€îøëæg7¨ÙWõúÚ è ¬ Û—î8)[» vÀß àSãlÁÁ7i†ñöÞ?Çe¨ÞâP¦9À*×ÈP®7ðWRKuÖ×X ƒªC¶ýsöññæ>Xêu Yüv”Õo|ú&[[ëIëKª~¡ ED$%Q%Q%%Q%Q%%Q%Q%%Q%Q%%Q‘ê°]%ƒ'Qܸ’2ÄŸ€ ‰n4plÊ8³?)c¹Ú‡¤øó:`Œqöæ$Š;¯¥ŒókãìoËX®®À`”ËósãìÃIïLº¥\”˳O&QÜxØ>EŒ&àbãìsI÷q:¦ˆÓœkœ}9‰âÞÀs)·›õÀYÆÙÿ)ãú:x"”µX+aÆÙ9I<ÊZ¬eÀ©ÆÙ$‰âÀý)ã| 5Î~’Dñ`à®”q„åù4‰âaÀ¯RÖÏ\à4ãì¢$ŠÏ®Nç`¸qvYÅ#ËS®nœaœ]™DñO€‹RÆ™ÚᚪO Án@Ÿþ¶.ü|¥„8»–¹Lua'Û'åßî’ó¼g qvª@´°WÊåÙ!%$Ne^_Ãò¤Ù±-:„çBœ4;ìÅ@ûð¼K q:çÄéZBœvá‡ðÅcŸ”qrâìXB=¯Ê9Ò³SXž4q–õI×;—°/\¶±êïg’(¾øQÊkŒ³@cøÐ1eœÕe.Úz Îiði¾ý¬Ž+a=¬(s¹V_ÏÙpŠõEx\ ô%ý!Òeáqp@ ~iÎF³O q–ä|+í•2NSNœr™•²·Ø¼<‹ÃóW)ã4æ”ëùã4/ÏS%Äiȉó0© qîƧŒ³!§=ßü¾„}Î ãlSÅ7w–gM¹`ÑBèîοžlœ}Š H¢øLàÁÐpúgge¤\W׆üŸŒ³ÿ‹Tóúz€?8Ø8»Vµ"[¹ öf‡ü0Â8;ns‡,DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDJTÔ\XI÷š§£þwãì-9ïu>ÅO øGãì9¥.\Å#€ÆÙ[¼7 èìbœ]š"vGüTñçá'Ú[…Ÿ,ïEàNãì?RÄÜ?}s!wg/+¡>:gûá'F{/Ô÷˜J7–$Šçà'7,ä{ÆÙ'RÄ­F⧨Þ?)ãsøéáç¦\Ö ÀМ—Öã'Uœ Ü`œý{…ëê7À[óYãl±Ûáóø[hœÓâ½ÉÀ‰øyÏÞ)2n_àí-|lãìŠ ×]%¶ÍŠÔÙÖ¨»$Š›€¹ÆÙ>e®ç’ã¦v0¸%絡µ’5“(îü8,$ÄÑøI.ÅϽ_ŠÐ‹Qáyoà!Ö„ðÚŒ–ygüÌ¢G¯÷àg'þ6p0f+VáMÀº<¯ÏIï6üŒÍÃÏXÚ8˜Dqãìü–õwÀ|üÔÞG„uò½$Š0ξWÁ:šÌÆÙ€ÁO‘^(ã#U¾‰|š@o]n›™¯»m!m™ŸDñ¾9ßä†áoÚsB ”ûüÐ@omy£©$Š éî?qv1pMˆóͰ³zÇ8{M–ùÖ<®1ÎŽj±ÌC¶rýÝX®o¡I¾©¿‹ŸyuxýüM…®&ýÍsî5ξ‘óÿn~⎨Tg'‡$Òü äÝ2µ‡Jz.cE¶Í6Rw[]ÚÙxg†¤AÅ»ÇÔÀ7«fÂã=y6ü¤Ú¦rwÂûAøöz]že~º†ÛࡎnN9=‡åÀ°$ŠËyßšûÃãሶMÙ& „,†‡ç§†îúÔ)wóŽªw,ï °® 7ØÊ’æùk-vëð÷$Ø^¤\6„Ç5ÚüµmJiJùf÷ðó$Š÷=‘‰9gµ›œŒw÷zÜ8ûQ/ïAáñí ¶Á}Âc¾]5ŸûèCúó+-§e î.I¢øó¯•#ÙîŸDñ5y^Ÿgœ½¿Æ·ÍJÕY5Ô]í$ãì_“(~?ZâhàúZ)´qvBXÉ¿nnO¢x`ÛŒ³ïWÙ"ï—TÉò,O¢¸åkS³Ç¥ˆÕ|õ•yÞ[K9î}A8GÔ 8Žîà—Ø~GV(îW ÔÏt6¬Õmsd…×É6«»Zë4÷B®>ñۣV nœ•Dñï€ïâ‡÷ .ÎN¢86Î>_E‹Û|ò¦*Yž|£°’ þ¿RÊ}~‹ßÎ ‡ÈjÝæ†¤–Ô 0ÎÉè¶Y©:«Šº«µ2¸ ˜hœmÈó­tkì8šJh¨ó»»Ã‰ÚóðÃïL­§Åá±{•,Ïe¼`9Ðè ,kñ^—œÏ¤u˜qö$Š»á‡ _ÖñyH5Á«•m³M«/q%;ãlqöâ -ßúͼ×|ñÕº25Ø áb¼g}’(îYEë©ùÜGß ¶ÁÂãžyÞëÙâ3¥¬ßÅÆÙ«€g€s“(>R›Í$“jÞ6•@ªØÂßDsuV·úYÍñºVQ=L =­S“(®ÏXl}ux‹Ã ðƒ–à‡Œ—Ë/ÂãuÚükN5n›J UìÕð8´ÅÎ¥~äÄ›i‚&Q<,‰â£ò¼~þ˜ë"àÕR ábÍ À×ðÀå.s]ŧÖp|8$Ç‹ÂT-Íþ‚ýQãì†2Öå,ü4)dž‹û¤ŠÔÚ¶ÙÖmWåË7 ?Á¹až©éÀWðÆ7àÏ¿¤q(~˜à;ÀËøcï}ð'í:ÃÒì´ÂqöKïÍãØûæ ë›®PNc$p ðË$Š/íñS™ìËÆéRjíð„K¢øNüÕèo$Qü4Ð ѧlœ¦œnŽíg°v›(4à>ãì'üßÙ63Pw»&Qü‡ï=kœ·-ânWå;— I;v8¸"tc_Fg_Oúþ€ŽNÁŸœ^†Iöãì””q»±é¾(ü€Ÿçirʺø,|c¾8 ¸X‹Ÿ{çg5¾Ãº<|«¼¸ ÒüQàg%΃U¨.ŸO¢ø-à¤$Š5ÎÎTÎø’BCQ í·’ ¤RÛf­×]WàûÞ[Jáù·¶U\‘êóÿ2xL ÙPIIEND®B`‚kew/include/000077500000000000000000000000001507107350600132735ustar00rootroot00000000000000kew/include/minimp4/000077500000000000000000000000001507107350600146505ustar00rootroot00000000000000kew/include/minimp4/minimp4.h000066400000000000000000003635511507107350600164130ustar00rootroot00000000000000#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/include/nestegg/000077500000000000000000000000001507107350600147275ustar00rootroot00000000000000kew/include/nestegg/LICENSE000066400000000000000000000013341507107350600157350ustar00rootroot00000000000000Copyright © 2010 Mozilla Foundation Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, 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. kew/include/nestegg/nestegg.c000066400000000000000000002403651507107350600165410ustar00rootroot00000000000000/* * Copyright © 2010 Mozilla Foundation * * This program is made available under an ISC-style license. See the * accompanying file LICENSE for details. */ #include #include #include #include #include "nestegg.h" /* EBML Elements */ #define ID_EBML 0x1a45dfa3 #define ID_EBML_VERSION 0x4286 #define ID_EBML_READ_VERSION 0x42f7 #define ID_EBML_MAX_ID_LENGTH 0x42f2 #define ID_EBML_MAX_SIZE_LENGTH 0x42f3 #define ID_DOCTYPE 0x4282 #define ID_DOCTYPE_VERSION 0x4287 #define ID_DOCTYPE_READ_VERSION 0x4285 /* Global Elements */ #define ID_VOID 0xec #define ID_CRC32 0xbf /* WebM Elements */ #define ID_SEGMENT 0x18538067 /* Seek Head Elements */ #define ID_SEEK_HEAD 0x114d9b74 #define ID_SEEK 0x4dbb #define ID_SEEK_ID 0x53ab #define ID_SEEK_POSITION 0x53ac /* Info Elements */ #define ID_INFO 0x1549a966 #define ID_TIMECODE_SCALE 0x2ad7b1 #define ID_DURATION 0x4489 /* Cluster Elements */ #define ID_CLUSTER 0x1f43b675 #define ID_TIMECODE 0xe7 #define ID_BLOCK_GROUP 0xa0 #define ID_SIMPLE_BLOCK 0xa3 /* BlockGroup Elements */ #define ID_BLOCK 0xa1 #define ID_BLOCK_ADDITIONS 0x75a1 #define ID_BLOCK_DURATION 0x9b #define ID_REFERENCE_BLOCK 0xfb #define ID_DISCARD_PADDING 0x75a2 /* BlockAdditions Elements */ #define ID_BLOCK_MORE 0xa6 /* BlockMore Elements */ #define ID_BLOCK_ADD_ID 0xee #define ID_BLOCK_ADDITIONAL 0xa5 /* Tracks Elements */ #define ID_TRACKS 0x1654ae6b #define ID_TRACK_ENTRY 0xae #define ID_TRACK_NUMBER 0xd7 #define ID_TRACK_UID 0x73c5 #define ID_TRACK_TYPE 0x83 #define ID_FLAG_ENABLED 0xb9 #define ID_FLAG_DEFAULT 0x88 #define ID_FLAG_LACING 0x9c #define ID_TRACK_TIMECODE_SCALE 0x23314f #define ID_LANGUAGE 0x22b59c #define ID_CODEC_ID 0x86 #define ID_CODEC_PRIVATE 0x63a2 #define ID_CODEC_DELAY 0x56aa #define ID_SEEK_PREROLL 0x56bb #define ID_DEFAULT_DURATION 0x23e383 /* Video Elements */ #define ID_VIDEO 0xe0 #define ID_STEREO_MODE 0x53b8 #define ID_ALPHA_MODE 0x53c0 #define ID_PIXEL_WIDTH 0xb0 #define ID_PIXEL_HEIGHT 0xba #define ID_PIXEL_CROP_BOTTOM 0x54aa #define ID_PIXEL_CROP_TOP 0x54bb #define ID_PIXEL_CROP_LEFT 0x54cc #define ID_PIXEL_CROP_RIGHT 0x54dd #define ID_DISPLAY_WIDTH 0x54b0 #define ID_DISPLAY_HEIGHT 0x54ba #define ID_COLOUR 0x55b0 /* Audio Elements */ #define ID_AUDIO 0xe1 #define ID_SAMPLING_FREQUENCY 0xb5 #define ID_CHANNELS 0x9f #define ID_BIT_DEPTH 0x6264 /* Cues Elements */ #define ID_CUES 0x1c53bb6b #define ID_CUE_POINT 0xbb #define ID_CUE_TIME 0xb3 #define ID_CUE_TRACK_POSITIONS 0xb7 #define ID_CUE_TRACK 0xf7 #define ID_CUE_CLUSTER_POSITION 0xf1 #define ID_CUE_BLOCK_NUMBER 0x5378 /* Encoding Elements */ #define ID_CONTENT_ENCODINGS 0x6d80 #define ID_CONTENT_ENCODING 0x6240 #define ID_CONTENT_ENCODING_TYPE 0x5033 /* Encryption Elements */ #define ID_CONTENT_ENCRYPTION 0x5035 #define ID_CONTENT_ENC_ALGO 0x47e1 #define ID_CONTENT_ENC_KEY_ID 0x47e2 #define ID_CONTENT_ENC_AES_SETTINGS 0x47e7 #define ID_AES_SETTINGS_CIPHER_MODE 0x47e8 /* Colour Elements */ #define ID_MATRIX_COEFFICIENTS 0x55b1 #define ID_RANGE 0x55b9 #define ID_TRANSFER_CHARACTERISTICS 0x55ba #define ID_PRIMARIES 0x55bb #define ID_MASTERING_METADATA 0x55d0 /* MasteringMetadata Elements */ #define ID_PRIMARY_R_CHROMATICITY_X 0x55d1 #define ID_PRIMARY_R_CHROMATICITY_Y 0x55d2 #define ID_PRIMARY_G_CHROMATICITY_X 0x55d3 #define ID_PRIMARY_G_CHROMATICITY_Y 0x55d4 #define ID_PRIMARY_B_CHROMATICITY_X 0x55d5 #define ID_PRIMARY_B_CHROMATICITY_Y 0x55d6 #define ID_WHITE_POINT_CHROMATICITY_X 0x55d7 #define ID_WHITE_POINT_CHROMATICITY_Y 0x55d8 #define ID_LUMINANCE_MAX 0x55d9 #define ID_LUMINANCE_MIN 0x55da /* EBML Types */ enum ebml_type_enum { TYPE_UNKNOWN, TYPE_MASTER, TYPE_UINT, TYPE_FLOAT, TYPE_STRING, TYPE_BINARY }; #define LIMIT_STRING (1 << 20) #define LIMIT_BINARY (1 << 24) #define LIMIT_BLOCK (1 << 30) #define LIMIT_FRAME (1 << 28) /* Field Flags */ #define DESC_FLAG_NONE 0 #define DESC_FLAG_MULTI (1 << 0) #define DESC_FLAG_SUSPEND (1 << 1) #define DESC_FLAG_OFFSET (1 << 2) /* Block Header Flags */ #define SIMPLE_BLOCK_FLAGS_KEYFRAME (1 << 7) #define BLOCK_FLAGS_LACING 6 /* Lacing Constants */ #define LACING_NONE 0 #define LACING_XIPH 1 #define LACING_FIXED 2 #define LACING_EBML 3 /* Track Types */ #define TRACK_TYPE_VIDEO 1 #define TRACK_TYPE_AUDIO 2 /* Track IDs */ #define TRACK_ID_VP8 "V_VP8" #define TRACK_ID_VP9 "V_VP9" #define TRACK_ID_AV1 "V_AV1" #define TRACK_ID_VORBIS "A_VORBIS" #define TRACK_ID_OPUS "A_OPUS" /* Track Encryption */ #define CONTENT_ENC_ALGO_AES 5 #define AES_SETTINGS_CIPHER_CTR 1 /* Packet Encryption */ #define SIGNAL_BYTE_SIZE 1 #define IV_SIZE 8 #define NUM_PACKETS_SIZE 1 #define PACKET_OFFSET_SIZE 4 /* Signal Byte */ #define PACKET_ENCRYPTED 1 #define ENCRYPTED_BIT_MASK (1 << 0) #define PACKET_PARTITIONED 2 #define PARTITIONED_BIT_MASK (1 << 1) enum vint_mask { MASK_NONE, MASK_FIRST_BIT }; struct ebml_binary { unsigned char * data; size_t length; }; struct ebml_list_node { struct ebml_list_node * next; uint64_t id; void * data; }; struct ebml_list { struct ebml_list_node * head; struct ebml_list_node * tail; }; struct ebml_type { union ebml_value { uint64_t u; double f; int64_t i; char * s; struct ebml_binary b; } v; enum ebml_type_enum type; int read; }; /* EBML Definitions */ struct ebml { struct ebml_type ebml_version; struct ebml_type ebml_read_version; struct ebml_type ebml_max_id_length; struct ebml_type ebml_max_size_length; struct ebml_type doctype; struct ebml_type doctype_version; struct ebml_type doctype_read_version; }; /* Matroksa Definitions */ struct seek { struct ebml_type id; struct ebml_type position; }; struct seek_head { struct ebml_list seek; }; struct info { struct ebml_type timecode_scale; struct ebml_type duration; }; struct mastering_metadata { struct ebml_type primary_r_chromacity_x; struct ebml_type primary_r_chromacity_y; struct ebml_type primary_g_chromacity_x; struct ebml_type primary_g_chromacity_y; struct ebml_type primary_b_chromacity_x; struct ebml_type primary_b_chromacity_y; struct ebml_type white_point_chromaticity_x; struct ebml_type white_point_chromaticity_y; struct ebml_type luminance_max; struct ebml_type luminance_min; }; struct colour { struct ebml_type matrix_coefficients; struct ebml_type range; struct ebml_type transfer_characteristics; struct ebml_type primaries; struct mastering_metadata mastering_metadata; }; struct video { struct ebml_type stereo_mode; struct ebml_type alpha_mode; struct ebml_type pixel_width; struct ebml_type pixel_height; struct ebml_type pixel_crop_bottom; struct ebml_type pixel_crop_top; struct ebml_type pixel_crop_left; struct ebml_type pixel_crop_right; struct ebml_type display_width; struct ebml_type display_height; struct colour colour; }; struct audio { struct ebml_type sampling_frequency; struct ebml_type channels; struct ebml_type bit_depth; }; struct content_enc_aes_settings { struct ebml_type aes_settings_cipher_mode; }; struct content_encryption { struct ebml_type content_enc_algo; struct ebml_type content_enc_key_id; struct ebml_list content_enc_aes_settings; }; struct content_encoding { struct ebml_type content_encoding_type; struct ebml_list content_encryption; }; struct content_encodings { struct ebml_list content_encoding; }; struct track_entry { struct ebml_type number; struct ebml_type uid; struct ebml_type type; struct ebml_type flag_enabled; struct ebml_type flag_default; struct ebml_type flag_lacing; struct ebml_type track_timecode_scale; struct ebml_type language; struct ebml_type codec_id; struct ebml_type codec_private; struct ebml_type codec_delay; struct ebml_type seek_preroll; struct ebml_type default_duration; struct video video; struct audio audio; struct content_encodings content_encodings; }; struct tracks { struct ebml_list track_entry; }; struct cue_track_positions { struct ebml_type track; struct ebml_type cluster_position; struct ebml_type block_number; }; struct cue_point { struct ebml_type time; struct ebml_list cue_track_positions; }; struct cues { struct ebml_list cue_point; }; struct segment { struct ebml_list seek_head; struct info info; struct tracks tracks; struct cues cues; }; /* Misc. */ struct pool_node { struct pool_node * next; void * data; }; struct pool_ctx { struct pool_node * head; }; struct list_node { struct list_node * previous; struct ebml_element_desc * node; unsigned char * data; }; struct saved_state { int64_t stream_offset; uint64_t last_id; uint64_t last_size; int last_valid; }; struct frame_encryption { unsigned char * iv; size_t length; uint8_t signal_byte; uint8_t num_partitions; uint32_t * partition_offsets; }; struct frame { unsigned char * data; size_t length; struct frame_encryption * frame_encryption; struct frame * next; }; struct block_additional { unsigned int id; unsigned char * data; size_t length; struct block_additional * next; }; /* Public (opaque) Structures */ struct nestegg { nestegg_io * io; nestegg_log log; struct pool_ctx * alloc_pool; uint64_t last_id; uint64_t last_size; int last_valid; struct list_node * ancestor; struct ebml ebml; struct segment segment; int64_t segment_offset; unsigned int track_count; /* Last read cluster. */ uint64_t cluster_timecode; int read_cluster_timecode; struct saved_state saved; }; struct nestegg_packet { uint64_t track; uint64_t timecode; uint64_t duration; int read_duration; struct frame * frame; struct block_additional * block_additional; int64_t discard_padding; int read_discard_padding; int64_t reference_block; int read_reference_block; uint8_t keyframe; }; /* Element Descriptor */ struct ebml_element_desc { char const * name; uint64_t id; enum ebml_type_enum type; size_t offset; unsigned int flags; struct ebml_element_desc * children; size_t size; size_t data_offset; }; #define E_FIELD(ID, TYPE, STRUCT, FIELD) \ { #ID, ID, TYPE, offsetof(STRUCT, FIELD), DESC_FLAG_NONE, NULL, 0, 0 } #define E_MASTER(ID, TYPE, STRUCT, FIELD) \ { #ID, ID, TYPE, offsetof(STRUCT, FIELD), DESC_FLAG_MULTI, ne_ ## FIELD ## _elements, \ sizeof(struct FIELD), 0 } #define E_SINGLE_MASTER_O(ID, TYPE, STRUCT, FIELD) \ { #ID, ID, TYPE, offsetof(STRUCT, FIELD), DESC_FLAG_OFFSET, ne_ ## FIELD ## _elements, 0, \ offsetof(STRUCT, FIELD ## _offset) } #define E_SINGLE_MASTER(ID, TYPE, STRUCT, FIELD) \ { #ID, ID, TYPE, offsetof(STRUCT, FIELD), DESC_FLAG_NONE, ne_ ## FIELD ## _elements, 0, 0 } #define E_SUSPEND(ID, TYPE) \ { #ID, ID, TYPE, 0, DESC_FLAG_SUSPEND, NULL, 0, 0 } #define E_LAST \ { NULL, 0, 0, 0, DESC_FLAG_NONE, NULL, 0, 0 } /* EBML Element Lists */ static struct ebml_element_desc ne_ebml_elements[] = { E_FIELD(ID_EBML_VERSION, TYPE_UINT, struct ebml, ebml_version), E_FIELD(ID_EBML_READ_VERSION, TYPE_UINT, struct ebml, ebml_read_version), E_FIELD(ID_EBML_MAX_ID_LENGTH, TYPE_UINT, struct ebml, ebml_max_id_length), E_FIELD(ID_EBML_MAX_SIZE_LENGTH, TYPE_UINT, struct ebml, ebml_max_size_length), E_FIELD(ID_DOCTYPE, TYPE_STRING, struct ebml, doctype), E_FIELD(ID_DOCTYPE_VERSION, TYPE_UINT, struct ebml, doctype_version), E_FIELD(ID_DOCTYPE_READ_VERSION, TYPE_UINT, struct ebml, doctype_read_version), E_LAST }; /* WebM Element Lists */ static struct ebml_element_desc ne_seek_elements[] = { E_FIELD(ID_SEEK_ID, TYPE_BINARY, struct seek, id), E_FIELD(ID_SEEK_POSITION, TYPE_UINT, struct seek, position), E_LAST }; static struct ebml_element_desc ne_seek_head_elements[] = { E_MASTER(ID_SEEK, TYPE_MASTER, struct seek_head, seek), E_LAST }; static struct ebml_element_desc ne_info_elements[] = { E_FIELD(ID_TIMECODE_SCALE, TYPE_UINT, struct info, timecode_scale), E_FIELD(ID_DURATION, TYPE_FLOAT, struct info, duration), E_LAST }; static struct ebml_element_desc ne_mastering_metadata_elements[] = { E_FIELD(ID_PRIMARY_R_CHROMATICITY_X, TYPE_FLOAT, struct mastering_metadata, primary_r_chromacity_x), E_FIELD(ID_PRIMARY_R_CHROMATICITY_Y, TYPE_FLOAT, struct mastering_metadata, primary_r_chromacity_y), E_FIELD(ID_PRIMARY_G_CHROMATICITY_X, TYPE_FLOAT, struct mastering_metadata, primary_g_chromacity_x), E_FIELD(ID_PRIMARY_G_CHROMATICITY_Y, TYPE_FLOAT, struct mastering_metadata, primary_g_chromacity_y), E_FIELD(ID_PRIMARY_B_CHROMATICITY_X, TYPE_FLOAT, struct mastering_metadata, primary_b_chromacity_x), E_FIELD(ID_PRIMARY_B_CHROMATICITY_Y, TYPE_FLOAT, struct mastering_metadata, primary_b_chromacity_y), E_FIELD(ID_WHITE_POINT_CHROMATICITY_X, TYPE_FLOAT, struct mastering_metadata, white_point_chromaticity_x), E_FIELD(ID_WHITE_POINT_CHROMATICITY_Y, TYPE_FLOAT, struct mastering_metadata, white_point_chromaticity_y), E_FIELD(ID_LUMINANCE_MAX, TYPE_FLOAT, struct mastering_metadata, luminance_max), E_FIELD(ID_LUMINANCE_MIN, TYPE_FLOAT, struct mastering_metadata, luminance_min), E_LAST }; static struct ebml_element_desc ne_colour_elements[] = { E_FIELD(ID_MATRIX_COEFFICIENTS, TYPE_UINT, struct colour, matrix_coefficients), E_FIELD(ID_RANGE, TYPE_UINT, struct colour, range), E_FIELD(ID_TRANSFER_CHARACTERISTICS, TYPE_UINT, struct colour, transfer_characteristics), E_FIELD(ID_PRIMARIES, TYPE_UINT, struct colour, primaries), E_SINGLE_MASTER(ID_MASTERING_METADATA, TYPE_MASTER, struct colour, mastering_metadata), E_LAST }; static struct ebml_element_desc ne_video_elements[] = { E_FIELD(ID_STEREO_MODE, TYPE_UINT, struct video, stereo_mode), E_FIELD(ID_ALPHA_MODE, TYPE_UINT, struct video, alpha_mode), E_FIELD(ID_PIXEL_WIDTH, TYPE_UINT, struct video, pixel_width), E_FIELD(ID_PIXEL_HEIGHT, TYPE_UINT, struct video, pixel_height), E_FIELD(ID_PIXEL_CROP_BOTTOM, TYPE_UINT, struct video, pixel_crop_bottom), E_FIELD(ID_PIXEL_CROP_TOP, TYPE_UINT, struct video, pixel_crop_top), E_FIELD(ID_PIXEL_CROP_LEFT, TYPE_UINT, struct video, pixel_crop_left), E_FIELD(ID_PIXEL_CROP_RIGHT, TYPE_UINT, struct video, pixel_crop_right), E_FIELD(ID_DISPLAY_WIDTH, TYPE_UINT, struct video, display_width), E_FIELD(ID_DISPLAY_HEIGHT, TYPE_UINT, struct video, display_height), E_SINGLE_MASTER(ID_COLOUR, TYPE_MASTER, struct video, colour), E_LAST }; static struct ebml_element_desc ne_audio_elements[] = { E_FIELD(ID_SAMPLING_FREQUENCY, TYPE_FLOAT, struct audio, sampling_frequency), E_FIELD(ID_CHANNELS, TYPE_UINT, struct audio, channels), E_FIELD(ID_BIT_DEPTH, TYPE_UINT, struct audio, bit_depth), E_LAST }; static struct ebml_element_desc ne_content_enc_aes_settings_elements[] = { E_FIELD(ID_AES_SETTINGS_CIPHER_MODE, TYPE_UINT, struct content_enc_aes_settings, aes_settings_cipher_mode), E_LAST }; static struct ebml_element_desc ne_content_encryption_elements[] = { E_FIELD(ID_CONTENT_ENC_ALGO, TYPE_UINT, struct content_encryption, content_enc_algo), E_FIELD(ID_CONTENT_ENC_KEY_ID, TYPE_BINARY, struct content_encryption, content_enc_key_id), E_MASTER(ID_CONTENT_ENC_AES_SETTINGS, TYPE_MASTER, struct content_encryption, content_enc_aes_settings), E_LAST }; static struct ebml_element_desc ne_content_encoding_elements[] = { E_FIELD(ID_CONTENT_ENCODING_TYPE, TYPE_UINT, struct content_encoding, content_encoding_type), E_MASTER(ID_CONTENT_ENCRYPTION, TYPE_MASTER, struct content_encoding, content_encryption), E_LAST }; static struct ebml_element_desc ne_content_encodings_elements[] = { E_MASTER(ID_CONTENT_ENCODING, TYPE_MASTER, struct content_encodings, content_encoding), E_LAST }; static struct ebml_element_desc ne_track_entry_elements[] = { E_FIELD(ID_TRACK_NUMBER, TYPE_UINT, struct track_entry, number), E_FIELD(ID_TRACK_UID, TYPE_UINT, struct track_entry, uid), E_FIELD(ID_TRACK_TYPE, TYPE_UINT, struct track_entry, type), E_FIELD(ID_FLAG_ENABLED, TYPE_UINT, struct track_entry, flag_enabled), E_FIELD(ID_FLAG_DEFAULT, TYPE_UINT, struct track_entry, flag_default), E_FIELD(ID_FLAG_LACING, TYPE_UINT, struct track_entry, flag_lacing), E_FIELD(ID_TRACK_TIMECODE_SCALE, TYPE_FLOAT, struct track_entry, track_timecode_scale), E_FIELD(ID_LANGUAGE, TYPE_STRING, struct track_entry, language), E_FIELD(ID_CODEC_ID, TYPE_STRING, struct track_entry, codec_id), E_FIELD(ID_CODEC_PRIVATE, TYPE_BINARY, struct track_entry, codec_private), E_FIELD(ID_CODEC_DELAY, TYPE_UINT, struct track_entry, codec_delay), E_FIELD(ID_SEEK_PREROLL, TYPE_UINT, struct track_entry, seek_preroll), E_FIELD(ID_DEFAULT_DURATION, TYPE_UINT, struct track_entry, default_duration), E_SINGLE_MASTER(ID_VIDEO, TYPE_MASTER, struct track_entry, video), E_SINGLE_MASTER(ID_AUDIO, TYPE_MASTER, struct track_entry, audio), E_SINGLE_MASTER(ID_CONTENT_ENCODINGS, TYPE_MASTER, struct track_entry, content_encodings), E_LAST }; static struct ebml_element_desc ne_tracks_elements[] = { E_MASTER(ID_TRACK_ENTRY, TYPE_MASTER, struct tracks, track_entry), E_LAST }; static struct ebml_element_desc ne_cue_track_positions_elements[] = { E_FIELD(ID_CUE_TRACK, TYPE_UINT, struct cue_track_positions, track), E_FIELD(ID_CUE_CLUSTER_POSITION, TYPE_UINT, struct cue_track_positions, cluster_position), E_FIELD(ID_CUE_BLOCK_NUMBER, TYPE_UINT, struct cue_track_positions, block_number), E_LAST }; static struct ebml_element_desc ne_cue_point_elements[] = { E_FIELD(ID_CUE_TIME, TYPE_UINT, struct cue_point, time), E_MASTER(ID_CUE_TRACK_POSITIONS, TYPE_MASTER, struct cue_point, cue_track_positions), E_LAST }; static struct ebml_element_desc ne_cues_elements[] = { E_MASTER(ID_CUE_POINT, TYPE_MASTER, struct cues, cue_point), E_LAST }; static struct ebml_element_desc ne_segment_elements[] = { E_MASTER(ID_SEEK_HEAD, TYPE_MASTER, struct segment, seek_head), E_SINGLE_MASTER(ID_INFO, TYPE_MASTER, struct segment, info), E_SUSPEND(ID_CLUSTER, TYPE_MASTER), E_SINGLE_MASTER(ID_TRACKS, TYPE_MASTER, struct segment, tracks), E_SINGLE_MASTER(ID_CUES, TYPE_MASTER, struct segment, cues), E_LAST }; static struct ebml_element_desc ne_top_level_elements[] = { E_SINGLE_MASTER(ID_EBML, TYPE_MASTER, nestegg, ebml), E_SINGLE_MASTER_O(ID_SEGMENT, TYPE_MASTER, nestegg, segment), E_LAST }; #undef E_FIELD #undef E_MASTER #undef E_SINGLE_MASTER_O #undef E_SINGLE_MASTER #undef E_SUSPEND #undef E_LAST static struct pool_ctx * ne_pool_init(void) { return calloc(1, sizeof(struct pool_ctx)); } static void ne_pool_destroy(struct pool_ctx * pool) { struct pool_node * node = pool->head; while (node) { struct pool_node * old = node; node = node->next; free(old->data); free(old); } free(pool); } static void * ne_pool_alloc(size_t size, struct pool_ctx * pool) { struct pool_node * node; node = calloc(1, sizeof(*node)); if (!node) return NULL; node->data = calloc(1, size); if (!node->data) { free(node); return NULL; } node->next = pool->head; pool->head = node; return node->data; } static void * ne_alloc(size_t size) { return calloc(1, size); } static int ne_io_read(nestegg_io * io, void * buffer, size_t length) { return io->read(buffer, length, io->userdata); } static int ne_io_seek(nestegg_io * io, int64_t offset, int whence) { return io->seek(offset, whence, io->userdata); } static int ne_io_read_skip(nestegg_io * io, size_t length) { size_t get; unsigned char buf[8192]; int r = 1; while (length > 0) { get = length < sizeof(buf) ? length : sizeof(buf); r = ne_io_read(io, buf, get); if (r != 1) break; length -= get; } return r; } static int64_t ne_io_tell(nestegg_io * io) { return io->tell(io->userdata); } static int ne_bare_read_vint(nestegg_io * io, uint64_t * value, uint64_t * length, enum vint_mask maskflag) { int r; unsigned char b; size_t maxlen = 8; unsigned int count = 1, mask = 1 << 7; r = ne_io_read(io, &b, 1); if (r != 1) return r; while (count < maxlen) { if ((b & mask) != 0) break; mask >>= 1; count += 1; } if (length) *length = count; *value = b; if (maskflag == MASK_FIRST_BIT) *value = b & ~mask; while (--count) { r = ne_io_read(io, &b, 1); if (r != 1) return r; *value <<= 8; *value |= b; } return 1; } static int ne_read_id(nestegg_io * io, uint64_t * value, uint64_t * length) { return ne_bare_read_vint(io, value, length, MASK_NONE); } static int ne_read_vint(nestegg_io * io, uint64_t * value, uint64_t * length) { return ne_bare_read_vint(io, value, length, MASK_FIRST_BIT); } static int ne_read_svint(nestegg_io * io, int64_t * value, uint64_t * length) { int r; uint64_t uvalue; uint64_t ulength; int64_t svint_subtr[] = { 0x3f, 0x1fff, 0xfffff, 0x7ffffff, 0x3ffffffffLL, 0x1ffffffffffLL, 0xffffffffffffLL, 0x7fffffffffffffLL }; r = ne_bare_read_vint(io, &uvalue, &ulength, MASK_FIRST_BIT); if (r != 1) return r; *value = uvalue - svint_subtr[ulength - 1]; if (length) *length = ulength; return r; } static int ne_read_uint(nestegg_io * io, uint64_t * val, uint64_t length) { unsigned char b; int r; if (length == 0 || length > 8) return -1; r = ne_io_read(io, &b, 1); if (r != 1) return r; *val = b; while (--length) { r = ne_io_read(io, &b, 1); if (r != 1) return r; *val <<= 8; *val |= b; } return 1; } static int ne_read_int(nestegg_io * io, int64_t * val, uint64_t length) { int r; uint64_t uval, base; r = ne_read_uint(io, &uval, length); if (r != 1) return r; if (length < sizeof(int64_t)) { base = 1; base <<= length * 8 - 1; if (uval >= base) { base = 1; base <<= length * 8; } else { base = 0; } *val = uval - base; } else { *val = (int64_t) uval; } return 1; } static int ne_read_float(nestegg_io * io, double * val, uint64_t length) { union { uint64_t u; struct { #if defined(__FLOAT_WORD_ORDER__) && __FLOAT_WORD_ORDER__ == __ORDER_BIG_ENDIAN__ uint32_t _pad; float f; #else float f; uint32_t _pad; #endif } f; double d; } value; int r; /* Length == 10 not implemented. */ if (length != 4 && length != 8) return -1; r = ne_read_uint(io, &value.u, length); if (r != 1) return r; if (length == 4) *val = value.f.f; else *val = value.d; return 1; } static int ne_read_string(nestegg * ctx, char ** val, uint64_t length) { char * str; int r; if (length > LIMIT_STRING) return -1; str = ne_pool_alloc(length + 1, ctx->alloc_pool); if (!str) return -1; if (length) { r = ne_io_read(ctx->io, (unsigned char *) str, length); if (r != 1) return r; } str[length] = '\0'; *val = str; return 1; } static int ne_read_binary(nestegg * ctx, struct ebml_binary * val, uint64_t length) { if (length == 0 || length > LIMIT_BINARY) return -1; val->data = ne_pool_alloc(length, ctx->alloc_pool); if (!val->data) return -1; val->length = length; return ne_io_read(ctx->io, val->data, length); } static int ne_get_uint(struct ebml_type type, uint64_t * value) { if (!type.read) return -1; assert(type.type == TYPE_UINT); *value = type.v.u; return 0; } static int ne_get_float(struct ebml_type type, double * value) { if (!type.read) return -1; assert(type.type == TYPE_FLOAT); *value = type.v.f; return 0; } static int ne_get_string(struct ebml_type type, char ** value) { if (!type.read) return -1; assert(type.type == TYPE_STRING); *value = type.v.s; return 0; } static int ne_get_binary(struct ebml_type type, struct ebml_binary * value) { if (!type.read) return -1; assert(type.type == TYPE_BINARY); *value = type.v.b; return 0; } static int ne_is_ancestor_element(uint64_t id, struct list_node * ancestor) { struct ebml_element_desc * element; for (; ancestor; ancestor = ancestor->previous) for (element = ancestor->node; element->id; ++element) if (element->id == id) return 1; return 0; } static struct ebml_element_desc * ne_find_element(uint64_t id, struct ebml_element_desc * elements) { struct ebml_element_desc * element; for (element = elements; element->id; ++element) if (element->id == id) return element; return NULL; } static int ne_ctx_push(nestegg * ctx, struct ebml_element_desc * ancestor, void * data) { struct list_node * item; item = ne_alloc(sizeof(*item)); if (!item) return -1; item->previous = ctx->ancestor; item->node = ancestor; item->data = data; ctx->ancestor = item; return 0; } static void ne_ctx_pop(nestegg * ctx) { struct list_node * item; item = ctx->ancestor; ctx->ancestor = item->previous; free(item); } static int ne_ctx_save(nestegg * ctx, struct saved_state * s) { s->stream_offset = ne_io_tell(ctx->io); if (s->stream_offset < 0) return -1; s->last_id = ctx->last_id; s->last_size = ctx->last_size; s->last_valid = ctx->last_valid; return 0; } static int ne_ctx_restore(nestegg * ctx, struct saved_state * s) { int r; if (s->stream_offset < 0) return -1; r = ne_io_seek(ctx->io, s->stream_offset, NESTEGG_SEEK_SET); if (r != 0) return -1; ctx->last_id = s->last_id; ctx->last_size = s->last_size; ctx->last_valid = s->last_valid; return 0; } static int ne_peek_element(nestegg * ctx, uint64_t * id, uint64_t * size) { int r; if (ctx->last_valid) { if (id) *id = ctx->last_id; if (size) *size = ctx->last_size; return 1; } r = ne_read_id(ctx->io, &ctx->last_id, NULL); if (r != 1) return r; r = ne_read_vint(ctx->io, &ctx->last_size, NULL); if (r != 1) return r; if (id) *id = ctx->last_id; if (size) *size = ctx->last_size; ctx->last_valid = 1; return 1; } static int ne_read_element(nestegg * ctx, uint64_t * id, uint64_t * size) { int r; r = ne_peek_element(ctx, id, size); if (r != 1) return r; ctx->last_valid = 0; return 1; } static int ne_read_master(nestegg * ctx, struct ebml_element_desc * desc) { struct ebml_list * list; struct ebml_list_node * node, * oldtail; assert(desc->type == TYPE_MASTER && desc->flags & DESC_FLAG_MULTI); ctx->log(ctx, NESTEGG_LOG_DEBUG, "multi master element %llx (%s)", desc->id, desc->name); list = (struct ebml_list *) (ctx->ancestor->data + desc->offset); node = ne_pool_alloc(sizeof(*node), ctx->alloc_pool); if (!node) return -1; node->id = desc->id; node->data = ne_pool_alloc(desc->size, ctx->alloc_pool); if (!node->data) return -1; oldtail = list->tail; if (oldtail) oldtail->next = node; list->tail = node; if (!list->head) list->head = node; ctx->log(ctx, NESTEGG_LOG_DEBUG, " -> using data %p", node->data); if (ne_ctx_push(ctx, desc->children, node->data) < 0) return -1; return 0; } static int ne_read_single_master(nestegg * ctx, struct ebml_element_desc * desc) { assert(desc->type == TYPE_MASTER && !(desc->flags & DESC_FLAG_MULTI)); ctx->log(ctx, NESTEGG_LOG_DEBUG, "single master element %llx (%s)", desc->id, desc->name); ctx->log(ctx, NESTEGG_LOG_DEBUG, " -> using data %p (%u)", ctx->ancestor->data + desc->offset, desc->offset); return ne_ctx_push(ctx, desc->children, ctx->ancestor->data + desc->offset); } static int ne_read_simple(nestegg * ctx, struct ebml_element_desc * desc, size_t length) { struct ebml_type * storage; int r = -1; storage = (struct ebml_type *) (ctx->ancestor->data + desc->offset); if (storage->read) { ctx->log(ctx, NESTEGG_LOG_DEBUG, "element %llx (%s) already read, skipping %u", desc->id, desc->name, length); return ne_io_read_skip(ctx->io, length); } storage->type = desc->type; ctx->log(ctx, NESTEGG_LOG_DEBUG, "element %llx (%s) -> %p (%u)", desc->id, desc->name, storage, desc->offset); switch (desc->type) { case TYPE_UINT: r = ne_read_uint(ctx->io, &storage->v.u, length); break; case TYPE_FLOAT: r = ne_read_float(ctx->io, &storage->v.f, length); break; case TYPE_STRING: r = ne_read_string(ctx, &storage->v.s, length); break; case TYPE_BINARY: r = ne_read_binary(ctx, &storage->v.b, length); break; case TYPE_MASTER: case TYPE_UNKNOWN: default: assert(0); break; } if (r == 1) storage->read = 1; return r; } static int ne_parse(nestegg * ctx, struct ebml_element_desc * top_level, int64_t max_offset) { int r; int64_t * data_offset; uint64_t id, size, peeked_id; struct ebml_element_desc * element; assert(ctx->ancestor); for (;;) { if (max_offset > 0 && ne_io_tell(ctx->io) >= max_offset) { /* Reached end of offset allowed for parsing - return gracefully */ r = 1; break; } r = ne_peek_element(ctx, &id, &size); if (r != 1) break; peeked_id = id; element = ne_find_element(id, ctx->ancestor->node); if (element) { if (element->flags & DESC_FLAG_SUSPEND) { assert(element->id == ID_CLUSTER && element->type == TYPE_MASTER); ctx->log(ctx, NESTEGG_LOG_DEBUG, "suspend parse at %llx", id); r = 1; break; } r = ne_read_element(ctx, &id, &size); if (r != 1) break; assert(id == peeked_id); if (element->flags & DESC_FLAG_OFFSET) { data_offset = (int64_t *) (ctx->ancestor->data + element->data_offset); *data_offset = ne_io_tell(ctx->io); if (*data_offset < 0) { r = -1; break; } } if (element->type == TYPE_MASTER) { if (element->flags & DESC_FLAG_MULTI) { if (ne_read_master(ctx, element) < 0) break; } else { if (ne_read_single_master(ctx, element) < 0) break; } continue; } else { r = ne_read_simple(ctx, element, size); if (r < 0) break; } } else if (ne_is_ancestor_element(id, ctx->ancestor->previous)) { ctx->log(ctx, NESTEGG_LOG_DEBUG, "parent element %llx", id); if (top_level && ctx->ancestor->node == top_level) { ctx->log(ctx, NESTEGG_LOG_DEBUG, "*** parse about to back up past top_level"); r = 1; break; } ne_ctx_pop(ctx); } else { r = ne_read_element(ctx, &id, &size); if (r != 1) break; if (id != ID_VOID && id != ID_CRC32) ctx->log(ctx, NESTEGG_LOG_DEBUG, "unknown element %llx", id); r = ne_io_read_skip(ctx->io, size); if (r != 1) break; } } if (r != 1) while (ctx->ancestor) ne_ctx_pop(ctx); return r; } static int ne_read_block_encryption(nestegg * ctx, struct track_entry const * entry, uint64_t * encoding_type, uint64_t * encryption_algo, uint64_t * encryption_mode) { struct content_encoding * encoding; struct content_encryption * encryption; struct content_enc_aes_settings * aes_settings; *encoding_type = 0; if (entry->content_encodings.content_encoding.head) { encoding = entry->content_encodings.content_encoding.head->data; if (ne_get_uint(encoding->content_encoding_type, encoding_type) != 0) return -1; if (*encoding_type == NESTEGG_ENCODING_ENCRYPTION) { /* Metadata states content is encrypted */ if (!encoding->content_encryption.head) return -1; encryption = encoding->content_encryption.head->data; if (ne_get_uint(encryption->content_enc_algo, encryption_algo) != 0) { ctx->log(ctx, NESTEGG_LOG_ERROR, "No ContentEncAlgo element found"); return -1; } if (*encryption_algo != CONTENT_ENC_ALGO_AES) { ctx->log(ctx, NESTEGG_LOG_ERROR, "Disallowed ContentEncAlgo used"); return -1; } if (!encryption->content_enc_aes_settings.head) { ctx->log(ctx, NESTEGG_LOG_ERROR, "No ContentEncAESSettings element found"); return -1; } aes_settings = encryption->content_enc_aes_settings.head->data; *encryption_mode = AES_SETTINGS_CIPHER_CTR; ne_get_uint(aes_settings->aes_settings_cipher_mode, encryption_mode); if (*encryption_mode != AES_SETTINGS_CIPHER_CTR) { ctx->log(ctx, NESTEGG_LOG_ERROR, "Disallowed AESSettingsCipherMode used"); return -1; } } } return 1; } static int ne_read_xiph_lace_value(nestegg_io * io, uint64_t * value, size_t * consumed) { int r; uint64_t lace; r = ne_read_uint(io, &lace, 1); if (r != 1) return r; *consumed += 1; *value = lace; while (lace == 255) { r = ne_read_uint(io, &lace, 1); if (r != 1) return r; *consumed += 1; *value += lace; } return 1; } static int ne_read_xiph_lacing(nestegg_io * io, size_t block, size_t * read, uint64_t n, uint64_t * sizes) { int r; size_t i = 0; uint64_t sum = 0; while (--n) { r = ne_read_xiph_lace_value(io, &sizes[i], read); if (r != 1) return r; sum += sizes[i]; i += 1; } if (*read + sum > block) return -1; /* Last frame is the remainder of the block. */ sizes[i] = block - *read - sum; return 1; } static int ne_read_ebml_lacing(nestegg_io * io, size_t block, size_t * read, uint64_t n, uint64_t * sizes) { int r; uint64_t lace, sum, length; int64_t slace; size_t i = 0; r = ne_read_vint(io, &lace, &length); if (r != 1) return r; *read += length; sizes[i] = lace; sum = sizes[i]; i += 1; n -= 1; while (--n) { r = ne_read_svint(io, &slace, &length); if (r != 1) return r; *read += length; sizes[i] = sizes[i - 1] + slace; sum += sizes[i]; i += 1; } if (*read + sum > block) return -1; /* Last frame is the remainder of the block. */ sizes[i] = block - *read - sum; return 1; } static uint64_t ne_get_timecode_scale(nestegg * ctx) { uint64_t scale; if (ne_get_uint(ctx->segment.info.timecode_scale, &scale) != 0) scale = 1000000; return scale; } static int ne_map_track_number_to_index(nestegg * ctx, unsigned int track_number, unsigned int * track_index) { struct ebml_list_node * node; struct track_entry * t_entry; uint64_t t_number = 0; if (!track_index) return -1; *track_index = 0; if (track_number == 0) return -1; node = ctx->segment.tracks.track_entry.head; while (node) { assert(node->id == ID_TRACK_ENTRY); t_entry = node->data; if (ne_get_uint(t_entry->number, &t_number) != 0) return -1; if (t_number == track_number) return 0; *track_index += 1; node = node->next; } return -1; } static struct track_entry * ne_find_track_entry(nestegg * ctx, unsigned int track) { struct ebml_list_node * node; unsigned int tracks = 0; node = ctx->segment.tracks.track_entry.head; while (node) { assert(node->id == ID_TRACK_ENTRY); if (track == tracks) return node->data; tracks += 1; node = node->next; } return NULL; } static struct frame * ne_alloc_frame(void) { struct frame * f = ne_alloc(sizeof(*f)); if (!f) return NULL; f->data = NULL; f->length = 0; f->frame_encryption = NULL; f->next = NULL; return f; } static struct frame_encryption * ne_alloc_frame_encryption(void) { struct frame_encryption * f = ne_alloc(sizeof(*f)); if (!f) return NULL; f->iv = NULL; f->length = 0; f->signal_byte = 0; f->num_partitions = 0; f->partition_offsets = NULL; return f; } static void ne_free_frame(struct frame * f) { if (f->frame_encryption) { free(f->frame_encryption->iv); free(f->frame_encryption->partition_offsets); } free(f->frame_encryption); free(f->data); free(f); } static int ne_read_block(nestegg * ctx, uint64_t block_id, uint64_t block_size, nestegg_packet ** data) { int r; int64_t timecode, abs_timecode; nestegg_packet * pkt; struct frame * f, * last; struct track_entry * entry; double track_scale; uint64_t track_number, length, frame_sizes[256], cluster_tc, flags, frames, tc_scale, total, encoding_type, encryption_algo, encryption_mode; unsigned int i, lacing, track; uint8_t signal_byte, keyframe = NESTEGG_PACKET_HAS_KEYFRAME_UNKNOWN, j = 0; size_t consumed = 0, data_size, encryption_size; *data = NULL; if (block_size > LIMIT_BLOCK) return -1; r = ne_read_vint(ctx->io, &track_number, &length); if (r != 1) return r; if (track_number == 0) return -1; consumed += length; r = ne_read_int(ctx->io, &timecode, 2); if (r != 1) return r; consumed += 2; r = ne_read_uint(ctx->io, &flags, 1); if (r != 1) return r; consumed += 1; frames = 0; /* Simple blocks have an explicit flag for if the contents a keyframes*/ if (block_id == ID_SIMPLE_BLOCK) keyframe = (flags & SIMPLE_BLOCK_FLAGS_KEYFRAME) == SIMPLE_BLOCK_FLAGS_KEYFRAME ? NESTEGG_PACKET_HAS_KEYFRAME_TRUE : NESTEGG_PACKET_HAS_KEYFRAME_FALSE; /* Flags are different between Block and SimpleBlock, but lacing is encoded the same way. */ lacing = (flags & BLOCK_FLAGS_LACING) >> 1; switch (lacing) { case LACING_NONE: frames = 1; break; case LACING_XIPH: case LACING_FIXED: case LACING_EBML: r = ne_read_uint(ctx->io, &frames, 1); if (r != 1) return r; consumed += 1; frames += 1; break; default: assert(0); return -1; } if (frames > 256) return -1; switch (lacing) { case LACING_NONE: frame_sizes[0] = block_size - consumed; break; case LACING_XIPH: if (frames == 1) return -1; r = ne_read_xiph_lacing(ctx->io, block_size, &consumed, frames, frame_sizes); if (r != 1) return r; break; case LACING_FIXED: if ((block_size - consumed) % frames) return -1; for (i = 0; i < frames; ++i) frame_sizes[i] = (block_size - consumed) / frames; break; case LACING_EBML: if (frames == 1) return -1; r = ne_read_ebml_lacing(ctx->io, block_size, &consumed, frames, frame_sizes); if (r != 1) return r; break; default: assert(0); return -1; } /* Sanity check unlaced frame sizes against total block size. */ total = consumed; for (i = 0; i < frames; ++i) total += frame_sizes[i]; if (total > block_size) return -1; if (ne_map_track_number_to_index(ctx, track_number, &track) != 0) return -1; entry = ne_find_track_entry(ctx, track); if (!entry) return -1; r = ne_read_block_encryption(ctx, entry, &encoding_type, &encryption_algo, &encryption_mode); if (r != 1) return r; /* Encryption does not support lacing */ if (lacing != LACING_NONE && encoding_type == NESTEGG_ENCODING_ENCRYPTION) { ctx->log(ctx, NESTEGG_LOG_ERROR, "Encrypted blocks may not also be laced"); return -1; } track_scale = 1.0; tc_scale = ne_get_timecode_scale(ctx); if (tc_scale == 0) return -1; if (!ctx->read_cluster_timecode) return -1; cluster_tc = ctx->cluster_timecode; abs_timecode = timecode + cluster_tc; if (abs_timecode < 0) { /* Ignore the spec and negative timestamps */ ctx->log(ctx, NESTEGG_LOG_WARNING, "ignoring negative timecode: %lld", abs_timecode); abs_timecode = 0; } pkt = ne_alloc(sizeof(*pkt)); if (!pkt) return -1; pkt->track = track; pkt->timecode = abs_timecode * tc_scale * track_scale; pkt->keyframe = keyframe; ctx->log(ctx, NESTEGG_LOG_DEBUG, "%sblock t %lld pts %f f %llx frames: %llu", block_id == ID_BLOCK ? "" : "simple", pkt->track, pkt->timecode / 1e9, flags, frames); last = NULL; for (i = 0; i < frames; ++i) { if (frame_sizes[i] > LIMIT_FRAME) { nestegg_free_packet(pkt); return -1; } f = ne_alloc_frame(); if (!f) { nestegg_free_packet(pkt); return -1; } /* Parse encryption */ if (encoding_type == NESTEGG_ENCODING_ENCRYPTION) { r = ne_io_read(ctx->io, &signal_byte, SIGNAL_BYTE_SIZE); if (r != 1) { ne_free_frame(f); nestegg_free_packet(pkt); return r; } f->frame_encryption = ne_alloc_frame_encryption(); if (!f->frame_encryption) { ne_free_frame(f); nestegg_free_packet(pkt); return -1; } f->frame_encryption->signal_byte = signal_byte; if ((signal_byte & ENCRYPTED_BIT_MASK) == PACKET_ENCRYPTED) { f->frame_encryption->iv = ne_alloc(IV_SIZE); if (!f->frame_encryption->iv) { ne_free_frame(f); nestegg_free_packet(pkt); return -1; } r = ne_io_read(ctx->io, f->frame_encryption->iv, IV_SIZE); if (r != 1) { ne_free_frame(f); nestegg_free_packet(pkt); return r; } f->frame_encryption->length = IV_SIZE; encryption_size = SIGNAL_BYTE_SIZE + IV_SIZE; if ((signal_byte & PARTITIONED_BIT_MASK) == PACKET_PARTITIONED) { r = ne_io_read(ctx->io, &f->frame_encryption->num_partitions, NUM_PACKETS_SIZE); if (r != 1) { ne_free_frame(f); nestegg_free_packet(pkt); return r; } encryption_size += NUM_PACKETS_SIZE + f->frame_encryption->num_partitions * PACKET_OFFSET_SIZE; f->frame_encryption->partition_offsets = ne_alloc(f->frame_encryption->num_partitions * PACKET_OFFSET_SIZE); for (j = 0; j < f->frame_encryption->num_partitions; ++j) { uint64_t value = 0; r = ne_read_uint(ctx->io, &value, PACKET_OFFSET_SIZE); if (r != 1) { break; } f->frame_encryption->partition_offsets[j] = (uint32_t) value; } /* If any of the partition offsets did not return 1, then fail. */ if (j != f->frame_encryption->num_partitions) { ne_free_frame(f); nestegg_free_packet(pkt); return r; } } } else { encryption_size = SIGNAL_BYTE_SIZE; } } else { encryption_size = 0; } if (encryption_size > frame_sizes[i]) { ne_free_frame(f); nestegg_free_packet(pkt); return -1; } data_size = frame_sizes[i] - encryption_size; /* Encryption parsed */ f->data = ne_alloc(data_size); if (!f->data) { ne_free_frame(f); nestegg_free_packet(pkt); return -1; } f->length = data_size; r = ne_io_read(ctx->io, f->data, data_size); if (r != 1) { ne_free_frame(f); nestegg_free_packet(pkt); return r; } if (!last) pkt->frame = f; else last->next = f; last = f; } *data = pkt; return 1; } static int ne_read_block_additions(nestegg * ctx, uint64_t block_size, struct block_additional ** pkt_block_additional) { int r; uint64_t id, size, data_size; int64_t block_additions_end, block_more_end; void * data; int has_data; struct block_additional * block_additional; uint64_t add_id; assert(*pkt_block_additional == NULL); block_additions_end = ne_io_tell(ctx->io) + block_size; while (ne_io_tell(ctx->io) < block_additions_end) { add_id = 1; data = NULL; has_data = 0; data_size = 0; r = ne_read_element(ctx, &id, &size); if (r != 1) return r; if (id != ID_BLOCK_MORE) { /* We don't know what this element is, so skip over it */ if (id != ID_VOID && id != ID_CRC32) ctx->log(ctx, NESTEGG_LOG_DEBUG, "unknown element %llx in BlockAdditions", id); r = ne_io_read_skip(ctx->io, size); if (r != 1) return r; continue; } block_more_end = ne_io_tell(ctx->io) + size; while (ne_io_tell(ctx->io) < block_more_end) { r = ne_read_element(ctx, &id, &size); if (r != 1) { free(data); return r; } if (id == ID_BLOCK_ADD_ID) { r = ne_read_uint(ctx->io, &add_id, size); if (r != 1) { free(data); return r; } if (add_id == 0) { ctx->log(ctx, NESTEGG_LOG_ERROR, "Disallowed BlockAddId 0 used"); free(data); return -1; } } else if (id == ID_BLOCK_ADDITIONAL) { if (has_data) { /* BlockAdditional is supposed to only occur once in a BlockMore. */ ctx->log(ctx, NESTEGG_LOG_ERROR, "Multiple BlockAdditional elements in a BlockMore"); free(data); return -1; } has_data = 1; data_size = size; if (data_size != 0 && data_size < LIMIT_FRAME) { data = ne_alloc(data_size); if (!data) return -1; r = ne_io_read(ctx->io, data, data_size); if (r != 1) { free(data); return r; } } } else { /* We don't know what this element is, so skip over it */ if (id != ID_VOID && id != ID_CRC32) ctx->log(ctx, NESTEGG_LOG_DEBUG, "unknown element %llx in BlockMore", id); r = ne_io_read_skip(ctx->io, size); if (r != 1) { free(data); return r; } } } if (has_data == 0) { ctx->log(ctx, NESTEGG_LOG_ERROR, "No BlockAdditional element in a BlockMore"); return -1; } block_additional = ne_alloc(sizeof(*block_additional)); block_additional->next = *pkt_block_additional; block_additional->id = add_id; block_additional->data = data; block_additional->length = data_size; *pkt_block_additional = block_additional; } return 1; } static uint64_t ne_buf_read_id(unsigned char const * p, size_t length) { uint64_t id = 0; while (length--) { id <<= 8; id |= *p++; } return id; } static struct seek * ne_find_seek_for_id(struct ebml_list_node * seek_head, uint64_t id) { struct ebml_list * head; struct ebml_list_node * seek; struct ebml_binary binary_id; struct seek * s; while (seek_head) { assert(seek_head->id == ID_SEEK_HEAD); head = seek_head->data; seek = head->head; while (seek) { assert(seek->id == ID_SEEK); s = seek->data; if (ne_get_binary(s->id, &binary_id) == 0 && ne_buf_read_id(binary_id.data, binary_id.length) == id) return s; seek = seek->next; } seek_head = seek_head->next; } return NULL; } static struct cue_track_positions * ne_find_cue_position_for_track(nestegg * ctx, struct ebml_list_node * node, unsigned int track) { struct cue_track_positions * pos = NULL; uint64_t track_number; unsigned int t; while (node) { assert(node->id == ID_CUE_TRACK_POSITIONS); pos = node->data; if (ne_get_uint(pos->track, &track_number) != 0) return NULL; if (ne_map_track_number_to_index(ctx, track_number, &t) != 0) return NULL; if (t == track) return pos; node = node->next; } return NULL; } static struct cue_point * ne_find_cue_point_for_tstamp(nestegg * ctx, struct ebml_list_node * cue_point, unsigned int track, uint64_t scale, uint64_t tstamp) { uint64_t time; struct cue_point * c, * prev = NULL; while (cue_point) { assert(cue_point->id == ID_CUE_POINT); c = cue_point->data; if (!prev) prev = c; if (ne_get_uint(c->time, &time) == 0 && time * scale > tstamp) break; if (ne_find_cue_position_for_track(ctx, c->cue_track_positions.head, track) != NULL) prev = c; cue_point = cue_point->next; } return prev; } static void ne_null_log_callback(nestegg * ctx, unsigned int severity, char const * fmt, ...) { if (ctx && severity && fmt) return; } static int ne_init_cue_points(nestegg * ctx, int64_t max_offset) { int r; struct ebml_list_node * node = ctx->segment.cues.cue_point.head; struct seek * found; uint64_t seek_pos, id; struct saved_state state; /* If there are no cues loaded, check for cues element in the seek head and load it. */ if (!node) { found = ne_find_seek_for_id(ctx->segment.seek_head.head, ID_CUES); if (!found) return -1; if (ne_get_uint(found->position, &seek_pos) != 0) return -1; /* Save old parser state. */ r = ne_ctx_save(ctx, &state); if (r != 0) return -1; /* Seek and set up parser state for segment-level element (Cues). */ r = ne_io_seek(ctx->io, ctx->segment_offset + seek_pos, NESTEGG_SEEK_SET); if (r != 0) return -1; ctx->last_valid = 0; r = ne_read_element(ctx, &id, NULL); if (r != 1) return -1; if (id != ID_CUES) return -1; assert(ctx->ancestor == NULL); if (ne_ctx_push(ctx, ne_top_level_elements, ctx) < 0) return -1; if (ne_ctx_push(ctx, ne_segment_elements, &ctx->segment) < 0) return -1; if (ne_ctx_push(ctx, ne_cues_elements, &ctx->segment.cues) < 0) return -1; /* parser will run until end of cues element. */ ctx->log(ctx, NESTEGG_LOG_DEBUG, "seek: parsing cue elements"); r = ne_parse(ctx, ne_cues_elements, max_offset); while (ctx->ancestor) ne_ctx_pop(ctx); /* Reset parser state to original state and seek back to old position. */ if (ne_ctx_restore(ctx, &state) != 0) return -1; if (r < 0) return -1; node = ctx->segment.cues.cue_point.head; if (!node) return -1; } return 0; } /* Three functions that implement the nestegg_io interface, operating on a io_buffer. */ struct io_buffer { unsigned char const * buffer; size_t length; int64_t offset; }; static int ne_buffer_read(void * buffer, size_t length, void * userdata) { struct io_buffer * iob = userdata; size_t available = iob->length - iob->offset; if (available == 0) return 0; if (available < length) return -1; memcpy(buffer, iob->buffer + iob->offset, length); iob->offset += length; return 1; } static int ne_buffer_seek(int64_t offset, int whence, void * userdata) { struct io_buffer * iob = userdata; int64_t o = iob->offset; switch(whence) { case NESTEGG_SEEK_SET: o = offset; break; case NESTEGG_SEEK_CUR: o += offset; break; case NESTEGG_SEEK_END: o = iob->length + offset; break; } if (o < 0 || o > (int64_t) iob->length) return -1; iob->offset = o; return 0; } static int64_t ne_buffer_tell(void * userdata) { struct io_buffer * iob = userdata; return iob->offset; } static int ne_context_new(nestegg ** context, nestegg_io io, nestegg_log callback) { nestegg * ctx; if (!(io.read && io.seek && io.tell)) return -1; ctx = ne_alloc(sizeof(*ctx)); if (!ctx) return -1; ctx->io = ne_alloc(sizeof(*ctx->io)); if (!ctx->io) { nestegg_destroy(ctx); return -1; } *ctx->io = io; ctx->log = callback; ctx->alloc_pool = ne_pool_init(); if (!ctx->alloc_pool) { nestegg_destroy(ctx); return -1; } if (!ctx->log) ctx->log = ne_null_log_callback; *context = ctx; return 0; } static int ne_match_webm(nestegg_io io, int64_t max_offset) { int r; uint64_t id; char * doctype; nestegg * ctx; if (ne_context_new(&ctx, io, NULL) != 0) return -1; r = ne_peek_element(ctx, &id, NULL); if (r != 1) { nestegg_destroy(ctx); return 0; } if (id != ID_EBML) { nestegg_destroy(ctx); return 0; } if (ne_ctx_push(ctx, ne_top_level_elements, ctx) < 0) { nestegg_destroy(ctx); return -1; } /* we don't check the return value of ne_parse, that might fail because max_offset is not on a valid element end point. We only want to check the EBML ID and that the doctype is "webm". */ ne_parse(ctx, NULL, max_offset); while (ctx->ancestor) ne_ctx_pop(ctx); if (ne_get_string(ctx->ebml.doctype, &doctype) != 0 || strcmp(doctype, "webm") != 0) { nestegg_destroy(ctx); return 0; } nestegg_destroy(ctx); return 1; } static void ne_free_block_additions(struct block_additional * block_additional) { while (block_additional) { struct block_additional * tmp = block_additional; block_additional = block_additional->next; free(tmp->data); free(tmp); } } int nestegg_init(nestegg ** context, nestegg_io io, nestegg_log callback, int64_t max_offset) { int r; uint64_t id, version, docversion; struct ebml_list_node * track; char * doctype; nestegg * ctx; if (ne_context_new(&ctx, io, callback) != 0) return -1; r = ne_peek_element(ctx, &id, NULL); if (r != 1) { nestegg_destroy(ctx); return -1; } if (id != ID_EBML) { nestegg_destroy(ctx); return -1; } ctx->log(ctx, NESTEGG_LOG_DEBUG, "ctx %p", ctx); if (ne_ctx_push(ctx, ne_top_level_elements, ctx) < 0) { nestegg_destroy(ctx); return -1; } r = ne_parse(ctx, NULL, max_offset); while (ctx->ancestor) ne_ctx_pop(ctx); if (r != 1) { nestegg_destroy(ctx); return -1; } if (ne_get_uint(ctx->ebml.ebml_read_version, &version) != 0) version = 1; if (version != 1) { nestegg_destroy(ctx); return -1; } if (ne_get_string(ctx->ebml.doctype, &doctype) != 0) doctype = "matroska"; if (!!strcmp(doctype, "webm") && !!strcmp(doctype, "matroska")) { nestegg_destroy(ctx); return -1; } if (ne_get_uint(ctx->ebml.doctype_read_version, &docversion) != 0) docversion = 1; if (docversion < 1 || docversion > 2) { nestegg_destroy(ctx); return -1; } if (!ctx->segment.tracks.track_entry.head) { nestegg_destroy(ctx); return -1; } track = ctx->segment.tracks.track_entry.head; ctx->track_count = 0; while (track) { ctx->track_count += 1; track = track->next; } r = ne_ctx_save(ctx, &ctx->saved); if (r != 0) { nestegg_destroy(ctx); return -1; } *context = ctx; return 0; } void nestegg_destroy(nestegg * ctx) { assert(ctx->ancestor == NULL); if (ctx->alloc_pool) ne_pool_destroy(ctx->alloc_pool); free(ctx->io); free(ctx); } int nestegg_duration(nestegg * ctx, uint64_t * duration) { uint64_t tc_scale; double unscaled_duration; if (ne_get_float(ctx->segment.info.duration, &unscaled_duration) != 0) return -1; tc_scale = ne_get_timecode_scale(ctx); if (tc_scale == 0) return -1; if (unscaled_duration != unscaled_duration || unscaled_duration < 0 || unscaled_duration >= (double) UINT64_MAX || (uint64_t) unscaled_duration > UINT64_MAX / tc_scale) return -1; *duration = (uint64_t) (unscaled_duration * tc_scale); return 0; } int nestegg_tstamp_scale(nestegg * ctx, uint64_t * scale) { *scale = ne_get_timecode_scale(ctx); if (*scale == 0) return -1; return 0; } int nestegg_track_count(nestegg * ctx, unsigned int * tracks) { *tracks = ctx->track_count; return 0; } int nestegg_get_cue_point(nestegg * ctx, unsigned int cluster_num, int64_t max_offset, int64_t * start_pos, int64_t * end_pos, uint64_t * tstamp) { int range_obtained = 0; unsigned int cluster_count = 0; struct cue_point * cue_point; struct cue_track_positions * pos; uint64_t seek_pos, track_number, tc_scale, time; struct ebml_list_node * cues_node = ctx->segment.cues.cue_point.head; struct ebml_list_node * cue_pos_node = NULL; unsigned int track = 0, track_count = 0, track_index; if (!start_pos || !end_pos || !tstamp) return -1; /* Initialise return values */ *start_pos = -1; *end_pos = -1; *tstamp = 0; if (!cues_node) { ne_init_cue_points(ctx, max_offset); cues_node = ctx->segment.cues.cue_point.head; /* Verify cues have been added to context. */ if (!cues_node) return -1; } nestegg_track_count(ctx, &track_count); tc_scale = ne_get_timecode_scale(ctx); if (tc_scale == 0) return -1; while (cues_node && !range_obtained) { assert(cues_node->id == ID_CUE_POINT); cue_point = cues_node->data; cue_pos_node = cue_point->cue_track_positions.head; while (cue_pos_node) { assert(cue_pos_node->id == ID_CUE_TRACK_POSITIONS); pos = cue_pos_node->data; for (track = 0; track < track_count; ++track) { if (ne_get_uint(pos->track, &track_number) != 0) return -1; if (ne_map_track_number_to_index(ctx, track_number, &track_index) != 0) return -1; if (track_index == track) { if (ne_get_uint(pos->cluster_position, &seek_pos) != 0) return -1; if (cluster_count == cluster_num) { *start_pos = ctx->segment_offset + seek_pos; if (ne_get_uint(cue_point->time, &time) != 0) return -1; *tstamp = time * tc_scale; } else if (cluster_count == cluster_num + 1) { *end_pos = ctx->segment_offset + seek_pos - 1; range_obtained = 1; break; } cluster_count++; } } cue_pos_node = cue_pos_node->next; } cues_node = cues_node->next; } return 0; } int nestegg_offset_seek(nestegg * ctx, uint64_t offset) { int r; if (offset > INT64_MAX) return -1; /* Seek and set up parser state for segment-level element (Cluster). */ r = ne_io_seek(ctx->io, offset, NESTEGG_SEEK_SET); if (r != 0) return -1; ctx->last_valid = 0; assert(ctx->ancestor == NULL); return 0; } int nestegg_track_seek(nestegg * ctx, unsigned int track, uint64_t tstamp) { int r; struct cue_point * cue_point; struct cue_track_positions * pos; uint64_t seek_pos, tc_scale; /* If there are no cues loaded, check for cues element in the seek head and load it. */ if (!ctx->segment.cues.cue_point.head) { r = ne_init_cue_points(ctx, -1); if (r != 0) return -1; } tc_scale = ne_get_timecode_scale(ctx); if (tc_scale == 0) return -1; cue_point = ne_find_cue_point_for_tstamp(ctx, ctx->segment.cues.cue_point.head, track, tc_scale, tstamp); if (!cue_point) return -1; pos = ne_find_cue_position_for_track(ctx, cue_point->cue_track_positions.head, track); if (pos == NULL) return -1; if (ne_get_uint(pos->cluster_position, &seek_pos) != 0) return -1; /* Seek to (we assume) the start of a Cluster element. */ r = nestegg_offset_seek(ctx, ctx->segment_offset + seek_pos); if (r != 0) return -1; return 0; } int nestegg_track_type(nestegg * ctx, unsigned int track) { struct track_entry * entry; uint64_t type; entry = ne_find_track_entry(ctx, track); if (!entry) return -1; if (ne_get_uint(entry->type, &type) != 0) return -1; if (type == TRACK_TYPE_VIDEO) return NESTEGG_TRACK_VIDEO; if (type == TRACK_TYPE_AUDIO) return NESTEGG_TRACK_AUDIO; return NESTEGG_TRACK_UNKNOWN; } int nestegg_track_codec_id(nestegg * ctx, unsigned int track) { char * codec_id; struct track_entry * entry; entry = ne_find_track_entry(ctx, track); if (!entry) return -1; if (ne_get_string(entry->codec_id, &codec_id) != 0) return -1; if (strcmp(codec_id, TRACK_ID_VP8) == 0) return NESTEGG_CODEC_VP8; if (strcmp(codec_id, TRACK_ID_VP9) == 0) return NESTEGG_CODEC_VP9; if (strcmp(codec_id, TRACK_ID_AV1) == 0) return NESTEGG_CODEC_AV1; if (strcmp(codec_id, TRACK_ID_VORBIS) == 0) return NESTEGG_CODEC_VORBIS; if (strcmp(codec_id, TRACK_ID_OPUS) == 0) return NESTEGG_CODEC_OPUS; return NESTEGG_CODEC_UNKNOWN; } int nestegg_track_codec_data_count(nestegg * ctx, unsigned int track, unsigned int * count) { struct track_entry * entry; struct ebml_binary codec_private; int codec_id; unsigned char * p; *count = 0; entry = ne_find_track_entry(ctx, track); if (!entry) return -1; codec_id = nestegg_track_codec_id(ctx, track); if (codec_id == NESTEGG_CODEC_OPUS) { *count = 1; return 0; } if (codec_id != NESTEGG_CODEC_VORBIS) return -1; if (ne_get_binary(entry->codec_private, &codec_private) != 0) return -1; if (codec_private.length < 1) return -1; p = codec_private.data; *count = *p + 1; if (*count > 3) return -1; return 0; } int nestegg_track_codec_data(nestegg * ctx, unsigned int track, unsigned int item, unsigned char ** data, size_t * length) { struct track_entry * entry; struct ebml_binary codec_private; *data = NULL; *length = 0; entry = ne_find_track_entry(ctx, track); if (!entry) return -1; if (nestegg_track_codec_id(ctx, track) != NESTEGG_CODEC_VORBIS && nestegg_track_codec_id(ctx, track) != NESTEGG_CODEC_OPUS) return -1; if (ne_get_binary(entry->codec_private, &codec_private) != 0) return -1; if (nestegg_track_codec_id(ctx, track) == NESTEGG_CODEC_VORBIS) { uint64_t count; uint64_t sizes[3]; size_t total; unsigned char * p; unsigned int i; int r; nestegg_io io; struct io_buffer userdata; userdata.buffer = codec_private.data; userdata.length = codec_private.length; userdata.offset = 0; io.read = ne_buffer_read; io.seek = ne_buffer_seek; io.tell = ne_buffer_tell; io.userdata = &userdata; total = 0; r = ne_read_uint(&io, &count, 1); if (r != 1) return r; total += 1; count += 1; if (count > 3) return -1; r = ne_read_xiph_lacing(&io, codec_private.length, &total, count, sizes); if (r != 1) return r; if (item >= count) return -1; p = codec_private.data + total; for (i = 0; i < item; ++i) { p += sizes[i]; } assert((size_t) (p - codec_private.data) <= codec_private.length && codec_private.length - (p - codec_private.data) >= sizes[item]); *data = p; *length = sizes[item]; } else { if (item >= 1) return -1; *data = codec_private.data; *length = codec_private.length; } return 0; } int nestegg_track_video_params(nestegg * ctx, unsigned int track, nestegg_video_params * params) { struct track_entry * entry; uint64_t value; double fvalue; memset(params, 0, sizeof(*params)); entry = ne_find_track_entry(ctx, track); if (!entry) return -1; if (nestegg_track_type(ctx, track) != NESTEGG_TRACK_VIDEO) return -1; value = 0; ne_get_uint(entry->video.stereo_mode, &value); if (value <= NESTEGG_VIDEO_STEREO_TOP_BOTTOM || value == NESTEGG_VIDEO_STEREO_RIGHT_LEFT) params->stereo_mode = value; value = 0; ne_get_uint(entry->video.alpha_mode, &value); params->alpha_mode = value; if (ne_get_uint(entry->video.pixel_width, &value) != 0) return -1; params->width = value; if (ne_get_uint(entry->video.pixel_height, &value) != 0) return -1; params->height = value; value = 0; ne_get_uint(entry->video.pixel_crop_bottom, &value); params->crop_bottom = value; value = 0; ne_get_uint(entry->video.pixel_crop_top, &value); params->crop_top = value; value = 0; ne_get_uint(entry->video.pixel_crop_left, &value); params->crop_left = value; value = 0; ne_get_uint(entry->video.pixel_crop_right, &value); params->crop_right = value; value = params->width; ne_get_uint(entry->video.display_width, &value); params->display_width = value; value = params->height; ne_get_uint(entry->video.display_height, &value); params->display_height = value; value = 2; ne_get_uint(entry->video.colour.matrix_coefficients, &value); params->matrix_coefficients = value; value = 0; ne_get_uint(entry->video.colour.range, &value); params->range = value; value = 2; ne_get_uint(entry->video.colour.transfer_characteristics, &value); params->transfer_characteristics = value; value = 2; ne_get_uint(entry->video.colour.primaries, &value); params->primaries = value; fvalue = strtod("NaN", NULL); ne_get_float(entry->video.colour.mastering_metadata.primary_r_chromacity_x, &fvalue); params->primary_r_chromacity_x = fvalue; fvalue = strtod("NaN", NULL); ne_get_float(entry->video.colour.mastering_metadata.primary_r_chromacity_y, &fvalue); params->primary_r_chromacity_y = fvalue; fvalue = strtod("NaN", NULL); ne_get_float(entry->video.colour.mastering_metadata.primary_g_chromacity_x, &fvalue); params->primary_g_chromacity_x = fvalue; fvalue = strtod("NaN", NULL); ne_get_float(entry->video.colour.mastering_metadata.primary_g_chromacity_y, &fvalue); params->primary_g_chromacity_y = fvalue; fvalue = strtod("NaN", NULL); ne_get_float(entry->video.colour.mastering_metadata.primary_b_chromacity_x, &fvalue); params->primary_b_chromacity_x = fvalue; fvalue = strtod("NaN", NULL); ne_get_float(entry->video.colour.mastering_metadata.primary_b_chromacity_y, &fvalue); params->primary_b_chromacity_y = fvalue; fvalue = strtod("NaN", NULL); ne_get_float(entry->video.colour.mastering_metadata.white_point_chromaticity_x, &fvalue); params->white_point_chromaticity_x = fvalue; fvalue = strtod("NaN", NULL); ne_get_float(entry->video.colour.mastering_metadata.white_point_chromaticity_y, &fvalue); params->white_point_chromaticity_y = fvalue; fvalue = strtod("NaN", NULL); ne_get_float(entry->video.colour.mastering_metadata.luminance_max, &fvalue); params->luminance_max = fvalue; fvalue = strtod("NaN", NULL); ne_get_float(entry->video.colour.mastering_metadata.luminance_min, &fvalue); params->luminance_min = fvalue; return 0; } int nestegg_track_audio_params(nestegg * ctx, unsigned int track, nestegg_audio_params * params) { struct track_entry * entry; uint64_t value; memset(params, 0, sizeof(*params)); entry = ne_find_track_entry(ctx, track); if (!entry) return -1; if (nestegg_track_type(ctx, track) != NESTEGG_TRACK_AUDIO) return -1; params->rate = 8000; ne_get_float(entry->audio.sampling_frequency, ¶ms->rate); value = 1; ne_get_uint(entry->audio.channels, &value); params->channels = value; value = 16; ne_get_uint(entry->audio.bit_depth, &value); params->depth = value; value = 0; ne_get_uint(entry->codec_delay, &value); params->codec_delay = value; value = 0; ne_get_uint(entry->seek_preroll, &value); params->seek_preroll = value; return 0; } int nestegg_track_encoding(nestegg * ctx, unsigned int track) { struct track_entry * entry; struct content_encoding * encoding; uint64_t encoding_value; entry = ne_find_track_entry(ctx, track); if (!entry) { ctx->log(ctx, NESTEGG_LOG_ERROR, "No track entry found"); return -1; } if (!entry->content_encodings.content_encoding.head) { /* Default encoding is compression */ return NESTEGG_ENCODING_COMPRESSION; } encoding = entry->content_encodings.content_encoding.head->data; encoding_value = NESTEGG_ENCODING_COMPRESSION; ne_get_uint(encoding->content_encoding_type, &encoding_value); if (encoding_value != NESTEGG_ENCODING_COMPRESSION && encoding_value != NESTEGG_ENCODING_ENCRYPTION) { ctx->log(ctx, NESTEGG_LOG_ERROR, "Invalid ContentEncoding element found"); return -1; } return encoding_value; } int nestegg_track_content_enc_key_id(nestegg * ctx, unsigned int track, unsigned char const ** content_enc_key_id, size_t * content_enc_key_id_length) { struct track_entry * entry; struct content_encoding * encoding; struct content_encryption * encryption; struct content_enc_aes_settings * aes_settings; struct nestegg_encryption_params; uint64_t value; struct ebml_binary enc_key_id; entry = ne_find_track_entry(ctx, track); if (!entry) { ctx->log(ctx, NESTEGG_LOG_ERROR, "No track entry found"); return -1; } if (!entry->content_encodings.content_encoding.head) { ctx->log(ctx, NESTEGG_LOG_ERROR, "No ContentEncoding element found"); return -1; } encoding = entry->content_encodings.content_encoding.head->data; value = 0; ne_get_uint(encoding->content_encoding_type, &value); if (value != NESTEGG_ENCODING_ENCRYPTION) { ctx->log(ctx, NESTEGG_LOG_ERROR, "Disallowed ContentEncodingType found"); return -1; } if (!encoding->content_encryption.head) { ctx->log(ctx, NESTEGG_LOG_ERROR, "No ContentEncryption element found"); return -1; } encryption = encoding->content_encryption.head->data; value = 0; ne_get_uint(encryption->content_enc_algo, &value); if (value != CONTENT_ENC_ALGO_AES) { ctx->log(ctx, NESTEGG_LOG_ERROR, "Disallowed ContentEncAlgo found"); return -1; } if (!encryption->content_enc_aes_settings.head) { ctx->log(ctx, NESTEGG_LOG_ERROR, "No ContentEncAesSettings element found"); return -1; } aes_settings = encryption->content_enc_aes_settings.head->data; value = AES_SETTINGS_CIPHER_CTR; ne_get_uint(aes_settings->aes_settings_cipher_mode, &value); if (value != AES_SETTINGS_CIPHER_CTR) { ctx->log(ctx, NESTEGG_LOG_ERROR, "Disallowed AESSettingCipherMode used"); return -1; } if (ne_get_binary(encryption->content_enc_key_id, &enc_key_id) != 0) { ctx->log(ctx, NESTEGG_LOG_ERROR, "Could not retrieve track ContentEncKeyId"); return -1; } *content_enc_key_id = enc_key_id.data; *content_enc_key_id_length = enc_key_id.length; return 0; } int nestegg_track_default_duration(nestegg * ctx, unsigned int track, uint64_t * duration) { struct track_entry * entry; uint64_t value; entry = ne_find_track_entry(ctx, track); if (!entry) return -1; if (ne_get_uint(entry->default_duration, &value) != 0) return -1; *duration = value; return 0; } int nestegg_read_reset(nestegg * ctx) { assert(ctx->ancestor == NULL); return ne_ctx_restore(ctx, &ctx->saved); } int nestegg_read_packet(nestegg * ctx, nestegg_packet ** pkt) { int r, read_block = 0; uint64_t id, size; *pkt = NULL; assert(ctx->ancestor == NULL); /* Prepare for read_reset to resume parsing from this point upon error. */ r = ne_ctx_save(ctx, &ctx->saved); if (r != 0) return -1; while (!read_block) { r = ne_read_element(ctx, &id, &size); if (r != 1) return r; switch (id) { case ID_CLUSTER: { r = ne_read_element(ctx, &id, &size); if (r != 1) return r; /* Matroska may place a CRC32 before the Timecode. Skip and continue parsing. */ if (id == ID_CRC32) { r = ne_io_read_skip(ctx->io, size); if (r != 1) return r; r = ne_read_element(ctx, &id, &size); if (r != 1) return r; } /* Timecode must be the first element in a Cluster, per WebM spec. */ if (id != ID_TIMECODE) return -1; r = ne_read_uint(ctx->io, &ctx->cluster_timecode, size); if (r != 1) return r; ctx->read_cluster_timecode = 1; break; } case ID_SIMPLE_BLOCK: r = ne_read_block(ctx, id, size, pkt); if (r != 1) return r; read_block = 1; break; case ID_BLOCK_GROUP: { int64_t block_group_end; uint64_t block_duration = 0; int read_block_duration = 0; int64_t discard_padding = 0; int read_discard_padding = 0; int64_t reference_block = 0; int read_reference_block = 0; struct block_additional * block_additional = NULL; uint64_t tc_scale; block_group_end = ne_io_tell(ctx->io) + size; /* Read the entire BlockGroup manually. */ while (ne_io_tell(ctx->io) < block_group_end) { r = ne_read_element(ctx, &id, &size); if (r != 1) { ne_free_block_additions(block_additional); if (*pkt) { nestegg_free_packet(*pkt); *pkt = NULL; } return r; } switch (id) { case ID_BLOCK: { if (*pkt) { ctx->log(ctx, NESTEGG_LOG_DEBUG, "read_packet: multiple Blocks in BlockGroup, dropping previously read Block"); nestegg_free_packet(*pkt); } r = ne_read_block(ctx, id, size, pkt); if (r != 1) { ne_free_block_additions(block_additional); if (*pkt) { nestegg_free_packet(*pkt); *pkt = NULL; } return r; } read_block = 1; break; } case ID_BLOCK_DURATION: { r = ne_read_uint(ctx->io, &block_duration, size); if (r != 1) { ne_free_block_additions(block_additional); if (*pkt) { nestegg_free_packet(*pkt); *pkt = NULL; } return r; } tc_scale = ne_get_timecode_scale(ctx); if (tc_scale == 0) { ne_free_block_additions(block_additional); if (*pkt) { nestegg_free_packet(*pkt); *pkt = NULL; } return -1; } block_duration *= tc_scale; read_block_duration = 1; break; } case ID_DISCARD_PADDING: { r = ne_read_int(ctx->io, &discard_padding, size); if (r != 1) { ne_free_block_additions(block_additional); if (*pkt) { nestegg_free_packet(*pkt); *pkt = NULL; } return r; } read_discard_padding = 1; break; } case ID_BLOCK_ADDITIONS: { /* There should only be one BlockAdditions; treat multiple as an error. */ if (block_additional) { ne_free_block_additions(block_additional); if (*pkt) { nestegg_free_packet(*pkt); *pkt = NULL; } return -1; } r = ne_read_block_additions(ctx, size, &block_additional); if (r != 1) { ne_free_block_additions(block_additional); if (*pkt) { nestegg_free_packet(*pkt); *pkt = NULL; } return r; } break; } case ID_REFERENCE_BLOCK: { r = ne_read_int(ctx->io, &reference_block, size); if (r != 1) { ne_free_block_additions(block_additional); if (*pkt) { nestegg_free_packet(*pkt); *pkt = NULL; } return r; } read_reference_block = 1; break; } default: /* We don't know what this element is, so skip over it */ if (id != ID_VOID && id != ID_CRC32) ctx->log(ctx, NESTEGG_LOG_DEBUG, "read_packet: unknown element %llx in BlockGroup", id); r = ne_io_read_skip(ctx->io, size); if (r != 1) { ne_free_block_additions(block_additional); if (*pkt) { nestegg_free_packet(*pkt); *pkt = NULL; } return r; } } } assert(read_block == (*pkt != NULL)); if (*pkt) { (*pkt)->duration = block_duration; (*pkt)->read_duration = read_block_duration; (*pkt)->discard_padding = discard_padding; (*pkt)->read_discard_padding = read_discard_padding; (*pkt)->reference_block = reference_block; (*pkt)->read_reference_block = read_reference_block; (*pkt)->block_additional = block_additional; if ((*pkt)->read_reference_block) /* If a packet has a reference block it contains predictive frames and no keyframes */ (*pkt)->keyframe = NESTEGG_PACKET_HAS_KEYFRAME_FALSE; } else { ne_free_block_additions(block_additional); } break; } default: ctx->log(ctx, NESTEGG_LOG_DEBUG, "read_packet: unknown element %llx", id); r = ne_io_read_skip(ctx->io, size); if (r != 1) return r; } } return 1; } void nestegg_free_packet(nestegg_packet * pkt) { struct frame * frame; while (pkt->frame) { frame = pkt->frame; pkt->frame = frame->next; ne_free_frame(frame); } ne_free_block_additions(pkt->block_additional); free(pkt); } int nestegg_packet_has_keyframe(nestegg_packet * pkt) { return pkt->keyframe; } int nestegg_packet_track(nestegg_packet * pkt, unsigned int * track) { *track = pkt->track; return 0; } int nestegg_packet_tstamp(nestegg_packet * pkt, uint64_t * tstamp) { *tstamp = pkt->timecode; return 0; } int nestegg_packet_duration(nestegg_packet * pkt, uint64_t * duration) { if (!pkt->read_duration) return -1; *duration = pkt->duration; return 0; } int nestegg_packet_discard_padding(nestegg_packet * pkt, int64_t * discard_padding) { if (!pkt->read_discard_padding) return -1; *discard_padding = pkt->discard_padding; return 0; } int nestegg_packet_reference_block(nestegg_packet * pkt, int64_t * reference_block) { if (!pkt->read_reference_block) return -1; *reference_block = pkt->reference_block; return 0; } int nestegg_packet_count(nestegg_packet * pkt, unsigned int * count) { struct frame * f = pkt->frame; *count = 0; while (f) { *count += 1; f = f->next; } return 0; } int nestegg_packet_data(nestegg_packet * pkt, unsigned int item, unsigned char ** data, size_t * length) { struct frame * f = pkt->frame; unsigned int count = 0; *data = NULL; *length = 0; while (f) { if (count == item) { *data = f->data; *length = f->length; return 0; } count += 1; f = f->next; } return -1; } int nestegg_packet_additional_data(nestegg_packet * pkt, unsigned int id, unsigned char ** data, size_t * length) { struct block_additional * a = pkt->block_additional; *data = NULL; *length = 0; while (a) { if (a->id == id) { *data = a->data; *length = a->length; return 0; } a = a->next; } return -1; } int nestegg_packet_encryption(nestegg_packet * pkt) { struct frame * f = pkt->frame; unsigned char encrypted_bit; unsigned char partitioned_bit; if (!f->frame_encryption) return NESTEGG_PACKET_HAS_SIGNAL_BYTE_FALSE; /* Should never have parsed blocks with both encryption and lacing */ assert(f->next == NULL); encrypted_bit = f->frame_encryption->signal_byte & ENCRYPTED_BIT_MASK; partitioned_bit = f->frame_encryption->signal_byte & PARTITIONED_BIT_MASK; if (encrypted_bit != PACKET_ENCRYPTED) return NESTEGG_PACKET_HAS_SIGNAL_BYTE_UNENCRYPTED; if (partitioned_bit == PACKET_PARTITIONED) return NESTEGG_PACKET_HAS_SIGNAL_BYTE_PARTITIONED; return NESTEGG_PACKET_HAS_SIGNAL_BYTE_ENCRYPTED; } int nestegg_packet_iv(nestegg_packet * pkt, unsigned char const ** iv, size_t * length) { struct frame * f = pkt->frame; unsigned char encrypted_bit; *iv = NULL; *length = 0; if (!f->frame_encryption) return -1; /* Should never have parsed blocks with both encryption and lacing */ assert(f->next == NULL); encrypted_bit = f->frame_encryption->signal_byte & ENCRYPTED_BIT_MASK; if (encrypted_bit != PACKET_ENCRYPTED) return 0; *iv = f->frame_encryption->iv; *length = f->frame_encryption->length; return 0; } int nestegg_packet_offsets(nestegg_packet * pkt, uint32_t const ** partition_offsets, uint8_t * num_partitions) { struct frame * f = pkt->frame; unsigned char encrypted_bit; unsigned char partitioned_bit; *partition_offsets = NULL; *num_partitions = 0; if (!f->frame_encryption) return -1; /* Should never have parsed blocks with both encryption and lacing */ assert(f->next == NULL); encrypted_bit = f->frame_encryption->signal_byte & ENCRYPTED_BIT_MASK; partitioned_bit = f->frame_encryption->signal_byte & PARTITIONED_BIT_MASK; if (encrypted_bit != PACKET_ENCRYPTED || partitioned_bit != PACKET_PARTITIONED) return -1; *num_partitions = f->frame_encryption->num_partitions; *partition_offsets = f->frame_encryption->partition_offsets; return 0; } int nestegg_has_cues(nestegg * ctx) { return ctx->segment.cues.cue_point.head || ne_find_seek_for_id(ctx->segment.seek_head.head, ID_CUES); } int nestegg_sniff(unsigned char const * buffer, size_t length) { nestegg_io io; struct io_buffer userdata; userdata.buffer = buffer; userdata.length = length; userdata.offset = 0; io.read = ne_buffer_read; io.seek = ne_buffer_seek; io.tell = ne_buffer_tell; io.userdata = &userdata; return ne_match_webm(io, length); } kew/include/nestegg/nestegg.h000066400000000000000000000573461507107350600165530ustar00rootroot00000000000000/* * Copyright © 2010 Mozilla Foundation * * This program is made available under an ISC-style license. See the * accompanying file LICENSE for details. */ #if !defined(NESTEGG_671cac2a_365d_ed69_d7a3_4491d3538d79) #define NESTEGG_671cac2a_365d_ed69_d7a3_4491d3538d79 #include #include #include #if defined(__cplusplus) extern "C" { #endif /** @mainpage @section intro Introduction This is the documentation for the libnestegg C API. libnestegg is a demultiplexing library for WebM media files. @section example Example code @code nestegg * demux_ctx; nestegg_init(&demux_ctx, io, NULL, -1); nestegg_packet * pkt; while ((r = nestegg_read_packet(demux_ctx, &pkt)) > 0) { unsigned int track; nestegg_packet_track(pkt, &track); // This example decodes the first track only. if (track == 0) { unsigned int chunk, chunks; nestegg_packet_count(pkt, &chunks); // Decode each chunk of data. for (chunk = 0; chunk < chunks; ++chunk) { unsigned char * data; size_t data_size; nestegg_packet_data(pkt, chunk, &data, &data_size); example_codec_decode(codec_ctx, data, data_size); } } nestegg_free_packet(pkt); } nestegg_destroy(demux_ctx); @endcode */ /** @file The libnestegg C API. */ #define NESTEGG_TRACK_VIDEO 0 /**< Track is of type video. */ #define NESTEGG_TRACK_AUDIO 1 /**< Track is of type audio. */ #define NESTEGG_TRACK_UNKNOWN INT_MAX /**< Track is of type unknown. */ #define NESTEGG_CODEC_VP8 0 /**< Track uses Google On2 VP8 codec. */ #define NESTEGG_CODEC_VORBIS 1 /**< Track uses Xiph Vorbis codec. */ #define NESTEGG_CODEC_VP9 2 /**< Track uses Google On2 VP9 codec. */ #define NESTEGG_CODEC_OPUS 3 /**< Track uses Xiph Opus codec. */ #define NESTEGG_CODEC_AV1 4 /**< Track uses AOMedia AV1 codec. */ #define NESTEGG_CODEC_UNKNOWN INT_MAX /**< Track uses unknown codec. */ #define NESTEGG_VIDEO_MONO 0 /**< Track is mono video. */ #define NESTEGG_VIDEO_STEREO_LEFT_RIGHT 1 /**< Track is side-by-side stereo video. Left first. */ #define NESTEGG_VIDEO_STEREO_BOTTOM_TOP 2 /**< Track is top-bottom stereo video. Right first. */ #define NESTEGG_VIDEO_STEREO_TOP_BOTTOM 3 /**< Track is top-bottom stereo video. Left first. */ #define NESTEGG_VIDEO_STEREO_RIGHT_LEFT 11 /**< Track is side-by-side stereo video. Right first. */ #define NESTEGG_SEEK_SET 0 /**< Seek offset relative to beginning of stream. */ #define NESTEGG_SEEK_CUR 1 /**< Seek offset relative to current position in stream. */ #define NESTEGG_SEEK_END 2 /**< Seek offset relative to end of stream. */ #define NESTEGG_LOG_DEBUG 1 /**< Debug level log message. */ #define NESTEGG_LOG_INFO 10 /**< Informational level log message. */ #define NESTEGG_LOG_WARNING 100 /**< Warning level log message. */ #define NESTEGG_LOG_ERROR 1000 /**< Error level log message. */ #define NESTEGG_LOG_CRITICAL 10000 /**< Critical level log message. */ #define NESTEGG_ENCODING_COMPRESSION 0 /**< Content encoding type is compression. */ #define NESTEGG_ENCODING_ENCRYPTION 1 /**< Content encoding type is encryption. */ #define NESTEGG_PACKET_HAS_SIGNAL_BYTE_FALSE 0 /**< Packet does not have signal byte */ #define NESTEGG_PACKET_HAS_SIGNAL_BYTE_UNENCRYPTED 1 /**< Packet has signal byte and is unencrypted */ #define NESTEGG_PACKET_HAS_SIGNAL_BYTE_ENCRYPTED 2 /**< Packet has signal byte and is encrypted */ #define NESTEGG_PACKET_HAS_SIGNAL_BYTE_PARTITIONED 4 /**< Packet has signal byte and is partitioned */ #define NESTEGG_PACKET_HAS_KEYFRAME_FALSE 0 /**< Packet contains only keyframes. */ #define NESTEGG_PACKET_HAS_KEYFRAME_TRUE 1 /**< Packet does not contain any keyframes */ #define NESTEGG_PACKET_HAS_KEYFRAME_UNKNOWN 2 /**< Packet may or may not contain keyframes */ typedef struct nestegg nestegg; /**< Opaque handle referencing the stream state. */ typedef struct nestegg_packet nestegg_packet; /**< Opaque handle referencing a packet of data. */ /** User supplied IO context. */ typedef struct { /** User supplied read callback. @param buffer Buffer to read data into. @param length Length of supplied buffer in bytes. @param userdata The #userdata supplied by the user. @retval 1 Read succeeded. @retval 0 End of stream. @retval -1 Error. */ int (* read)(void * buffer, size_t length, void * userdata); /** User supplied seek callback. @param offset Offset within the stream to seek to. @param whence Seek direction. One of #NESTEGG_SEEK_SET, #NESTEGG_SEEK_CUR, or #NESTEGG_SEEK_END. @param userdata The #userdata supplied by the user. @retval 0 Seek succeeded. @retval -1 Error. */ int (* seek)(int64_t offset, int whence, void * userdata); /** User supplied tell callback. @param userdata The #userdata supplied by the user. @returns Current position within the stream. @retval -1 Error. */ int64_t (* tell)(void * userdata); /** User supplied pointer to be passed to the IO callbacks. */ void * userdata; } nestegg_io; /** Parameters specific to a video track. */ typedef struct { unsigned int stereo_mode; /**< Video mode. One of #NESTEGG_VIDEO_MONO, #NESTEGG_VIDEO_STEREO_LEFT_RIGHT, #NESTEGG_VIDEO_STEREO_BOTTOM_TOP, or #NESTEGG_VIDEO_STEREO_TOP_BOTTOM. */ unsigned int width; /**< Width of the video frame in pixels. */ unsigned int height; /**< Height of the video frame in pixels. */ unsigned int display_width; /**< Display width of the video frame in pixels. */ unsigned int display_height; /**< Display height of the video frame in pixels. */ unsigned int crop_bottom; /**< Pixels to crop from the bottom of the frame. */ unsigned int crop_top; /**< Pixels to crop from the top of the frame. */ unsigned int crop_left; /**< Pixels to crop from the left of the frame. */ unsigned int crop_right; /**< Pixels to crop from the right of the frame. */ unsigned int alpha_mode; /**< 1 if an additional opacity stream is available, otherwise 0. */ unsigned int matrix_coefficients; /**< See Table 4 of ISO/IEC 23001-8:2016. */ unsigned int range; /**< Clipping of color ranges. */ unsigned int transfer_characteristics; /**< See Table 3 of ISO/IEC 23091-4. */ unsigned int primaries; /**< See Table 2 of ISO/IEC 23091-4. */ double primary_r_chromacity_x; /**< Red X chromaticity coordinate per CIE 1931. NaN means element not present. */ double primary_r_chromacity_y; /**< Red Y chromaticity coordinate per CIE 1931. NaN means element not present. */ double primary_g_chromacity_x; /**< Green X chromaticity coordinate per CIE 1931. NaN means element not present. */ double primary_g_chromacity_y; /**< Green Y chromaticity coordinate per CIE 1931. NaN means element not present. */ double primary_b_chromacity_x; /**< Blue X chromaticity coordinate per CIE 1931. NaN means element not present. */ double primary_b_chromacity_y; /**< Blue Y chromaticity coordinate per CIE 1931. NaN means element not present. */ double white_point_chromaticity_x; /**< White X chromaticity coordinate per CIE 1931. NaN means element not present. */ double white_point_chromaticity_y; /**< White Y chromaticity coordinate per CIE 1931. NaN means element not present. */ double luminance_max; /**< Maximum luminance in cd/m2. NaN means element not present. */ double luminance_min; /**< Minimum luminance in cd/m2. NaN means element not present. */ } nestegg_video_params; /** Parameters specific to an audio track. */ typedef struct { double rate; /**< Sampling rate in Hz. */ unsigned int channels; /**< Number of audio channels. */ unsigned int depth; /**< Bits per sample. */ uint64_t codec_delay; /**< Nanoseconds that must be discarded from the start. */ uint64_t seek_preroll;/**< Nanoseconds that must be discarded after a seek. */ } nestegg_audio_params; /** Logging callback function pointer. */ typedef void (* nestegg_log)(nestegg * context, unsigned int severity, char const * format, ...); /** Initialize a nestegg context. During initialization the parser will read forward in the stream processing all elements until the first block of media is reached. All track metadata has been processed at this point. @param context Storage for the new nestegg context. @see nestegg_destroy @param io User supplied IO context. @param callback Optional logging callback function pointer. May be NULL. @param max_offset Optional maximum offset to be read. Set -1 to ignore. @retval 0 Success. @retval -1 Error. */ int nestegg_init(nestegg ** context, nestegg_io io, nestegg_log callback, int64_t max_offset); /** Destroy a nestegg context and free associated memory. @param context #nestegg context to be freed. @see nestegg_init */ void nestegg_destroy(nestegg * context); /** Query the duration of the media stream in nanoseconds. @param context Stream context initialized by #nestegg_init. @param duration Storage for the queried duration. @retval 0 Success. @retval -1 Error. */ int nestegg_duration(nestegg * context, uint64_t * duration); /** Query the tstamp scale of the media stream in nanoseconds. @note Timestamps presented by nestegg have been scaled by this value before presentation to the caller. @param context Stream context initialized by #nestegg_init. @param scale Storage for the queried scale factor. @retval 0 Success. @retval -1 Error. */ int nestegg_tstamp_scale(nestegg * context, uint64_t * scale); /** Query the number of tracks in the media stream. @param context Stream context initialized by #nestegg_init. @param tracks Storage for the queried track count. @retval 0 Success. @retval -1 Error. */ int nestegg_track_count(nestegg * context, unsigned int * tracks); /** Query the start and end offset for a particular cluster. @param context Stream context initialized by #nestegg_init. @param cluster_num Zero-based cluster number; order they appear in cues. @param max_offset Optional maximum offset to be read. Set -1 to ignore. @param start_pos Starting offset of the cluster. -1 means non-existant. @param end_pos Starting offset of the cluster. -1 means non-existant or final cluster. @param tstamp Starting timestamp of the cluster. @retval 0 Success. @retval -1 Error. */ int nestegg_get_cue_point(nestegg * context, unsigned int cluster_num, int64_t max_offset, int64_t * start_pos, int64_t * end_pos, uint64_t * tstamp); /** Seek to @a offset. Stream will seek directly to offset. Must be used to seek to the start of a cluster; the parser will not be able to understand other offsets. @param context Stream context initialized by #nestegg_init. @param offset Absolute offset in bytes. @retval 0 Success. @retval -1 Error. */ int nestegg_offset_seek(nestegg * context, uint64_t offset); /** Seek @a track to @a tstamp. Stream seek will terminate at the earliest key point in the stream at or before @a tstamp. Other tracks in the stream will output packets with unspecified but nearby timestamps. @param context Stream context initialized by #nestegg_init. @param track Zero based track number. @param tstamp Absolute timestamp in nanoseconds. @retval 0 Success. @retval -1 Error. */ int nestegg_track_seek(nestegg * context, unsigned int track, uint64_t tstamp); /** Query the type specified by @a track. @param context Stream context initialized by #nestegg_init. @param track Zero based track number. @retval #NESTEGG_TRACK_VIDEO Track type is video. @retval #NESTEGG_TRACK_AUDIO Track type is audio. @retval #NESTEGG_TRACK_UNKNOWN Track type is unknown. @retval -1 Error. */ int nestegg_track_type(nestegg * context, unsigned int track); /** Query the codec ID specified by @a track. @param context Stream context initialized by #nestegg_init. @param track Zero based track number. @retval #NESTEGG_CODEC_VP8 Track codec is VP8. @retval #NESTEGG_CODEC_VP9 Track codec is VP9. @retval #NESTEGG_CODEC_AV1 Track codec is AV1. @retval #NESTEGG_CODEC_VORBIS Track codec is Vorbis. @retval #NESTEGG_CODEC_OPUS Track codec is Opus. @retval #NESTEGG_CODEC_UNKNOWN Track codec is unknown. @retval -1 Error. */ int nestegg_track_codec_id(nestegg * context, unsigned int track); /** Query the number of codec initialization chunks for @a track. Each chunk of data should be passed to the codec initialization functions in the order returned. @param context Stream context initialized by #nestegg_init. @param track Zero based track number. @param count Storage for the queried chunk count. @retval 0 Success. @retval -1 Error. */ int nestegg_track_codec_data_count(nestegg * context, unsigned int track, unsigned int * count); /** Get a pointer to chunk number @a item of codec initialization data for @a track. @param context Stream context initialized by #nestegg_init. @param track Zero based track number. @param item Zero based chunk item number. @param data Storage for the queried data pointer. The data is owned by the #nestegg context. @param length Storage for the queried data size. @retval 0 Success. @retval -1 Error. */ int nestegg_track_codec_data(nestegg * context, unsigned int track, unsigned int item, unsigned char ** data, size_t * length); /** Query the video parameters specified by @a track. @param context Stream context initialized by #nestegg_init. @param track Zero based track number. @param params Storage for the queried video parameters. @retval 0 Success. @retval -1 Error. */ int nestegg_track_video_params(nestegg * context, unsigned int track, nestegg_video_params * params); /** Query the audio parameters specified by @a track. @param context Stream context initialized by #nestegg_init. @param track Zero based track number. @param params Storage for the queried audio parameters. @retval 0 Success. @retval -1 Error. */ int nestegg_track_audio_params(nestegg * context, unsigned int track, nestegg_audio_params * params); /** Query the encoding status for @a track. If a track has multiple encodings the first will be returned. @param context Stream context initialized by #nestegg_init. @param track Zero based track number. @retval #NESTEGG_ENCODING_COMPRESSION The track is compressed, but not encrypted. @retval #NESTEGG_ENCODING_ENCRYPTION The track is encrypted and compressed. @retval -1 Error. */ int nestegg_track_encoding(nestegg * context, unsigned int track); /** Query the ContentEncKeyId for @a track. Will return an error if the track in not encrypted, or is not recognized. @param context Stream context initialized by #nestegg_init. @param track Zero based track number. @param content_enc_key_id Storage for queried id. The content encryption key used. Owned by nestegg and will be freed separately. @param content_enc_key_id_length Length of the queried ContentEncKeyId in bytes. @retval 0 Success. @retval -1 Error. */ int nestegg_track_content_enc_key_id(nestegg * context, unsigned int track, unsigned char const ** content_enc_key_id, size_t * content_enc_key_id_length); /** Query the default frame duration for @a track. For a video track, this is typically the inverse of the video frame rate. @param context Stream context initialized by #nestegg_init. @param track Zero based track number. @param duration Storage for the default duration in nanoseconds. @retval 0 Success. @retval -1 Error. */ int nestegg_track_default_duration(nestegg * context, unsigned int track, uint64_t * duration); /** Reset parser state to the last valid state before nestegg_read_packet failed. @param context Stream context initialized by #nestegg_init. @retval 0 Success. @retval -1 Error. */ int nestegg_read_reset(nestegg * context); /** Read a packet of media data. A packet consists of one or more chunks of data associated with a single track. nestegg_read_packet should be called in a loop while the return value is 1 to drive the stream parser forward. @see nestegg_free_packet @param context Context returned by #nestegg_init. @param packet Storage for the returned nestegg_packet. @retval 1 Additional packets may be read in subsequent calls. @retval 0 End of stream. @retval -1 Error. */ int nestegg_read_packet(nestegg * context, nestegg_packet ** packet); /** Destroy a nestegg_packet and free associated memory. @param packet #nestegg_packet to be freed. @see nestegg_read_packet */ void nestegg_free_packet(nestegg_packet * packet); /** Query the keyframe status for a given packet. @param packet Packet initialized by #nestegg_read_packet. @retval #NESTEGG_PACKET_HAS_KEYFRAME_FALSE Packet contains no keyframes. @retval #NESTEGG_PACKET_HAS_KEYFRAME_TRUE Packet contains keyframes. @retval #NESTEGG_PACKET_HAS_KEYFRAME_UNKNOWN Unknown packet keyframe content. @retval -1 Error. */ int nestegg_packet_has_keyframe(nestegg_packet * packet); /** Query the track number of @a packet. @param packet Packet initialized by #nestegg_read_packet. @param track Storage for the queried zero based track index. @retval 0 Success. @retval -1 Error. */ int nestegg_packet_track(nestegg_packet * packet, unsigned int * track); /** Query the timestamp in nanoseconds of @a packet. @param packet Packet initialized by #nestegg_read_packet. @param tstamp Storage for the queried timestamp in nanoseconds. @retval 0 Success. @retval -1 Error. */ int nestegg_packet_tstamp(nestegg_packet * packet, uint64_t * tstamp); /** Query the duration in nanoseconds of @a packet. @param packet Packet initialized by #nestegg_read_packet. @param duration Storage for the queried duration in nanoseconds. @retval 0 Success. @retval -1 Error. */ int nestegg_packet_duration(nestegg_packet * packet, uint64_t * duration); /** Query the number of data chunks contained in @a packet. @param packet Packet initialized by #nestegg_read_packet. @param count Storage for the queried chunk count. @retval 0 Success. @retval -1 Error. */ int nestegg_packet_count(nestegg_packet * packet, unsigned int * count); /** Get a pointer to chunk number @a item of packet data. @param packet Packet initialized by #nestegg_read_packet. @param item Zero based chunk item number. @param data Storage for the queried data pointer. The data is owned by the #nestegg_packet packet. @param length Storage for the queried data size. @retval 0 Success. @retval -1 Error. */ int nestegg_packet_data(nestegg_packet * packet, unsigned int item, unsigned char ** data, size_t * length); /** Get a pointer to additional data with identifier @a id of additional packet data. If @a id isn't present in the packet, returns -1. @param packet Packet initialized by #nestegg_read_packet. @param id Codec specific identifer. For VP8, use 1 to get a VP8 encoded frame containing an alpha channel in its Y plane. @param data Storage for the queried data pointer. The data is owned by the #nestegg_packet packet. @param length Storage for the queried data size. @retval 0 Success. @retval -1 Error. */ int nestegg_packet_additional_data(nestegg_packet * packet, unsigned int id, unsigned char ** data, size_t * length); /** Returns discard_padding for given packet @param packet Packet initialized by #nestegg_read_packet. @param discard_padding pointer to store discard padding in. @retval 0 Success. @retval -1 Error. */ int nestegg_packet_discard_padding(nestegg_packet * packet, int64_t * discard_padding); /** Query if a packet is encrypted. @param packet Packet initialized by #nestegg_read_packet. @retval #NESTEGG_PACKET_HAS_SIGNAL_BYTE_FALSE No signal byte, encryption information not read from packet. @retval #NESTEGG_PACKET_HAS_SIGNAL_BYTE_UNENCRYPTED Encrypted bit not set, encryption information not read from packet. @retval #NESTEGG_PACKET_HAS_SIGNAL_BYTE_ENCRYPTED Encrypted bit set, encryption infomation read from packet. @retval #NESTEGG_PACKET_HAS_SIGNAL_BYTE_PARTITIONED Partitioned bit set, encryption and parition information read from packet. @retval -1 Error.*/ int nestegg_packet_encryption(nestegg_packet * packet); /** Query the IV for an encrypted packet. Expects a packet from an encrypted track, and will return error if given a packet that has no signal btye. @param packet Packet initialized by #nestegg_read_packet. @param iv Storage for queried iv. @param length Length of returned iv, may be 0. The data is owned by the #nestegg_packet packet. @retval 0 Success. @retval -1 Error. */ int nestegg_packet_iv(nestegg_packet * packet, unsigned char const ** iv, size_t * length); /** Query the packet for offsets. @param packet Packet initialized by #nestegg_read_packet. @param partition_offsets Storage for queried offsets. @param num_offsets Length of returned offsets, may be 0. The data is owned by the #nestegg_packet packet. @retval 0 Success. @retval -1 Error. */ int nestegg_packet_offsets(nestegg_packet * packet, uint32_t const ** partition_offsets, uint8_t * num_offsets); /** Returns reference_block given packet @param packet Packet initialized by #nestegg_read_packet. @param reference_block pointer to store reference block in. @retval 0 Success. @retval -1 Error. */ int nestegg_packet_reference_block(nestegg_packet * packet, int64_t * reference_block); /** Query the presence of cues. @param context Stream context initialized by #nestegg_init. @retval 0 The media has no cues. @retval 1 The media has cues. */ int nestegg_has_cues(nestegg * context); /** Try to determine if the buffer looks like the beginning of a WebM file. @param buffer A buffer containing the beginning of a media file. @param length The size of the buffer. @retval 0 The file is not a WebM file. @retval 1 The file is a WebM file. */ int nestegg_sniff(unsigned char const * buffer, size_t length); #if defined(__cplusplus) } #endif #endif /* NESTEGG_671cac2a_365d_ed69_d7a3_4491d3538d79 */ kew/src/000077500000000000000000000000001507107350600124375ustar00rootroot00000000000000kew/src/appstate.h000066400000000000000000000230541507107350600144350ustar00rootroot00000000000000#ifndef APPSTATE_H #define APPSTATE_H #include "cache.h" #include #include #include #include "theme.h" #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 } ViewState; typedef enum { COLOR_MODE_DEFAULT = 0, // Colors from ANSI 16-color palette theme COLOR_MODE_ALBUM = 1, // Colors derived from album art COLOR_MODE_THEME = 2 // Colors from truecolor theme } ColorMode; typedef struct { 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 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 int visualizerBarWidth; // 0=Thin bars, 1=Bars twice the width or 2=Auto (Depends on window size, default) int replayGainCheckFirst; // Prioritize track or album replay gain setting bool saveRepeatShuffleSettings; // Save repeat and shuffle settings between sessions. Default on. int repeatState; // 0=disabled,1=repeat track ,2=repeat list bool shuffleEnabled; bool trackTitleAsWindowTitle; // Set the window title to the title of the currently playing track Theme theme; // The color theme. bool themeIsSet; // Whether a theme has been loaded; char themeName[NAME_MAX]; // the name part of .theme, usually lowercase first character, unlike theme.name which is taken from within the file. char themeAuthor[NAME_MAX]; ColorMode colorMode; // Which color mode to use. } 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; #ifndef DEFAULTCOLOR #define DEFAULTCOLOR static const unsigned char defaultColor = 150; static const PixelData defaultColorRGB = { .r = defaultColor, .g = defaultColor, .b = defaultColor }; #endif #ifndef KEYVALUEPAIR_STRUCT #define KEYVALUEPAIR_STRUCT typedef struct { char *key; char *value; } KeyValuePair; #endif #ifndef APPSETTINGS_STRUCT typedef struct { char path[MAXPATHLEN]; char theme[NAME_MAX]; char ansiTheme[NAME_MAX]; char colorMode[6]; 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 toggleNotifications[6]; char cycleColorsDerivedFrom[6]; char cycleThemes[6]; char toggleVisualizer[6]; char toggleAscii[6]; char toggleRepeat[6]; char toggleShuffle[6]; char seekBackward[6]; char seekForward[6]; char savePlaylist[6]; char addToFavoritesPlaylist[6]; char updateLibrary[6]; char quit[6]; char altQuit[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 showSearchAlt[6]; char hardShowTrack[6]; char hardShowTrackAlt[6]; char showTrackAlt[6]; char nextPage[6]; char prevPage[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[3]; char mouseAltScrollDownAction[3]; 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 sortLibrary[6]; char visualizerBrailleMode[2]; char progressBarElapsedEvenChar[12]; char progressBarElapsedOddChar[12]; char progressBarApproachingEvenChar[12]; char progressBarApproachingOddChar[12]; char progressBarCurrentEvenChar[12]; char progressBarCurrentOddChar[12]; char visualizerBarWidth[2]; char replayGainCheckFirst[2]; char saveRepeatShuffleSettings[2]; char repeatState[2]; char shuffleEnabled[2]; char trackTitleAsWindowTitle[2]; } AppSettings; #endif #endif kew/src/cache.c000066400000000000000000000046521507107350600136550ustar00rootroot00000000000000#define _XOPEN_SOURCE 700 #include "cache.h" #include #include #include /* cache.c Related to cache which contains paths to cached files. */ #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif Cache *createCache() { Cache *cache = malloc(sizeof(Cache)); if (cache == NULL) { fprintf(stderr, "createCache: malloc\n"); return NULL; } cache->head = NULL; return cache; } void addToCache(Cache *cache, const char *filePath) { if (cache == NULL) { fprintf(stderr, "Cache is null.\n"); return; } if (filePath == NULL || *filePath == '\0') { fprintf(stderr, "Invalid filePath.\n"); return; } if (strnlen(filePath, MAXPATHLEN + 1) >= MAXPATHLEN) { fprintf(stderr, "File path too long.\n"); return; } CacheNode *newNode = malloc(sizeof(CacheNode)); if (newNode == NULL) { fprintf(stderr, "addToCache: malloc\n"); return; } newNode->filePath = strdup(filePath); if (newNode->filePath == NULL) { fprintf(stderr, "addToCache: strdup\n"); free(newNode); // prevent memory leak return; } newNode->next = cache->head; cache->head = newNode; } void deleteCache(Cache *cache) { if (cache == NULL) { fprintf(stderr, "deleteCache: Cache is null.\n"); return; } 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) { if (filePath == NULL) return false; if (cache == NULL) { fprintf(stderr, "existsInCache: Cache is null.\n"); return false; } CacheNode *current = cache->head; while (current != NULL) { if (strcmp(filePath, current->filePath) == 0) { return true; } current = current->next; } return false; } kew/src/cache.h000066400000000000000000000005761507107350600136630ustar00rootroot00000000000000#ifndef CACHE_H #define CACHE_H #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/src/common.c000066400000000000000000000020351507107350600140730ustar00rootroot00000000000000#include #include #include #include "common.h" const char VERSION[] = "3.5.2"; const char LAST_ROW[] = " [F2 Playlist|F3 Library|F4 Track|F5 Search|F6 Help]"; const int MOUSE_DRAG = 32; const int MOUSE_CLICK = 0; double pauseSeconds = 0.0; double totalPauseSeconds = 0.0; double seekAccumulatedSeconds = 0.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) { if (message == NULL) return; 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/src/common.h000066400000000000000000000013651507107350600141050ustar00rootroot00000000000000#ifndef COMMON_H #define COMMON_H #include #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif 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; extern volatile bool refresh; extern double pauseSeconds; extern double totalPauseSeconds; extern double seekAccumulatedSeconds; extern const char VERSION[]; extern const int MOUSE_DRAG; extern const int MOUSE_CLICK; extern const char LAST_ROW[]; extern bool hasPrintedError; void setErrorMessage(const char *message); bool hasErrorMessage(); char *getErrorMessage(); void clearErrorMessage(); #endif kew/src/common_ui.c000066400000000000000000000336431507107350600146010ustar00rootroot00000000000000#define _XOPEN_SOURCE 700 #include "common_ui.h" #include "common.h" #include "term.h" #include "theme.h" #include "utils.h" #include #include #include #include /* common_ui.c UI functions. */ #define MIN_CHANNEL 50 // Minimum color red green blue unsigned int updateCounter = 0; // Name scrolling bool finishedScrolling = false; int lastNamePosition = 0; bool isLongName = false; int scrollDelaySkippedCount = 0; bool isSameNameAsLastTime = false; const int startScrollingDelay = 10; // Delay before beginning to scroll const int scrollingInterval = 1; // Interval between scrolling updates void setRGB(PixelData p) { // ANSI escape code for setting RGB foreground printf("\033[38;2;%d;%d;%dm", p.r, p.g, p.b); } void setAlbumColor(PixelData color) { if (color.r >= 210 && color.g >= 210 && color.b >= 210) { setRGB(defaultColorRGB); } else { setRGB(color); } } void resetColor(void) { printf("\033[0m"); } void inverseText(void) { printf("\x1b[7m"); } void applyColor(ColorMode mode, ColorValue themeColor, PixelData albumColor) { resetColor(); switch (mode) { case COLOR_MODE_ALBUM: setAlbumColor(albumColor); break; case COLOR_MODE_THEME: case COLOR_MODE_DEFAULT: if (themeColor.type == COLOR_TYPE_RGB) { setRGB(themeColor.rgb); // RGB field } else { setTerminalColor(themeColor.ansiIndex); } break; } } 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) { if (table == NULL || max < 0) return 0; // Range validation to avoid unsafe casts if (table[0].first > ucs || table[max].last < ucs) return 0; size_t min = 0; size_t maxIndex = (size_t)max; while (min <= maxIndex) { size_t mid = min + ((maxIndex - min) >> 1); if (ucs > table[mid].last) { min = mid + 1; } else if (ucs < table[mid].first) { if (mid == 0) return 0; maxIndex = 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 width = 0; for (; *pwcs && n-- > 0; pwcs++) { int w; 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 maxDisplayWidth) { mbstate_t state; memset(&state, 0, sizeof(state)); const char *p = src; char *o = dst; wchar_t wc; int widthSum = 0; while (*p) { size_t len = mbrtowc(&wc, p, MB_CUR_MAX, &state); if (len == (size_t)-1) { // Invalid UTF-8/locale error // Skip one byte, reinit state p++; memset(&state, 0, sizeof(state)); continue; } if (len == (size_t)-2) { // Incomplete sequence break; } if (len == 0) { // Null terminator break; } int w = wcwidth(wc); if (w < 0) { // Non-printable character; skip it p += len; continue; } if (widthSum + w > maxDisplayWidth) break; // Copy valid multibyte sequence memcpy(o, p, len); o += len; p += len; widthSum += w; } *o = '\0'; } static bool hasFullwidthChars(const char *str) { mbstate_t state; memset(&state, 0, sizeof(state)); const char *p = str; wchar_t wc; while (*p) { size_t 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, bool stripUnneededChars, bool stripSuffix) { if (!name) { output[0] = '\0'; return; } const char *lastDot = strrchr(name, '.'); if (lastDot != NULL && stripSuffix) { char tmp[MAXPATHLEN]; size_t len = lastDot - name + 1; if (len >= sizeof(tmp)) len = sizeof(tmp) - 1; c_strcpy(tmp, name, len); tmp[len] = '\0'; copyHalfOrFullWidthCharsWithMaxWidth(tmp, output, maxWidth); } else { copyHalfOrFullWidthCharsWithMaxWidth(name, output, maxWidth); } if (stripUnneededChars) removeUnneededChars(output, strnlen(output, maxWidth)); trim(output, strlen(output)); } void processNameScroll(const char *name, char *output, int maxWidth, bool isSameNameAsLastTime) { size_t scrollableLength = strnlen(name, maxWidth); size_t nameLength = strlen(name); if (scrollDelaySkippedCount <= startScrollingDelay && nameLength > (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, true, true); } else if (nameLength <= (size_t)maxWidth || finishedScrolling) { processName(name, output, scrollableLength, true, true); } else { isLongName = true; if ((size_t)(start + maxWidth) > nameLength) { start = 0; finishedScrolling = true; } c_strcpy(output, name + start, maxWidth + 1); removeUnneededChars(output, maxWidth); trim(output, maxWidth); lastNamePosition++; refresh = true; } } bool getIsLongName() { return isLongName; } 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; } PixelData decreaseLuminosityPct(PixelData base, float pct) { PixelData c; int r = (int)((float)base.r * pct); int g = (int)((float)base.g * pct); int b = (int)((float)base.b * pct); c.r = (r < MIN_CHANNEL) ? MIN_CHANNEL : r; c.g = (g < MIN_CHANNEL) ? MIN_CHANNEL : g; c.b = (b < MIN_CHANNEL) ? MIN_CHANNEL : b; return c; } PixelData getGradientColor(PixelData baseColor, int row, int maxListSize, int startGradient, float minPct) { if (row < startGradient) return baseColor; int steps = maxListSize - startGradient; float pct; if (steps <= 1) pct = minPct; else pct = 1.0f - ((row - startGradient) * (1.0f - minPct) / (steps - 1)); if (pct < minPct) pct = minPct; if (pct > 1.0f) pct = 1.0f; return decreaseLuminosityPct(baseColor, pct); } kew/src/common_ui.h000066400000000000000000000015471507107350600146040ustar00rootroot00000000000000#ifndef COMMON_UI_H #define COMMON_UI_H #include #include "appstate.h" extern unsigned int updateCounter; extern const int scrollingInterval; extern bool isSameNameAsLastTime; void setRGB(PixelData p); void setAlbumColor(PixelData color); void inverseText(void); void applyColor(ColorMode mode, ColorValue themeColor, PixelData albumColor); void processNameScroll(const char *name, char *output, int maxWidth, bool isSameNameAsLastTime); void resetNameScroll(); void resetColor(); bool getIsLongName(); void processName(const char *name, char *output, int maxWidth, bool stripUnneededChars, bool stripSuffix); PixelData increaseLuminosity(PixelData pixel, int amount); PixelData decreaseLuminosityPct(PixelData base, float pct); PixelData getGradientColor(PixelData baseColor, int row, int maxListSize, int startGradient, float minPct); #endif kew/src/directorytree.c000066400000000000000000000752371507107350600155050ustar00rootroot00000000000000#include "directorytree.h" #include "file.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include /* directorytree.c Related to library / directory structure. */ #define MAX_STACK_SIZE 1000000 #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif static int lastUsedId = 0; typedef void (*TimeoutCallback)(void); FileSystemEntry *createEntry(const char *name, int isDirectory, FileSystemEntry *parent) { if (lastUsedId == INT_MAX) return NULL; int newId = lastUsedId + 1; if (newId < 0) // overflow detected return NULL; FileSystemEntry *newEntry = malloc(sizeof(FileSystemEntry)); if (newEntry != NULL) { newEntry->name = strdup(name); if (newEntry->name == NULL) { fprintf(stderr, "createEntry: name is null\n"); free(newEntry); return NULL; } newEntry->isDirectory = isDirectory; newEntry->isEnqueued = 0; newEntry->parent = parent; newEntry->children = NULL; newEntry->next = NULL; newEntry->id = newId; 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; } } #ifndef MAX_NAME #define MAX_NAME 255 #endif int isValidEntryName(const char *name) { if (name == NULL) return 0; size_t len = strnlen(name, MAXPATHLEN + 1); if (len > MAX_NAME || len > MAXPATHLEN) return 0; if (len == 0) return 1; // Reject "." and ".." exactly if (len == 1 && name[0] == '.') return 0; if (len == 2 && name[0] == '.' && name[1] == '.') return 0; for (size_t i = 0; i < len; ++i) { unsigned char c = (unsigned char)name[i]; // Reject path separators if (c == '/' || c == '\\') return 0; // Reject ASCII control chars and DEL if (c <= 0x1F || c == 0x7F) return 0; } return 1; } void setFullPath(FileSystemEntry *entry, const char *parentPath, const char *entryName) { if (entry == NULL || parentPath == NULL || entryName == NULL) return; if (!isValidEntryName(entryName)) { char buf[257]; snprintf(buf, sizeof(buf), "%s", entryName); buf[sizeof(buf) - 1] = '\0'; // ensure null-termination fprintf(stderr, "Invalid entryName (possible path traversal): '%s'\n", buf); return; } size_t parentLen = strnlen(parentPath, MAXPATHLEN + 1); size_t nameLen = strnlen(entryName, MAXPATHLEN + 1); if (parentLen > MAXPATHLEN || nameLen > MAXPATHLEN) { fprintf( stderr, "Parent or entry name too long or not null-terminated.\n"); return; } if (parentLen > 0 && parentPath[parentLen - 1] == '/') parentLen--; // Normalize parent path (remove trailing slash) size_t needed = parentLen + 1 + nameLen + 1; // slash + null if (needed > MAXPATHLEN) { fprintf(stderr, "Path too long, rejecting.\n"); return; } entry->fullPath = malloc(needed); if (entry->fullPath == NULL) return; snprintf(entry->fullPath, needed, "%.*s/%s", (int)parentLen, parentPath, entryName); entry->fullPath[needed - 1] = '\0'; // Explicit null-termination // Post-check for directory traversal patterns if (strstr(entry->fullPath, "/../") != NULL || strstr(entry->fullPath, "/..") == entry->fullPath + strlen(entry->fullPath) - 3 || strncmp(entry->fullPath, "../", 3) == 0) { fprintf(stderr, "Path traversal attempt detected in fullPath: '%s'\n", entry->fullPath); free(entry->fullPath); entry->fullPath = NULL; return; } } void freeTree(FileSystemEntry *root) { if (root == NULL) return; size_t cap = 128, top = 0; FileSystemEntry **stack = malloc(cap * sizeof(*stack)); if (!stack) return; stack[top++] = root; while (top > 0) { FileSystemEntry *node = stack[top - 1]; if (node->children) { if (top + 2 > cap) { if (cap > INT_MAX / 2 || cap * 2 > MAX_STACK_SIZE) { fprintf(stderr, "Stack capacity limit exceeded " "in freeTree\n"); break; } size_t nc = cap * 2; FileSystemEntry **tmp = realloc(stack, nc * sizeof(*stack)); if (!tmp) break; stack = tmp; cap = nc; } if (node->next) stack[top++] = node->next; stack[top++] = node->children; node->children = NULL; node->next = NULL; continue; } top--; free(node->name); free(node->fullPath); free(node); } free(stack); } int naturalCompare(const char *a, const char *b) { while (*a && *b) { if (*a >= '0' && *a <= '9' && *b >= '0' && *b <= '9') { // Parse number sequences char *endA, *endB; errno = 0; unsigned long long numA = strtoull(a, &endA, 10); int overflowA = (errno == ERANGE); errno = 0; unsigned long long numB = strtoull(b, &endB, 10); int overflowB = (errno == ERANGE); if (!overflowA && !overflowB) { if (numA < numB) return -1; if (numA > numB) return 1; } else { // Fallback: compare digit length, then // lexicographically size_t lenA = endA - a; size_t lenB = endB - b; if (lenA < lenB) return -1; if (lenA > lenB) return 1; int cmp = strncmp(a, b, lenA); if (cmp != 0) return cmp; } // Numbers equal, advance a = endA; b = endB; } else { if (*a != *b) return (unsigned char)*a - (unsigned char)*b; a++; b++; } } if (*a == 0 && *b == 0) return 0; if (*a == 0) return -1; return 1; } 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; 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); } #define MAX_RECURSION_DEPTH 1024 int removeEmptyDirectories(FileSystemEntry *node, int depth) { if (node == NULL || depth > MAX_RECURSION_DEPTH) return 0; FileSystemEntry *currentChild = node->children; FileSystemEntry *prevChild = NULL; int numEntries = 0; while (currentChild != NULL) { if (currentChild->isDirectory) { numEntries += removeEmptyDirectories(currentChild, depth + 1); 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) { struct dirent **entries; int dirEntries = scandir(path, &entries, NULL, compareLibEntriesReversed); if (dirEntries < 0) { 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 == NULL) { continue; } 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 isDir = true; if (S_ISREG(fileStats.st_mode)) { isDir = false; } char exto[100]; extractExtension(entry->d_name, sizeof(exto) - 1, exto); int isAudio = match_regex(®ex, exto); if (isAudio == 0 || isDir) { FileSystemEntry *child = createEntry(entry->d_name, isDir, parent); if (child == NULL) continue; setFullPath(child, path, entry->d_name); if (child->fullPath == NULL) continue; addChild(parent, child); if (isDir) { numEntries++; numEntries += readDirectory(childPath, child); } } } free(entry); } free(entries); regfree(®ex); 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, 0); 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; } int countLinesAndMaxId(const char *filename, int *maxId_out) { FILE *file = fopen(filename, "r"); if (!file) return -1; char line[1024]; int lines = 0, maxId = -1; while (fgets(line, sizeof(line), file)) { int id; if (sscanf(line, "%d", &id) == 1) { if (id > maxId) maxId = id; } lines++; } fclose(file); if (maxId_out) *maxId_out = maxId; return lines; } FileSystemEntry *reconstructTreeFromFile(const char *filename, const char *startMusicPath, int *numDirectoryEntries) { int maxId = -1; int nodeCount = countLinesAndMaxId(filename, &maxId); if (nodeCount <= 0 || maxId < 0) return NULL; FILE *file = fopen(filename, "r"); if (!file) return NULL; // Allocate memory for maxid + 1 nodes FileSystemEntry **nodes = calloc((size_t)(maxId + 1), sizeof(FileSystemEntry *)); if (!nodes) { fclose(file); return NULL; } char line[1024]; FileSystemEntry *root = NULL; if (numDirectoryEntries) *numDirectoryEntries = 0; while (fgets(line, sizeof(line), file)) { int id, parentId, isDir; char name[256]; if (sscanf(line, "%d\t%255[^\t]\t%d\t%d", &id, name, &isDir, &parentId) != 4) continue; FileSystemEntry *node = malloc(sizeof(FileSystemEntry)); node->id = id; node->name = strdup(name); if (node->name == NULL) { fprintf(stderr, "reconstructTreeFromFile:name is null\n"); free(node); continue; } node->isDirectory = isDir; node->isEnqueued = 0; node->parentId = parentId; node->parent = NULL; node->children = NULL; node->next = NULL; node->lastChild = NULL; nodes[id] = node; if (parentId >= 0 && parentId <= maxId && nodes[parentId]) { node->parent = nodes[parentId]; FileSystemEntry *parent = nodes[parentId]; if (!parent->children) { parent->children = parent->lastChild = node; } else { parent->lastChild->next = node; parent->lastChild = node; } // fullPath = parent/fullName size_t plen = strlen(parent->fullPath); size_t nlen = strlen(name); node->fullPath = malloc(plen + 1 + nlen + 1); memcpy(node->fullPath, parent->fullPath, plen); node->fullPath[plen] = '/'; memcpy(node->fullPath + plen + 1, name, nlen); node->fullPath[plen + 1 + nlen] = 0; if (isDir && numDirectoryEntries) (*numDirectoryEntries)++; } else { node->parent = NULL; node->fullPath = strdup(startMusicPath); if (node->fullPath == NULL) { fprintf(stderr, "reconstructTreeFromFiley: " "fullPath is null\n"); free(node); continue; } root = node; } } 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 = malloc((len2 + 1) * sizeof(int)); int *currRow = malloc((len2 + 1) * sizeof(int)); if (prevRow == NULL) { if (currRow != NULL) free(currRow); perror("malloc"); return 0; } if (currRow == NULL) { free(prevRow); perror("malloc"); return 0; } // 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; } // Helper function to normalize and remove accents char* normalizeString(const char *str) { // First normalize to NFD (decomposed form) which separates base chars from accents char *normalized = g_utf8_normalize(str, -1, G_NORMALIZE_NFD); if (!normalized) return g_utf8_strdown(str, -1); // Then remove combining diacritical marks (accents) GString *result = g_string_new(""); for (const char *p = normalized; *p; p = g_utf8_next_char(p)) { gunichar c = g_utf8_get_char(p); GUnicodeType type = g_unichar_type(c); // Skip combining marks (accents, diacritics) if (type != G_UNICODE_NON_SPACING_MARK && type != G_UNICODE_SPACING_MARK && type != G_UNICODE_ENCLOSING_MARK) { g_string_append_unichar(result, g_unichar_tolower(c)); } } // FIXME: This doesn't seem to work! g_free(normalized); return g_string_free(result, FALSE); } int calculateSearchDistance(const char *needle, const char *haystack, int isDirectory) { // Convert to lowercase for case-insensitive matching char *needleLower = normalizeString(needle); char *haystackLower = normalizeString(haystack); int distance; // Check for exact match (distance 0) if (strcmp(haystackLower, needleLower) == 0) { distance = 0; } // Check for substring match (low distance based on extra chars) else if (strstr(haystackLower, needleLower) != NULL) { // Substring match: distance = extra characters int needleLen = g_utf8_strlen(needleLower, -1); int haystackLen = g_utf8_strlen(haystackLower, -1); distance = haystackLen - needleLen; } // Check if haystack starts with needle (prefix match) else if (g_str_has_prefix(haystackLower, needleLower)) { int needleLen = g_utf8_strlen(needleLower, -1); int haystackLen = g_utf8_strlen(haystackLower, -1); distance = haystackLen - needleLen; } // No substring match: use Levenshtein but add penalty else { int levenshtein = utf8_levenshteinDistance(needleLower, haystackLower); // Add large penalty to ensure substring matches rank higher int needleLen = g_utf8_strlen(needleLower, -1); distance = needleLen + levenshtein + 100; } // Add penalty for files (non-directories) to prioritize albums if (!isDirectory) { distance += 50; } g_free(needleLower); g_free(haystackLower); return distance; } #ifdef __GNUC__ #ifndef __APPLE__ #pragma GCC diagnostic pop #endif #endif char *stripFileExtension(const char *filename) { if (filename == NULL) return NULL; const char *dot = strrchr(filename, '.'); // Don't treat a leading '.' as an extension (e.g. ".bashrc") if (dot == NULL || dot == filename) dot = filename + strlen(filename); size_t length = (size_t)(dot - filename); char *result = malloc(length + 1); if (!result) { perror("malloc"); return NULL; } memcpy(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(searchTerm, -1); char *lowerName = g_utf8_casefold(node->name, -1); int nameDistance = calculateSearchDistance(lowerName, lowerSearchTerm, node->isDirectory); // Partial matching with lowercase strings if (strstr(lowerName, lowerSearchTerm) != NULL) { callback(node, nameDistance); } else if (nameDistance <= threshold) { callback(node, nameDistance); } // Free the allocated memory for lowercase strings g_free(lowerSearchTerm); g_free(lowerName); 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 *)); if (entryArray == NULL) { perror("malloc"); return; } 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/src/directorytree.h000066400000000000000000000037461507107350600155060ustar00rootroot00000000000000#ifndef DIRECTORYTREE_H #define DIRECTORYTREE_H #include #include #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) struct FileSystemEntry *lastChild; // TEMP: only for construction } 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); FileSystemEntry *findCorrespondingEntry(FileSystemEntry *tmp, const char *fullPath); #endif kew/src/events.h000066400000000000000000000027141507107350600141200ustar00rootroot00000000000000#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_ADDTOFAVORITESPLAYLIST, EVENT_DELETEFROMMAINPLAYLIST, EVENT_EXPORTPLAYLIST, EVENT_UPDATELIBRARY, EVENT_SHUFFLE, EVENT_KEY_PRESS, EVENT_SHOWKEYBINDINGS, EVENT_SHOWPLAYLIST, EVENT_SHOWSEARCH, EVENT_GOTOSONG, EVENT_GOTOBEGINNINGOFPLAYLIST, EVENT_GOTOENDOFPLAYLIST, EVENT_CYCLECOLORMODE, 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_MOVESONGUP, EVENT_MOVESONGDOWN, EVENT_ENQUEUEANDPLAY, EVENT_STOP, EVENT_SORTLIBRARY, EVENT_CYCLETHEMES, EVENT_TOGGLENOTIFICATIONS }; 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/src/file.c000066400000000000000000000254641507107350600135350ustar00rootroot00000000000000#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "file.h" #include "utils.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. */ #define MAX_RECURSION_DEPTH 64 void getDirectoryFromPath(const char *path, char *directory) { if (!path || !directory) return; size_t len = strnlen(path, MAXPATHLEN); char *tmp = malloc(len + 1); if (!tmp) { fprintf(stderr, "Out of memory while processing path\n"); return; } memcpy(tmp, path, len + 1); // dirname() may modify the buffer, so we keep it in tmp char *dir = dirname(tmp); // Copy the result to the caller‑supplied buffer safely strncpy(directory, dir, MAXPATHLEN - 1); directory[MAXPATHLEN - 1] = '\0'; // Ensure null termination /// Ensure a trailing '/' size_t dlen = strnlen(directory, MAXPATHLEN); if (dlen > 0 && directory[dlen - 1] != '/' && dlen + 1 < MAXPATHLEN) { directory[dlen] = '/'; directory[dlen + 1] = '\0'; } free(tmp); } int existsFile(const char *fname) { if (fname == NULL || fname[0] == '\0') return -1; 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, int depth) { if (depth > MAX_RECURSION_DEPTH) { fprintf(stderr, "Maximum recursion depth exceeded\n"); return 1; } if (!startPath || !lowCaseSearching || !result || !allowedExtensions) { fprintf(stderr, "Invalid arguments to walker\n"); return 1; } struct stat path_stat; if (stat(startPath, &path_stat) != 0) { fprintf(stderr, "Cannot stat path '%s': %s\n", startPath, strerror(errno)); return 1; } if (!S_ISDIR(path_stat.st_mode)) { // Not a directory, stop here return 1; } DIR *d = opendir(startPath); if (!d) { fprintf(stderr, "Failed to open directory '%s': %s\n", startPath, strerror(errno)); return 1; } regex_t regex; if (regcomp(®ex, allowedExtensions, REG_EXTENDED) != 0) { fprintf(stderr, "Failed to compile regex\n"); closedir(d); return 1; } bool found = false; struct dirent *entry; char ext[100] = {0}; while ((entry = readdir(d)) != NULL) { // Skip . and .. if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; // Build full path for entry char entryPath[PATH_MAX]; if (snprintf(entryPath, sizeof(entryPath), "%s/%s", startPath, entry->d_name) >= (int)sizeof(entryPath)) { fprintf(stderr, "Path too long: %s/%s\n", startPath, entry->d_name); continue; } if (stat(entryPath, &path_stat) != 0) { // Can't stat, skip continue; } if (S_ISDIR(path_stat.st_mode)) { // Directory handling char *foldedName = g_utf8_casefold(entry->d_name, -1); if (!foldedName) continue; bool nameMatch = exactSearch ? (strcasecmp(foldedName, lowCaseSearching) == 0) : (c_strcasestr(foldedName, lowCaseSearching, PATH_MAX) != NULL); free(foldedName); if (nameMatch && searchType != FileOnly && searchType != SearchPlayList) { strncpy(result, entryPath, PATH_MAX - 1); result[PATH_MAX - 1] = '\0'; found = true; break; } // Recurse into subdirectory if (walker(entryPath, lowCaseSearching, result, allowedExtensions, searchType, exactSearch, depth + 1) == 0) { found = true; break; } } else { // File handling if (searchType == DirOnly) continue; if (strlen(entry->d_name) <= 4) continue; extractExtension(entry->d_name, sizeof(ext) - 1, ext); if (match_regex(®ex, ext) != 0) continue; char *foldedName = g_utf8_casefold(entry->d_name, -1); if (!foldedName) continue; bool nameMatch = exactSearch ? (strcasecmp(foldedName, lowCaseSearching) == 0) : (c_strcasestr(foldedName, lowCaseSearching, PATH_MAX) != NULL); free(foldedName); if (nameMatch) { strncpy(result, entryPath, PATH_MAX - 1); result[PATH_MAX - 1] = '\0'; found = true; break; } } } regfree(®ex); closedir(d); return found ? 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) { const 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; const 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"); static char tmpdirBuf[PATH_MAX + 2]; if (tmpDir == NULL || strnlen(tmpDir, PATH_MAX) >= PATH_MAX) tmpDir = "/tmp"; size_t len = strlen(tmpDir); strncpy(tmpdirBuf, tmpDir, PATH_MAX); tmpdirBuf[PATH_MAX] = '\0'; if (len == 0 || tmpdirBuf[len - 1] != '/') { tmpdirBuf[len] = '/'; tmpdirBuf[len + 1] = '\0'; } return pathStartsWith(path, tmpdirBuf); } 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/src/file.h000066400000000000000000000021471507107350600135330ustar00rootroot00000000000000#ifndef FILE_H #define FILE_H #include #define __USE_GNU #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef MUSIC_FILE_EXTENSIONS #define MUSIC_FILE_EXTENSIONS "(m4a|aac|mp3|ogg|flac|wav|opus|webm)$" #endif #ifndef AUDIO_EXTENSIONS #define AUDIO_EXTENSIONS "(m4a|aac|mp3|ogg|flac|wav|opus|webm|m3u|m3u8)$" #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 depth); 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/src/imgfunc.c000066400000000000000000001015631507107350600142410ustar00rootroot00000000000000#include #include #include #include #include #include "common.h" #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; #ifdef CHAFA_VERSION_1_16 static gchar *tmux_allow_passthrough_original; static gboolean tmux_allow_passthrough_is_changed; static gboolean apply_passthrough_workarounds_tmux(void) { gboolean result = FALSE; gchar *standard_output = NULL; gchar *standard_error = NULL; gchar **argv = NULL; gint wait_status = -1; gchar *mode = NULL; /* Use g_spawn_sync with explicit argv to avoid shell injection */ argv = g_new0(gchar*, 4); argv[0] = g_strdup("tmux"); argv[1] = g_strdup("show"); argv[2] = g_strdup("allow-passthrough"); argv[3] = NULL; if (!g_spawn_sync(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &standard_output, &standard_error, &wait_status, NULL)) { g_strfreev(argv); goto out; } g_strfreev(argv); /* Parse output safely */ if (standard_output && *standard_output) { gchar **lines = g_strsplit(standard_output, "\n", 2); if (lines[0]) { gchar **parts = g_strsplit(lines[0], " ", 3); if (parts[0] && parts[1]) { mode = g_ascii_strdown(parts[1], -1); g_strstrip(mode); } g_strfreev(parts); } g_strfreev(lines); } if (!mode || (strcmp(mode, "on") && strcmp(mode, "all"))) { argv = g_new0(gchar*, 4); argv[0] = g_strdup("tmux"); argv[1] = g_strdup("set-option"); argv[2] = g_strdup("allow-passthrough on"); argv[3] = NULL; result = g_spawn_sync(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &standard_output, &standard_error, &wait_status, NULL); g_strfreev(argv); if (result) { tmux_allow_passthrough_original = mode; tmux_allow_passthrough_is_changed = TRUE; } } else { g_free(mode); } out: g_free(standard_output); g_free(standard_error); g_free(mode); return result; } gboolean retire_passthrough_workarounds_tmux(void) { gboolean result = FALSE; gchar *standard_output = NULL; gchar *standard_error = NULL; gint wait_status = -1; gchar **argv = NULL; if (!tmux_allow_passthrough_is_changed) return TRUE; if (tmux_allow_passthrough_original) { // Use argument array to avoid shell injection argv = g_new0(gchar *, 5); argv[0] = g_strdup("tmux"); argv[1] = g_strdup("set-option"); argv[2] = g_strdup("allow-passthrough"); argv[3] = g_strdup(tmux_allow_passthrough_original); argv[4] = NULL; } else { // Use argument array for unsetting the option argv = g_new0(gchar *, 4); argv[0] = g_strdup("tmux"); argv[1] = g_strdup("set-option"); argv[2] = g_strdup("-u"); argv[3] = g_strdup("allow-passthrough"); } result = g_spawn_sync( NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &standard_output, &standard_error, &wait_status, NULL); // Free the argument array for (int i = 0; argv[i] != NULL; i++) g_free(argv[i]); g_free(argv); if (result) { g_free(tmux_allow_passthrough_original); tmux_allow_passthrough_original = NULL; tmux_allow_passthrough_is_changed = FALSE; } g_free(standard_output); g_free(standard_error); return result; } static void detect_terminal(ChafaTermInfo **term_info_out, ChafaCanvasMode *mode_out, ChafaPixelMode *pixel_mode_out, ChafaPassthrough *passthrough_out, ChafaSymbolMap **symbol_map_out) { ChafaCanvasMode mode; ChafaPixelMode pixel_mode; ChafaPassthrough passthrough; 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); /* Pick the most high-quality rendering possible */ mode = chafa_term_info_get_best_canvas_mode(term_info); pixel_mode = chafa_term_info_get_best_pixel_mode(term_info); passthrough = chafa_term_info_get_is_pixel_passthrough_needed(term_info, pixel_mode) ? chafa_term_info_get_passthrough_type(term_info) : CHAFA_PASSTHROUGH_NONE; const gchar *term_name = chafa_term_info_get_name(term_info); if (strstr(term_name, "tmux") != NULL && pixel_mode != CHAFA_PIXEL_MODE_KITTY) { /* Always use sixels in tmux */ pixel_mode = CHAFA_PIXEL_MODE_SIXELS; mode = CHAFA_CANVAS_MODE_TRUECOLOR; } *symbol_map_out = chafa_symbol_map_new(); chafa_symbol_map_add_by_tags(*symbol_map_out, chafa_term_info_get_safe_symbol_tags(term_info)); /* Hand over the information to caller */ *term_info_out = term_info; *mode_out = mode; *pixel_mode_out = pixel_mode; *passthrough_out = passthrough; /* Cleanup */ g_strfreev(envp); } #else 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); } #endif 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; #ifdef CHAFA_VERSION_1_16 ChafaPassthrough passthrough; ChafaFrame *frame; ChafaImage *image; ChafaPlacement *placement; detect_terminal(&term_info, &mode, &pixel_mode, &passthrough, &symbol_map); if (passthrough == CHAFA_PASSTHROUGH_TMUX) apply_passthrough_workarounds_tmux(); 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); 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); } chafa_canvas_config_set_passthrough(config, passthrough); chafa_canvas_config_set_symbol_map(config, symbol_map); canvas = chafa_canvas_new(config); frame = chafa_frame_new_borrow((gpointer)pixels, pixel_type, pix_width, pix_height, pix_rowstride); image = chafa_image_new(); chafa_image_set_frame(image, frame); placement = chafa_placement_new(image, 1); chafa_placement_set_tuck(placement, CHAFA_TUCK_STRETCH); chafa_placement_set_halign(placement, CHAFA_ALIGN_START); chafa_placement_set_valign(placement, CHAFA_ALIGN_START); chafa_canvas_set_placement(canvas, placement); printable = chafa_canvas_print(canvas, NULL); /* Clean up and return */ chafa_placement_unref(placement); chafa_image_unref(image); chafa_frame_unref(frame); 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; #else 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; #endif } // 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; } float getAspectRatio() { 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 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 return (float)cell_height / (float)cell_width; } void printSquareBitmap(int row, int col, unsigned char *pixels, int width, int height, int baseHeight) { if (pixels == NULL) { setErrorMessage("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) { setErrorMessage("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; } if (cell_width == 0 || cell_height == 0) { setErrorMessage("Invalid image cell width dimensions.\n"); return; } // 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); if (term_size.width_cells > 0 && correctedWidth > term_size.width_cells) { setErrorMessage("Invalid terminal dimensions.\n"); return; } if (term_size.height_cells > 0 && baseHeight > term_size.height_cells) { setErrorMessage("Invalid terminal dimensions.\n"); return; } // 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); // Print each line with indentation for (int i = 0; lines[i] != NULL; i++) { printf("\033[%d;%dH", row + i, col); printf("%s", lines[i]); fflush(stdout); } // Free allocated memory g_strfreev(lines); g_string_free(printable, TRUE); } void printSquareBitmapCentered(unsigned char *pixels, int width, int height, int baseHeight) { if (pixels == NULL) { setErrorMessage("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) { setErrorMessage("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; } if (cell_width == 0 || cell_height == 0) { setErrorMessage("Invalid image cell width dimensions.\n"); return; } // 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); if (term_size.width_cells > 0 && correctedWidth > term_size.width_cells) { setErrorMessage("Invalid terminal dimensions.\n"); return; } if (term_size.height_cells > 0 && baseHeight > term_size.height_cells) { setErrorMessage("Invalid terminal dimensions.\n"); return; } // Calculate indentation to center the image int indentation = ((term_size.width_cells - correctedWidth) / 2); // 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); // 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 convertToAsciiCentered(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; } // Set default cell size for some terminals if (cell_width == -1 || cell_height == -1) { cell_width = 8; cell_height = 16; } float aspect_ratio_correction = (float)cell_height / (float)cell_width; 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 convertToAscii(int indentation, 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; } // Set default cell size for some terminals if (cell_width == -1 || cell_height == -1) { cell_width = 8; cell_height = 16; } float aspect_ratio_correction = (float)cell_height / (float)cell_width; unsigned int correctedWidth = (int)(height * aspect_ratio_correction) - 1; 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", indentation, ""); for (unsigned int d = 0; d < correctedWidth * height; d++) { if (d % correctedWidth == 0 && d != 0) { printf("\n"); printf("%*s", indentation, ""); } 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(int indentation, const char *pathToImgFile, int height) { printf("\r"); int ret = convertToAscii(indentation, pathToImgFile, (unsigned)height); if (ret == -1) printf("\033[0m"); return 0; } int printInAsciiCentered(const char *pathToImgFile, int height) { printf("\r"); int ret = convertToAsciiCentered(pathToImgFile, (unsigned)height); if (ret == -1) printf("\033[0m"); return 0; } kew/src/imgfunc.h000066400000000000000000000016511507107350600142430ustar00rootroot00000000000000#ifndef IMGFUNC_H #define IMGFUNC_H #include #include #ifndef PIXELDATA_STRUCT #define PIXELDATA_STRUCT typedef struct { unsigned char r; unsigned char g; unsigned char b; } PixelData; #endif int printInAsciiCentered(const char *pathToImgFile, int height); int printInAscii(int indentation, 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); void printSquareBitmap(int row, int col, 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); float getAspectRatio(); #ifdef CHAFA_VERSION_1_16 gboolean retire_passthrough_workarounds_tmux(void); #endif #endif kew/src/kew.c000066400000000000000000002113041507107350600133720ustar00rootroot00000000000000/* kew - A terminal music player Copyright (C) 2022 Ravachol http://codeberg.org/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 "appstate.h" #include "cache.h" #include "common_ui.h" #include "events.h" #include "file.h" #include "imgfunc.h" #include "mpris.h" #include "notifications.h" #include "player_ui.h" #include "playerops.h" #include "playlist.h" #include "search_ui.h" #include "settings.h" #include "songloader.h" #include "sound.h" #include "soundcommon.h" #include "term.h" #include "theme.h" #include "utils.h" #include "visuals.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MAX_TMP_SEQ_LEN 256 // Maximum length of temporary sequence buffer #define COOLDOWN_MS 500 #define COOLDOWN2_MS 100 #define TMPPIDFILE "/tmp/kew_" #ifndef PREFIX #define PREFIX "/usr/local" // Fallback if not set in the makefile #endif 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 = 100; 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; } enum EventType getMouseLastRowEvent(int mouseXOnLastRow) { enum EventType result = EVENT_NONE; size_t lastRowLen = strlen(LAST_ROW); if (mouseXOnLastRow < 0 || (size_t)mouseXOnLastRow > lastRowLen) { // Out of bounds, return default return EVENT_NONE; } int viewClicked = 1; for (int i = 0; i < mouseXOnLastRow; i++) { if (LAST_ROW[i] == '|') { viewClicked++; } } switch (viewClicked) { case 1: result = EVENT_SHOWPLAYLIST; break; case 2: result = EVENT_SHOWLIBRARY; break; case 3: result = EVENT_SHOWTRACK; break; case 4: result = EVENT_SHOWSEARCH; break; case 5: result = EVENT_SHOWKEYBINDINGS; break; default: result = EVENT_NONE; break; } // Switch to library view if track view is clicked and no song is // currently playing if (result == EVENT_SHOWTRACK && getCurrentSongData() == NULL) { result = EVENT_SHOWLIBRARY; } return result; } bool mouseInputHandled(char *seq, int i, struct Event *event) { if (!seq || !event) return false; if (i < 0 || i >= NUM_KEY_MAPPINGS || keyMappings[i].seq == NULL) return false; const char *expected = keyMappings[i].seq; char tmpSeq[MAX_SEQ_LEN]; size_t src_len = strnlen(seq, MAX_SEQ_LEN - 1); if (src_len < 4) // Must be at least ESC[ M + 3 digits return false; snprintf(tmpSeq, sizeof tmpSeq, "%.*s", (int)src_len, seq); int mouseButton = 0, mouseX = 0, mouseY = 0; const char *end = tmpSeq + src_len; const char *p = tmpSeq + 3; // Skip ESC[ M for (int field = 0; field < 3 && p && *p && p < end; ++field) { char *endptr; long val = strtol(p, &endptr, 10); if (endptr == p || endptr > end) // no progress or out of bounds break; p = endptr; if (*p == ';') ++p; switch (field) { case 0: mouseButton = (int)val; break; case 1: mouseX = (int)val; break; case 2: mouseY = (int)val; break; } } if (progressBarLength > 0) { long long deltaCol = (long long)mouseX - (long long)progressBarCol; if (deltaCol >= 0 && deltaCol <= (long long)progressBarLength) { double position = (double)deltaCol / (double)progressBarLength; double duration = getCurrentSongDuration(); draggedPositionSeconds = duration * position; } else { draggedPositionSeconds = 0.0; } } else { draggedPositionSeconds = 0.0; } if (mouseY == lastRowRow && lastRowCol > 0 && mouseX - lastRowCol > 0 && mouseX - lastRowCol < (int)strlen(LAST_ROW) && mouseButton != MOUSE_DRAG) { event->type = getMouseLastRowEvent(mouseX - lastRowCol); return true; } if ((mouseY == progressBarRow || draggingProgressBar) && mouseX - progressBarCol >= 0 && mouseX - progressBarCol < progressBarLength && appState.currentView == TRACK_VIEW) { if (mouseButton == MOUSE_DRAG || mouseButton == MOUSE_CLICK) { draggingProgressBar = true; gint64 newPosUs = (gint64)(draggedPositionSeconds * G_USEC_PER_SEC); setPosition(newPosUs); } return true; } size_t expected_len = strlen(expected); if (strlen(seq) < expected_len + 1) return false; if (strncmp(seq + 1, expected, expected_len) == 0) { event->type = keyMappings[i].eventType; return true; } return false; } struct Event processInput(UISettings *ui) { 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.nextPage) == 0 || strcmp(seq + 1, settings.prevPage) == 0) { keyReleased = 0; readInputSequence( tmpSeq, sizeof(tmpSeq)); // Dummy read to prevent scrolling // after key released break; } keyReleased = 0; } if (!foundInput && cooldownElapsed) { if (!draggingProgressBar) 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, ui); resetSearchResult(); fuzzySearch(getLibrary(), fuzzySearchThreshold); event.type = EVENT_SEARCH; } } if (seq[0] == 127) { seq[0] = '\b'; // Treat as Backspace } bool handledMouse = false; // 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; } event.type = keyMappings[i].eventType; break; } // Received mouse input instead of keyboard input if (keyMappings[i].seq[0] != '\0' && strncmp(seq, "\033[<", 3) == 0 && strnlen(seq, MAX_SEQ_LEN) > 4 && strchr(seq, 'M') != NULL && mouseInputHandled(seq, i, &event)) { handledMouse = true; break; } } if (!handledMouse) { // Stop dragging progress bar draggingProgressBar = false; } 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; waitingForNext = 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; clearScreen(); } } 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) { startFromTop = true; 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; 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 handleGoToSong(AppState *state) { bool canGoNext = (currentSong != NULL && currentSong->next != NULL); if (state->currentView == LIBRARY_VIEW) { FileSystemEntry *entry = getCurrentLibEntry(); if (entry == NULL) return; // Enqueue playlist if (pathEndsWith(entry->fullPath, "m3u") || pathEndsWith(entry->fullPath, "m3u8")) { FileSystemEntry *firstEnqueuedEntry = NULL; Node *prevTail = playlist.tail; readM3UFile(entry->fullPath, &playlist, library); if (prevTail != NULL && prevTail->next != NULL) { firstEnqueuedEntry = findCorrespondingEntry( library, prevTail->next->song.filePath); } else if (playlist.head != NULL) { firstEnqueuedEntry = findCorrespondingEntry( library, playlist.head->song.filePath); } autostartIfStopped(firstEnqueuedEntry); markListAsEnqueued(library, &playlist); deepCopyPlayListOntoList(&playlist, unshuffledPlaylist); } else enqueue(state, entry); // Enqueue song } 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 == PLAYLIST_VIEW) { if (digitsPressedCount == 0) { if (isPaused() && currentSong != NULL && state->uiState.chosenNodeId == currentSong->id) { togglePause(&totalPauseSeconds, &pauseSeconds, &pause_time); } 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) { 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(&(state->uiSettings)); 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: if (isStopped()) { handleGoToSong(state); } else { togglePause(&totalPauseSeconds, &pauseSeconds, &pause_time); } break; case EVENT_TOGGLEVISUALIZER: toggleVisualizer(&settings, &(state->uiSettings)); break; case EVENT_TOGGLEREPEAT: toggleRepeat(&(state->uiSettings)); break; case EVENT_TOGGLEASCII: toggleAscii(&settings, &(state->uiSettings)); break; case EVENT_SHUFFLE: toggleShuffle(&(state->uiSettings)); emitShuffleChanged(); break; case EVENT_CYCLECOLORMODE: cycleColorMode(&(state->uiSettings)); break; case EVENT_CYCLETHEMES: cycleThemes(&(state->uiSettings), &settings); break; case EVENT_TOGGLENOTIFICATIONS: toggleNotifications(&(state->uiSettings), &settings); 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_ADDTOFAVORITESPLAYLIST: addToFavoritesPlaylist(); break; case EVENT_EXPORTPLAYLIST: exportCurrentPlaylist(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; 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_MOVESONGUP: moveSongUp(); break; case EVENT_MOVESONGDOWN: moveSongDown(); break; case EVENT_ENQUEUEANDPLAY: enqueueAndPlay(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); int res = loadFirst(currentSong, state); finishLoading(); if (res >= 0) { res = createAudioDevice(); } if (res >= 0) { resetClock(); 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)); if (playlist.head != NULL) { if ((skipFromStopped || !loadedNextSong || nextSongNeedsRebuilding) && !audioData.endOfListReached) { loadAudioData(state); } if (songHasErrors) tryLoadNext(); if (isPlaybackDone()) { resetStartTime(); 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 % 2 == 0 && appState.currentView == SEARCH_VIEW) || (appState.currentView == TRACK_VIEW || appState.uiState.miniMode) || updateCounter % 3 == 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(); resetStartTime(); 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; if (!waitingForNext) 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(34, mainloop_callback, NULL); g_main_loop_run(main_loop); g_main_loop_unref(main_loop); } void cleanupOnExit() { pthread_mutex_lock(&dataSourceMutex); resetAllDecoders(); if (isContextInitialized) { #ifdef __ANDROID__ shutdownAndroid(); #else cleanupPlaybackDevice(); #endif 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); } #ifdef CHAFA_VERSION_1_16 retire_passthrough_workarounds_tmux(); #endif freeSearchResults(); cleanupMpris(); restoreTerminalMode(); enableInputBuffering(); setConfig(&settings, &(appState.uiSettings)); saveFavoritesPlaylist(settings.path); saveLastUsedPlaylist(); deleteCache(appState.tmpCache); freeMainDirectoryTree(&appState); deletePlaylist(&playlist); deletePlaylist(unshuffledPlaylist); deletePlaylist(favoritesPlaylist); free(favoritesPlaylist); free(unshuffledPlaylist); setDefaultTextColor(); pthread_mutex_destroy(&(loadingdata.mutex)); pthread_mutex_destroy(&(playlist.mutex)); pthread_mutex_destroy(&(switchMutex)); pthread_mutex_unlock(&dataSourceMutex); pthread_mutex_destroy(&(dataSourceMutex)); freeVisuals(); #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 (appState.uiSettings.trackTitleAsWindowTitle) restoreTerminalWindowTitle(); 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"); } if (hasErrorMessage()) { printf("%s\n", getErrorMessage()); } fflush(stdout); } void run(AppState *state, bool startPlaying) { if (unshuffledPlaylist == NULL) { unshuffledPlaylist = malloc(sizeof(PlayList)); *unshuffledPlaylist = deepCopyPlayList(&playlist); } if (state->uiSettings.saveRepeatShuffleSettings) { if (state->uiSettings.repeatState == 1) toggleRepeat(&(state->uiSettings)); if (state->uiSettings.repeatState == 2) { toggleRepeat(&(state->uiSettings)); toggleRepeat(&(state->uiSettings)); } if (state->uiSettings.shuffleEnabled) toggleShuffle(&(state->uiSettings)); } if (playlist.head == NULL) { state->currentView = LIBRARY_VIEW; } initMpris(); if (startPlaying) currentSong = playlist.head; else if (playlist.count > 0) waitingForNext = true; 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) { disableTerminalLineInput(); setRawInputMode(); 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); createLibrary(&settings, state); setlocale(LC_ALL, ""); 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 initDefaultState(AppState *state) { init(state); loadLastUsedPlaylist(); markListAsEnqueued(library, &playlist); resetListAfterDequeuingPlayingSong(state); audioData.restart = true; audioData.endOfListReached = true; loadedNextSong = false; state->currentView = LIBRARY_VIEW; run(state, false); } void playFavoritesPlaylist(AppState *state) { if (favoritesPlaylist->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(favoritesPlaylist, &playlist); shufflePlaylist(&playlist); markListAsEnqueued(library, &playlist); run(state, true); } void playAll(AppState *state) { init(state); FileSystemEntry *library = getLibrary(); createPlayListFromFileSystemEntry(library, &playlist, MAX_FILES); if (playlist.count == 0) { exit(0); } shufflePlaylist(&playlist); markListAsEnqueued(library, &playlist); run(state, true); } void playAllAlbums(AppState *state) { init(state); FileSystemEntry *library = getLibrary(); addShuffledAlbumsToPlayList(library, &playlist, MAX_FILES); if (playlist.count == 0) { exit(0); } markListAsEnqueued(library, &playlist); run(state, true); } 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); } // Returns 1 if the process is running 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 } const char *getTempDir() { const char *tmpdir = getenv("TMPDIR"); if (tmpdir != NULL) { return tmpdir; // Use TMPDIR if set (common on Android/Termux) } tmpdir = getenv("TEMP"); if (tmpdir != NULL) { return tmpdir; } // Fallback to /tmp on Unix-like systems return "/tmp"; } int isKewProcess(pid_t pid) { char comm_path[64]; char process_name[256]; FILE *file; // First check /proc/[pid]/comm for the process name snprintf(comm_path, sizeof(comm_path), "/proc/%d/comm", pid); file = fopen(comm_path, "r"); if (file != NULL) { if (fgets(process_name, sizeof(process_name), file)) { fclose(file); // Remove trailing newline process_name[strcspn(process_name, "\n")] = 0; // Check if it's kew (process name might be truncated to // 15 chars) if (strstr(process_name, "kew") != NULL) { return 1; // It's likely kew } } else { fclose(file); } } return 0; // Not kew or couldn't determine } // Ensures only a single instance of kew can run at a time for the current user. void exitIfAlreadyRunning() { char pidfile_path[512]; // Increased size for longer paths const char *temp_dir = getTempDir(); snprintf(pidfile_path, sizeof(pidfile_path), "%s/kew_%d.pid", temp_dir, getuid()); FILE *pidfile; pid_t pid; pidfile = fopen(pidfile_path, "r"); if (pidfile != NULL) { if (fscanf(pidfile, "%d", &pid) == 1) { fclose(pidfile); #ifdef __ANDROID__ if (isProcessRunning(pid) && isKewProcess(pid)) #else if (isProcessRunning(pid)) #endif { 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 { choice[0] = 'n'; break; } } } 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 setTrackTitleAsWindowTitle(UISettings *ui) { if (ui->trackTitleAsWindowTitle) { saveTerminalWindowTitle(); setTerminalWindowTitle("kew"); } } 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.coverEnabled = true; state->uiSettings.hideLogo = false; state->uiSettings.hideHelp = false; state->uiSettings.quitAfterStopping = false; state->uiSettings.hideGlimmeringText = false; state->uiSettings.coverAnsi = false; state->uiSettings.visualizerEnabled = true; state->uiSettings.visualizerHeight = 5; state->uiSettings.visualizerColorType = 0; state->uiSettings.visualizerBrailleMode = false; state->uiSettings.visualizerBarWidth = 2; state->uiSettings.titleDelay = 9; state->uiSettings.cacheLibrary = -1; 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.replayGainCheckFirst = 0; state->uiSettings.saveRepeatShuffleSettings = 1; state->uiSettings.repeatState = 0; state->uiSettings.shuffleEnabled = 0; state->uiSettings.trackTitleAsWindowTitle = 1; 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; } void initSettings(AppState *appState, AppSettings *settings) { getConfig(settings, &(appState->uiSettings)); userData.replayGainCheckFirst = appState->uiSettings.replayGainCheckFirst; mapSettingsToKeys(settings, &(appState->uiSettings), keyMappings); enableMouse(&(appState->uiSettings)); setTrackTitleAsWindowTitle(&(appState->uiSettings)); } // Copies default themes to config dir if they aren't alread there bool ensureDefaultThemes(void) { bool copied = false; char *configPath = getConfigPath(); if (!configPath) return false; char themesPath[MAXPATHLEN]; if (snprintf(themesPath, sizeof(themesPath), "%s/themes", configPath) >= (int)sizeof(themesPath)) { free(configPath); return false; } // Check if user themes directory exists struct stat st; if (stat(themesPath, &st) == -1) { mkdir(themesPath, 0755); char *systemThemes = PREFIX "/share/kew/themes"; // Copy themes from systemThemes to themesPath DIR *dir = opendir(systemThemes); if (dir) { struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (entry->d_type == DT_REG && (strstr(entry->d_name, ".theme") || strstr(entry->d_name, ".txt"))) { char src[MAXPATHLEN], dst[MAXPATHLEN]; // Check if paths would be truncated if (snprintf(src, sizeof(src), "%s/%s", systemThemes, entry->d_name) >= (int)sizeof(src)) { continue; // Skip this file if // path too long } if (snprintf(dst, sizeof(dst), "%s/%s", themesPath, entry->d_name) >= (int)sizeof(dst)) { continue; // Skip this file if // path too long } copyFile(src, dst); copied = true; } } closedir(dir); } } free(configPath); return copied; } void initTheme(int argc, char *argv[], UISettings *ui) { bool themeLoaded = false; // Command-line theme handling if (argc > 3 && strcmp(argv[1], "theme") == 0) { setErrorMessage("Couldn't load theme. Theme file names shouldn't contain space."); } else if (argc == 3 && strcmp(argv[1], "theme") == 0) { // Try to load the user-specified theme if (loadTheme(&appState, &settings, argv[2], false) > 0) { ui->colorMode = COLOR_MODE_THEME; initDefaultState(&appState); themeLoaded = true; } else { // Failed to load user theme → fall back to // default/ANSI if (ui->colorMode == COLOR_MODE_THEME) { ui->colorMode = COLOR_MODE_DEFAULT; } } } else if (ui->colorMode == COLOR_MODE_THEME) { // If UI has a themeName stored, try to load it if (loadTheme(&appState, &settings, ui->themeName, false) > 0) { ui->colorMode = COLOR_MODE_THEME; themeLoaded = true; } } // If still in default mode, load default ANSI theme if (ui->colorMode == COLOR_MODE_DEFAULT) { // Load "default" ANSI theme, but don't overwrite // settings->theme if (loadTheme(&appState, &settings, "default", true)) { themeLoaded = true; } } if (!themeLoaded && ui->colorMode != COLOR_MODE_ALBUM) { setErrorMessage("Couldn't load theme. Forgot to run 'sudo make install'?"); ui->colorMode = COLOR_MODE_ALBUM; } } int main(int argc, char *argv[]) { initState(&appState); 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); } initSettings(&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); loadFavoritesPlaylist(settings.path); ensureDefaultThemes(); initTheme(argc, argv, ui); if (argc == 1) { initDefaultState(&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 && favoritesPlaylist->count != 0) { playFavoritesPlaylist(&appState); } else if (argc >= 2) { init(&appState); makePlaylist(argc, argv, exactSearch, settings.path); if (playlist.count == 0) { noPlaylist = true; exit(0); } markListAsEnqueued(library, &playlist); run(&appState, true); } return 0; } kew/src/m4a.c000066400000000000000000000006371507107350600132720ustar00rootroot00000000000000 /* 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/src/m4a.h000066400000000000000000001442721507107350600133030ustar00rootroot00000000000000 /* 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 "player_ui.h" #include #include #include #include "common.h" typedef struct m4a_decoder { 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; // 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 } 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)) { // This is an alac file and is currently unsupported. setErrorMessage("M4a files that use the ALAC encoder are not supported."); return MA_ERROR; } 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 (decoder_config_len >= 2) { uint8_t object_type = (decoder_config[0] >> 3) & 0x1F; if (object_type == 5 || object_type == 29) { setErrorMessage("Unsupported AAC object type: (HE-AAC or PS)"); return MA_ERROR; } } 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); } 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->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) { setErrorMessage("Decoding Error: could be mislabeled and unsupported HE-AAC or PS file"); // 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/src/mpris.c000066400000000000000000001447521507107350600137520ustar00rootroot00000000000000#include "mpris.h" #include "common.h" #include "playerops.h" #include "sound.h" #include "soundcommon.h" #include /* 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; #define MAX_STATUS_LEN 64 void updatePlaybackStatus(const gchar *status) { if (status == NULL) { fprintf(stderr, "updatePlaybackStatus: status is NULL\n"); return; } size_t len = strlen(status); if (len == 0 || len > MAX_STATUS_LEN) { fprintf(stderr, "updatePlaybackStatus: invalid status length\n"); return; } GVariant *status_variant = g_variant_new_string(status); // Emit signal with a tuple containing the string g_dbus_connection_emit_signal( connection, NULL, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player", "PlaybackStatus", g_variant_new("(s)", status_variant), NULL); } 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 (g_strcmp0(currentSongData->metadata->artist, "") == 0) { artistList[0] = ""; } else { artistList[0] = currentSongData->metadata->artist; } 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(&(appState.uiSettings)); return TRUE; } else if (g_strcmp0(property_name, "Shuffle") == 0) { toggleShuffle(&(appState.uiSettings)); 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/src/mpris.h000066400000000000000000000011331507107350600137400ustar00rootroot00000000000000#ifndef MPRIS_H #define MPRIS_H #include #include "playlist.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/src/notifications.c000066400000000000000000000270541507107350600154640ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include "notifications.h" /* notifications.c Related to desktop notifications. */ bool isValidFilepath(const char *path) { if (path == NULL || *path == '\0' || strnlen(path, PATH_MAX) >= PATH_MAX) return false; struct stat st; return stat(path, &st) == 0 && S_ISREG(st.st_mode); } void removeBlacklistedChars(const char *input, const char *blacklist, char *output, size_t output_size) { if (!input || !blacklist || !output || output_size < 2) { if (output && output_size > 0) output[0] = '\0'; return; } const char *in_ptr = input; char *out_ptr = output; size_t chars_copied = 0; while (*in_ptr && chars_copied < output_size - 1) { unsigned char c = (unsigned char)*in_ptr; // Skip non-printable characters and blacklist if (isprint(c) && !strchr(blacklist, c)) { *out_ptr++ = c; chars_copied++; } in_ptr++; } *out_ptr = '\0'; } void ensureNonEmpty(char *str, size_t bufferSize) { if (str == NULL|| bufferSize < 2) { return; } 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]; static pthread_mutex_t notificationMutex = PTHREAD_MUTEX_INITIALIZER; int canShowNotification(void) { struct timeval now; gettimeofday(&now, NULL); pthread_mutex_lock(¬ificationMutex); // Calculate elapsed time in microseconds using 64-bit unsigned math int64_t sec_diff = (int64_t)(now.tv_sec - lastNotificationTime.tv_sec); int64_t usec_diff = (int64_t)(now.tv_usec - lastNotificationTime.tv_usec); if (usec_diff < 0) { usec_diff += 1000000; sec_diff -= 1; } uint64_t elapsed = (uint64_t)(sec_diff * 1000000 + usec_diff); if (elapsed >= NOTIFICATION_INTERVAL_MICROSECONDS) { lastNotificationTime = now; pthread_mutex_unlock(¬ificationMutex); return 1; } pthread_mutex_unlock(¬ificationMutex); 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, sizeof(sanitizedTitle)); ensureNonEmpty(sanitizedTitle, sizeof(sanitizedTitle)); int coverExists = isValidFilepath(cover); 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 kew/src/notifications.h000066400000000000000000000004001507107350600154530ustar00rootroot00000000000000 #ifndef NOTIFICATIONS_H #define NOTIFICATIONS_H #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 #endif kew/src/player_ui.c000066400000000000000000002107771507107350600146120ustar00rootroot00000000000000#include "player_ui.h" #include "appstate.h" #include "common_ui.h" #include "directorytree.h" #include "imgfunc.h" #include "playerops.h" #include "playlist.h" #include "playlist_ui.h" #include "search_ui.h" #include "songloader.h" #include "sound.h" #include "term.h" #include "utils.h" #include "visuals.h" #include #include #include #include /* player_ui.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 = 65; #endif bool fastForwarding = false; bool rewinding = false; 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 numTopLevelSongs = 0; int startLibIter = 0; int startSearchIter = 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 libIter = 0; int libSongIter = 0; int libTopLevelSongIter = 0; int previousChosenLibRow = 0; int libCurrentDirSongCount = 0; int lastRowRow = 0; int lastRowCol = 0; int progressBarRow = 0; int progressBarCol = 0; int progressBarLength = 0; int draggedProgressBarCol = 0; double draggedPositionSeconds = 0.0; bool draggingProgressBar = false; int miniVisualizerRow = 0; PixelData lastRowColor = {120, 120, 120}; const char LIBRARY_FILE[] = "kewlibrary"; FileSystemEntry *currentEntry = NULL; FileSystemEntry *lastEntry = NULL; FileSystemEntry *chosenDir = NULL; FileSystemEntry *library = NULL; static const int LOGO_WIDTH = 22; const char *LOGO[] = {" __\n", "| |--.-----.--.--.--.\n", "| <| -__| | | |\n", "|__|__|_____|________|"}; #define MAX_TERM_SIZE 10000 // Safety limit int calcIdealImgSize(int *width, int *height, const int visualizerHeight, const int metatagHeight) { if (!width || !height) return -1; float aspectRatio = calcAspectRatio(); if (!isfinite(aspectRatio) || aspectRatio <= 0.0f || aspectRatio > 100.0f) aspectRatio = 1.0f; // fallback to square int term_w = 0, term_h = 0; getTermSize(&term_w, &term_h); if (term_w <= 0 || term_h <= 0 || term_w > MAX_TERM_SIZE || term_h > MAX_TERM_SIZE) { *width = 1; *height = 1; return -1; } const int timeDisplayHeight = 1; const int heightMargin = 4; const int minHeight = visualizerHeight + metatagHeight + timeDisplayHeight + heightMargin + 1; if (minHeight < 0 || minHeight > term_h) { *width = 1; *height = 1; return -1; } int availableHeight = term_h - minHeight; if (availableHeight <= 0) { *width = 1; *height = 1; return -1; } // Safe calculation using double double safeHeight = (double)availableHeight; double safeAspect = (double)aspectRatio; double tempWidth = safeHeight * safeAspect; // Clamp to INT_MAX and reasonable limits if (tempWidth < 1.0) tempWidth = 1.0; else if (tempWidth > INT_MAX) tempWidth = INT_MAX; int calcWidth = (int)ceil(tempWidth); int calcHeight = availableHeight; if (calcWidth > term_w) { calcWidth = term_w; if (calcWidth <= 0) { *width = 1; *height = 1; return -1; } double tempHeight = (double)calcWidth / safeAspect; if (tempHeight < 1.0) tempHeight = 1.0; else if (tempHeight > INT_MAX) tempHeight = INT_MAX; calcHeight = (int)floor(tempHeight); } // Final clamping if (calcWidth < 1) calcWidth = 1; if (calcHeight < 2) calcHeight = 2; // Slight adjustment calcHeight -= 1; if (calcHeight < 1) calcHeight = 1; *width = calcWidth; *height = calcHeight; 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() { fputs(" 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)\n" " 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 theme (sets a theme)\n" " kew . (plays kew favorites.m3u file)\n" " kew shuffle (random and rand works too)\n" " kew artistA:artistB (plays artistA and artistB " "shuffled)\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 key bindings view.\n" " You can also use the mouse to switch views.\n" " u to update the library.\n" " v to toggle the spectrum visualizer.\n" " i to cycle between colors from kewrc, theme," " or from the track album 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 current playlist to a .m3u in your music folder " "named after the first song.\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 to 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 currently playing song to \"kew favorites.m3u\" (run " "with \"kew .\").\n" " Esc or q to quit.\n" "\n", stdout); } static const char *getPlayerStatusIcon(void) { if (isPaused()) return "â¸"; if (isStopped()) return "â– "; return "â–¶"; } static int printLogoArt(const UISettings *ui, int indent) { if (ui->hideLogo) { clearLine(); printf("\n"); return 1; } size_t logoHeight = sizeof(LOGO) / sizeof(LOGO[0]); for (size_t i = 0; i < logoHeight; i++) { PixelData rowColor = {defaultColor, defaultColor, defaultColor}; if (!(ui->color.r == defaultColor && ui->color.g == defaultColor && ui->color.b == defaultColor)) { rowColor = getGradientColor(ui->color, logoHeight - i, logoHeight, 2, 0.8f); } applyColor(ui->colorMode, ui->theme.logo, rowColor); clearLine(); printBlankSpaces(indent); printf("%s", LOGO[i]); } return 3; // lines used by logo } static void buildSongTitle(const SongData *songData, const UISettings *ui, char *out, size_t outSize, int indent) { if (!songData || !songData->metadata) { out[0] = '\0'; return; } const char *icon = getPlayerStatusIcon(); char prettyTitle[METADATA_MAX_SIZE] = {0}; snprintf(prettyTitle, METADATA_MAX_SIZE, "%s", songData->metadata->title); trim(prettyTitle, strlen(prettyTitle)); if (ui->hideLogo && songData->metadata->artist[0] != '\0') { snprintf(out, outSize, "%*s%s %s - %s", indent, "", icon, songData->metadata->artist, prettyTitle); } else if (ui->hideLogo) { snprintf(out, outSize, "%*s%s %s", indent, "", icon, prettyTitle); } else { strncpy(out, prettyTitle, outSize - 1); out[outSize - 1] = '\0'; } } int printLogo(SongData *songData, UISettings *ui) { int term_w, term_h; getTermSize(&term_w, &term_h); char title[MAXPATHLEN + 1]; int logoWidth = ui->hideLogo ? 0 : LOGO_WIDTH; int maxWidth = term_w - indent - indent - (ui->hideLogo ? 2 : logoWidth + 4); int height = printLogoArt(ui, indent); buildSongTitle(songData, ui, title, sizeof(title), indent); applyColor(ui->colorMode, ui->theme.nowplaying, ui->color); if (title[0] != '\0') { char processed[MAXPATHLEN + 1] = {0}; processName(title, processed, maxWidth, false, false); if (ui->hideLogo) printBlankSpaces(indent); printf(" %s", processed); } printf("\n"); clearLine(); printf("\n"); return height + 2; } int getYear(const char *dateString) { int year; if (sscanf(dateString, "%d", &year) != 1) { return -1; } return year; } void printCoverCentered(SongData *songdata, UISettings *ui) { if (songdata != NULL && songdata->cover != NULL && ui->coverEnabled) { if (!ui->coverAnsi) { printSquareBitmapCentered( songdata->cover, songdata->coverWidth, songdata->coverHeight, preferredHeight); } else { printInAsciiCentered(songdata->coverArtPath, preferredHeight); } } else { for (int i = 0; i <= preferredHeight; ++i) { printf("\n"); } } printf("\n\n"); } void printCover(int row, int col, int height, SongData *songdata, UISettings *ui) { int imgHeight = height; if (row == 2) imgHeight -= 2; clearScreen(); if (songdata != NULL && songdata->cover != NULL && ui->coverEnabled) { if (!ui->coverAnsi) { printSquareBitmap(row, col, songdata->cover, songdata->coverWidth, songdata->coverHeight, imgHeight); } else { printInAscii(col, songdata->coverArtPath, imgHeight); } } } void printTitleWithDelay(int row, int col, 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("\033[%d;%dH", row, col); clearRestOfLine(); for (int j = 0; j < i; j++) { printf("%c", text[j]); } printf("â–ˆ"); fflush(stdout); c_sleep(delay); } if (delay) c_sleep(delay * 20); printf("\033[%d;%dH", row, col); clearRestOfLine(); printf("%s", text); printf("\n"); fflush(stdout); } void printBasicMetadata(int row, int col, int maxWidth, TagSettings const *metadata, UISettings *ui) { if (strnlen(metadata->artist, METADATA_MAX_LENGTH) > 0) { applyColor(ui->colorMode, ui->theme.trackview_artist, ui->color); printf("\033[%d;%dH", row + 1, col); clearRestOfLine(); printf(" %.*s", maxWidth, metadata->artist); } if (strnlen(metadata->album, METADATA_MAX_LENGTH) > 0) { applyColor(ui->colorMode, ui->theme.trackview_album, ui->color); printf("\033[%d;%dH", row + 2, col); clearRestOfLine(); printf(" %.*s", maxWidth, metadata->album); } if (strnlen(metadata->date, METADATA_MAX_LENGTH) > 0) { applyColor(ui->colorMode, ui->theme.trackview_year, ui->color); printf("\033[%d;%dH", row + 3, col); clearRestOfLine(); int year = getYear(metadata->date); if (year == -1) printf(" %s", metadata->date); else printf(" %d", year); } PixelData pixel = increaseLuminosity(ui->color, 20); if (pixel.r == 255 && pixel.g == 255 && pixel.b == 255) { pixel.r = defaultColor; pixel.g = defaultColor; pixel.b = defaultColor; } applyColor(ui->colorMode, ui->theme.trackview_title, pixel); if (strnlen(metadata->title, METADATA_MAX_LENGTH) > 0) { // Clean up title before printing char prettyTitle[MAXPATHLEN + 1]; prettyTitle[0] = '\0'; processName(metadata->title, prettyTitle, maxWidth, false, false); printTitleWithDelay(row, col + 1, prettyTitle, ui->titleDelay, maxWidth); } } 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; 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(); if (total_seconds >= 3600) { // Song is more than 1 hour long: use full HH:MM:SS format 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); } else { // Song is less than 1 hour: use M:SS format int elapsed_total_minutes = elapsed_seconds / 60; int elapsed_secs = (int)elapsed_seconds % 60; int total_total_minutes = total_seconds / 60; int total_secs = (int)total_seconds % 60; printf(" %d:%02d / %d:%02d (%d%%) Vol:%d%%", elapsed_total_minutes, elapsed_secs, total_total_minutes, total_secs, progress_percentage, vol); } double rate = ((float)sampleRate) / 1000; if (term_w > progressWidth + 10) { if (rate == (int)rate) printf(" %dkHz", (int)rate); else printf(" %.1fkHz", rate); } if (term_w > progressWidth + 19) { if (avgBitRate > 0) printf(" %dkb/s ", avgBitRate); } } void printTime(int row, int col, double elapsedSeconds, ma_uint32 sampleRate, int avgBitRate, AppState *state) { applyColor(state->uiSettings.colorMode, state->uiSettings.theme.trackview_time, state->uiSettings.color); int term_w, term_h; getTermSize(&term_w, &term_h); printf("\033[%d;%dH", row, col); if (term_h > minHeight) { double duration = getCurrentSongDuration(); double elapsed = elapsedSeconds; printProgress(elapsed, duration, sampleRate, avgBitRate); clearRestOfLine(); } } 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); } } int getIndent() { return indent; } void printGlimmeringText(int row, int col, char *text, int textLength, char *nerdFontText, PixelData color) { int brightIndex = 0; PixelData vbright = increaseLuminosity(color, 120); PixelData bright = increaseLuminosity(color, 60); printf("\033[%d;%dH", row, col); clearRestOfLine(); 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("\033[%d;%dH", row, col); } } void printErrorRow(int row, int col, UISettings *ui) { int term_w, term_h; getTermSize(&term_w, &term_h); printf("\033[%d;%dH", row, col); if (!hasPrintedError && hasErrorMessage()) { applyColor(ui->colorMode, ui->theme.footer, lastRowColor); printf(" %s", getErrorMessage()); hasPrintedError = true; } clearRestOfLine(); fflush(stdout); } void formatWithShiftPlus(char *dest, size_t size, const char *src) { if (isupper((unsigned char)src[0])) { snprintf(dest, size, "Shft+%s", src); } else { snprintf(dest, size, "%s", src); } } void printFooter(int row, int col, UISettings *ui, AppSettings *settings) { int term_w, term_h; getTermSize(&term_w, &term_h); if (preferredWidth < 0 || preferredHeight < 0) // mini view return; lastRowRow = row; lastRowCol = col; printf("\033[%d;%dH", row, col); PixelData footerColor; footerColor.r = lastRowColor.r; footerColor.g = lastRowColor.g; footerColor.b = lastRowColor.b; applyColor(ui->colorMode, ui->theme.footer, footerColor); if (ui->themeIsSet && ui->theme.footer.type == COLOR_TYPE_RGB) { footerColor.r = ui->theme.footer.rgb.r; footerColor.g = ui->theme.footer.rgb.g; footerColor.b = ui->theme.footer.rgb.b; } char text[100]; #if defined(__ANDROID__) || defined(__APPLE__) char playlist[32], library[32], track[32], search[32], help[32]; // Assume settings->showPlaylistAlt etc. are defined properly formatWithShiftPlus(playlist, sizeof(playlist), settings->showPlaylistAlt); formatWithShiftPlus(library, sizeof(library), settings->showLibraryAlt); formatWithShiftPlus(track, sizeof(track), settings->showTrackAlt); formatWithShiftPlus(search, sizeof(search), settings->showSearchAlt); formatWithShiftPlus(help, sizeof(help), settings->showKeysAlt); snprintf(text, sizeof(text), "%s Playlist|%s Library|%s Track|%s Search|%s Help", playlist, library, track, search, help); #else (void)settings; strcpy(text, LAST_ROW); #endif char nerdFontText[100] = ""; size_t maxLength = sizeof(nerdFontText); size_t currentLength = strnlen(nerdFontText, maxLength); if (term_w >= ABSOLUTE_MIN_WIDTH) { if (isPaused()) { char pauseText[] = " â¸"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", pauseText); currentLength += strlen(pauseText); } else if (isStopped()) { char pauseText[] = " â– "; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", pauseText); currentLength += strlen(pauseText); } else { char pauseText[] = " â–¶"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", pauseText); currentLength += strlen(pauseText); } } if (isRepeatEnabled()) { char repeatText[] = " \u27f3"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", repeatText); currentLength += strlen(repeatText); } else if (isRepeatListEnabled()) { char repeatText[] = " \u27f3L"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", repeatText); currentLength += strlen(repeatText); } if (isShuffleEnabled()) { char shuffleText[] = " \uf074"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", shuffleText); currentLength += strlen(shuffleText); } if (fastForwarding) { char forwardText[] = " \uf04e"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", forwardText); currentLength += strlen(forwardText); } if (rewinding) { char rewindText[] = " \uf04a"; snprintf(nerdFontText + currentLength, maxLength - currentLength, "%s", rewindText); currentLength += strlen(rewindText); } if (term_w < ABSOLUTE_MIN_WIDTH) { #ifndef __ANDROID__ if (term_w > (int)currentLength + indent) { printf("%s", nerdFontText); // Print just the shuffle // and replay settings } #else // Always try to print the footer on Android because it will // most likely be too narrow. We use two rows for the footer on // Android. printBlankSpaces(indent); printf("%.*s", term_w * 2, text); #endif return; } int textLength = strnlen(text, 100); int randomNumber = getRandomNumber(1, 808); if (randomNumber == 808 && !ui->hideGlimmeringText) printGlimmeringText(row, col, text, textLength, nerdFontText, footerColor); else { printf("%s", text); printf("%s", nerdFontText); } clearRestOfLine(); } void calcAndPrintLastRowAndErrorRow(UISettings *ui, AppSettings *settings) { int term_w, term_h; getTermSize(&term_w, &term_h); #if defined(__ANDROID__) // Use two rows for the footer on Android. It makes everything // fit even with narrow terminal widths. if (hasErrorMessage()) printErrorRow(term_h - 1, indent, ui); else printFooter(term_h - 1, indent, ui, settings); #else printErrorRow(term_h - 1, indent, ui); printFooter(term_h, indent, ui, settings); #endif } int printAbout(SongData *songdata, UISettings *ui) { clearLine(); int numRows = printLogo(songdata, ui); applyColor(ui->colorMode, ui->theme.text, defaultColorRGB); printBlankSpaces(indent); printf(" kew version: "); applyColor(ui->colorMode, ui->theme.help, ui->color); printf("%s\n", VERSION); clearLine(); printf("\n"); 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; clearScreen(); numPrintedRows += printAbout(songdata, ui); applyColor(ui->colorMode, ui->theme.text, defaultColorRGB); printBlankSpaces(indent); printf(" Keybindings:\n\n"); printBlankSpaces(indent); printf(" · Play/Pause: SPACE, %s or right click\n", settings->togglePause); printBlankSpaces(indent); printf(" · Enqueue/Dequeue: Enter\n"); printBlankSpaces(indent); printf(" · Quit: Esc or %s\n", settings->quit); printBlankSpaces(indent); printf(" · Switch tracks: ↠and → or %s and %s\n", settings->previousTrackAlt, settings->nextTrackAlt); printBlankSpaces(indent); printf(" · Volume: %s (or %s) and %s\n", settings->volumeUp, settings->volumeUpAlt, settings->volumeDown); printBlankSpaces(indent); printf(" · Clear List: Backspace\n"); printBlankSpaces(indent); printf(" · Change View: TAB or "); #if defined(__ANDROID__) || defined(__APPLE__) printf("%s, %s, %s, %s, %s", settings->showPlaylistAlt, settings->showLibraryAlt, settings->showTrackAlt, settings->showSearchAlt, settings->showKeysAlt); #else printf("F2-F6"); #endif printf(" or click the footer\n"); printBlankSpaces(indent); printf( " · Cycle Color Mode: %s (default theme, theme or cover colors)\n", settings->cycleColorsDerivedFrom); printBlankSpaces(indent); printf(" · Cycle Themes: %s\n", settings->cycleThemes); printBlankSpaces(indent); printf(" · Stop: Shift+s\n"); printBlankSpaces(indent); printf(" · Update Library: %s\n", settings->updateLibrary); printBlankSpaces(indent); printf(" · Toggle Visualizer: %s\n", settings->toggleVisualizer); printBlankSpaces(indent); printf(" · Toggle ASCII Cover: %s\n", settings->toggleAscii); printBlankSpaces(indent); printf(" · Toggle Notifications: %s\n", settings->toggleNotifications); printBlankSpaces(indent); printf(" · Cycle Repeat: %s (repeat/repeat list/off)\n", settings->toggleRepeat); printBlankSpaces(indent); printf(" · Shuffle: %s\n", settings->toggleShuffle); printBlankSpaces(indent); printf(" · Seek: %s and %s\n", settings->seekBackward, settings->seekForward); printBlankSpaces(indent); printf(" · Export Playlist: %s (to Music folder, " "named after the first song)\n", settings->savePlaylist); printBlankSpaces(indent); printf(" · Add Song To 'kew favorites.m3u': %s (run with 'kew .')\n\n", settings->addToFavoritesPlaylist); printBlankSpaces(indent); printf(" Manual: See"); applyColor(ui->colorMode, ui->theme.help, ui->color); printf(" README"); applyColor(ui->colorMode, ui->theme.text, defaultColorRGB); printf(" Or man kew\n\n"); applyColor(ui->colorMode, ui->theme.text, defaultColorRGB); printBlankSpaces(indent); printf(" Theme: "); if (ui->colorMode == COLOR_MODE_ALBUM) { applyColor(ui->colorMode, ui->theme.text, defaultColorRGB); printf("Using "); applyColor(ui->colorMode, ui->theme.text, ui->color); printf("Colors "); applyColor(ui->colorMode, ui->theme.text, defaultColorRGB); printf("From Track Covers"); } else { applyColor(ui->colorMode, ui->theme.help, ui->color); printf("%s", ui->theme.theme_name); } applyColor(ui->colorMode, ui->theme.text, defaultColorRGB); if (ui->colorMode != COLOR_MODE_ALBUM) { printf(" Author: "); applyColor(ui->colorMode, ui->theme.help, ui->color); printf("%s", ui->theme.theme_author); numPrintedRows += 1; } printf("\n"); numPrintedRows += 1; printf("\n"); printBlankSpaces(indent); applyColor(ui->colorMode, ui->theme.help, defaultColorRGB); printf(" Project URL:"); applyColor(ui->colorMode, ui->theme.link, ui->color); printf(" https://codeberg.org/ravachol/kew\n"); printBlankSpaces(indent); applyColor(ui->colorMode, ui->theme.help, defaultColorRGB); printf(" Please Donate:"); applyColor(ui->colorMode, ui->theme.link, ui->color); printf(" https://ko-fi.com/ravachol\n\n"); applyColor(ui->colorMode, ui->theme.text, defaultColorRGB); printBlankSpaces(indent); printf(" Copyright © 2022-2025 Ravachol\n"); printf("\n"); numPrintedRows += 31; while (numPrintedRows < maxListSize) { printf("\n"); numPrintedRows++; } calcAndPrintLastRowAndErrorRow(ui, settings); numPrintedRows++; return numPrintedRows; } void toggleShowView(ViewState viewToShow) { refresh = true; if (appState.currentView == TRACK_VIEW) clearScreen(); 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) ? TRACK_VIEW : SEARCH_VIEW; break; case TRACK_VIEW: appState.currentView = SEARCH_VIEW; clearScreen(); break; case SEARCH_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; clearScreen(); break; case SEARCH_VIEW: appState.currentView = (currentSong != NULL) ? TRACK_VIEW : LIBRARY_VIEW; break; case KEYBINDINGS_VIEW: appState.currentView = SEARCH_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 >= unshuffledPlaylist->count) ? unshuffledPlaylist->count - 1 : chosenRow; refresh = true; } else if (appState.currentView == SEARCH_VIEW) { chosenSearchResultRow += maxSearchListSize - 1; chosenSearchResultRow = (chosenSearchResultRow >= getSearchResultsCount()) ? getSearchResultsCount() - 1 : chosenSearchResultRow; startSearchIter += maxSearchListSize - 1; refresh = true; } } void flipPrevPage(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; } } void scrollNext(void) { if (appState.currentView == PLAYLIST_VIEW) { chosenRow++; chosenRow = (chosenRow >= unshuffledPlaylist->count) ? unshuffledPlaylist->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; } } 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; } } int getRowWithinBounds(int row) { if (row >= unshuffledPlaylist->count) { row = unshuffledPlaylist->count - 1; } if (row < 0) row = 0; return row; } int printLogoAndAdjustments(SongData *songData, int termWidth, UISettings *ui, int indentation, AppSettings *settings) { int aboutRows = printLogo(songData, ui); applyColor(ui->colorMode, ui->theme.help, defaultColorRGB); if (termWidth > 52 && !ui->hideHelp) { printBlankSpaces(indentation); printf(" Use ↑/↓ or k/j to select. Enter=Accept. Backspace: " "clear.\n"); printBlankSpaces(indentation); #ifndef __APPLE__ printf(" PgUp/PgDn: scroll. Del: remove. %s/%s: move songs.\n", settings->moveSongUp, settings->moveSongDown); clearLine(); printf("\n"); #else printf(" Fn+↑/↓: scroll. Del: remove. %s/%s: move songs.\n", settings->moveSongUp, settings->moveSongDown); clearLine(); printf("\n"); #endif return aboutRows + 3; } return aboutRows; } void showSearch(SongData *songData, int *chosenRow, UISettings *ui, AppSettings *settings) { int term_w, term_h; getTermSize(&term_w, &term_h); maxSearchListSize = term_h - 3; gotoFirstLineFirstRow(); int aboutRows = printLogo(songData, ui); maxSearchListSize -= aboutRows; applyColor(ui->colorMode, ui->theme.help, defaultColorRGB); if (term_w > indent + 38 && !ui->hideHelp) { clearLine(); printBlankSpaces(indent); printf(" Use ↑/↓ to select. Enter=Enqueue. Alt+Enter=Play.\n"); clearLine(); printf("\n"); maxSearchListSize -= 2; } displaySearch(maxSearchListSize, indent, chosenRow, startSearchIter, ui); calcAndPrintLastRowAndErrorRow(ui, settings); } void showPlaylist(SongData *songData, PlayList *list, int *chosenSong, int *chosenNodeId, AppState *state, AppSettings *settings) { int term_w, term_h; getTermSize(&term_w, &term_h); maxListSize = term_h - 3; UISettings *ui = &(state->uiSettings); // Setup scrolling names if (getIsLongName() && isSameNameAsLastTime && updateCounter % scrollingInterval != 0) { updateCounter++; refresh = true; return; } else refresh = false; gotoFirstLineFirstRow(); int aboutRows = printLogoAndAdjustments(songData, term_w, ui, indent, settings); maxListSize -= aboutRows; applyColor(ui->colorMode, ui->theme.header, ui->color); if (maxListSize > 0) { clearLine(); printBlankSpaces(indent); printf(" ─ PLAYLIST ─\n"); } maxListSize -= 1; if (maxListSize > 0) displayPlaylist(list, maxListSize, indent, chosenSong, chosenNodeId, state->uiState.resetPlaylistDisplay, state); calcAndPrintLastRowAndErrorRow(&(state->uiSettings), settings); } void resetSearchResult(void) { chosenSearchResultRow = 0; } void printProgressBar(int row, int col, AppSettings *settings, UISettings *ui, int elapsedBars, int numProgressBars) { PixelData color = ui->color; progressBarRow = row; progressBarCol = col + 1; progressBarLength = numProgressBars; printf("\033[%d;%dH", row, col + 1); for (int i = 0; i < numProgressBars; i++) { if (i > elapsedBars) { if (ui->colorMode == COLOR_MODE_ALBUM) { PixelData tmp = increaseLuminosity(color, 50); printf("\033[38;2;%d;%d;%dm", tmp.r, tmp.g, tmp.b); applyColor(ui->colorMode, ui->theme.progress_empty, tmp); } else { applyColor(ui->colorMode, ui->theme.progress_empty, color); } if (i % 2 == 0) printf( "%s", settings->progressBarApproachingEvenChar); else printf("%s", settings->progressBarApproachingOddChar); continue; } if (i < elapsedBars) { applyColor(ui->colorMode, ui->theme.progress_filled, color); if (i % 2 == 0) printf("%s", settings->progressBarElapsedEvenChar); else printf("%s", settings->progressBarElapsedOddChar); } else if (i == elapsedBars) { applyColor(ui->colorMode, ui->theme.progress_elapsed, color); if (i % 2 == 0) printf("%s", settings->progressBarCurrentEvenChar); else printf("%s", settings->progressBarCurrentOddChar); } } clearRestOfLine(); } void printVisualizer(int row, int col, int visualizerWidth, AppSettings *settings, double elapsedSeconds, AppState *state) { UISettings *ui = &(state->uiSettings); UIState *uis = &(state->uiState); int height = state->uiSettings.visualizerHeight; int term_w, term_h; getTermSize(&term_w, &term_h); if (row + height + 2 > term_h) height -= (row + height + 1 - term_h); if (height < 2) return; if (ui->visualizerEnabled) { uis->numProgressBars = (int)visualizerWidth / 2; double duration = getCurrentSongDuration(); drawSpectrumVisualizer(row, col, height, state); int elapsedBars = calcElapsedBars(elapsedSeconds, duration, visualizerWidth); printProgressBar(row + height - 1, col, settings, ui, elapsedBars, visualizerWidth - 1); } } 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; } void applyTreeItemColor(UISettings *ui, int depth, PixelData rowColor, bool isEnqueued, bool isPlaying) { if (depth <= 1) { applyColor(ui->colorMode, ui->theme.library_artist, rowColor); } else { applyColor(ui->colorMode, ui->theme.library_track, defaultColorRGB); } if (isEnqueued) { if (isPlaying) { applyColor(ui->colorMode, ui->theme.library_playing, rowColor); } else { applyColor(ui->colorMode, ui->theme.library_enqueued, rowColor); } } } int displayTree(FileSystemEntry *root, int depth, int maxListSize, int maxNameWidth, AppState *state) { if (maxNameWidth < 0) maxNameWidth = 0; char dirName[maxNameWidth + 1]; char filename[MAXPATHLEN + 1]; bool foundChosen = false; int isPlaying = 0; int extraIndent = 0; UISettings *ui = &(state->uiSettings); UIState *uis = &(state->uiState); if (currentSong != NULL && (strcmp(currentSong->song.filePath, root->fullPath) == 0)) { isPlaying = 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; PixelData rowColor; rowColor.r = defaultColor; rowColor.g = defaultColor; rowColor.b = defaultColor; if (!(ui->color.r == defaultColor && ui->color.g == defaultColor && ui->color.b == defaultColor)) rowColor = getGradientColor(ui->color, libIter - startLibIter, maxListSize, maxListSize / 2, 0.7f); 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)))) { return foundChosen; } 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) { applyTreeItemColor(ui, depth, rowColor, root->isEnqueued, isPlaying); clearLine(); 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) { printf("\x1b[7m * "); } else { printf(" \x1b[7m "); } currentEntry = root; if (uis->allowChooseSongs == true && (chosenDir == NULL || (currentEntry != NULL && currentEntry->parent != NULL && chosenDir != NULL && !isContainedWithin(currentEntry, chosenDir) && 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) { 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, true, true); } if (isPlaying) { if (chosenLibRow == libIter) { printf("\x1b[7m"); } } printf("└─ "); // Playlist if (pathEndsWith(root->fullPath, "m3u") || pathEndsWith(root->fullPath, "m3u8")) { printf("♫ "); maxNameWidth = maxNameWidth - 2; } if (isPlaying && chosenLibRow != libIter) { printf("\e[4m"); } printf("%s\n", filename); libSongIter++; } } 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, AppSettings *settings) { // For scrolling names, update every nth time if (getIsLongName() && isSameNameAsLastTime && updateCounter % scrollingInterval != 0) { refresh = true; return; } else refresh = false; gotoFirstLineFirstRow(); 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; int aboutSize = printLogo(songData, ui); int maxNameWidth = term_w - 10 - indent; maxLibListSize -= aboutSize + 2; applyColor(ui->colorMode, ui->theme.help, defaultColorRGB); if (term_w > 67 && !ui->hideHelp) { maxLibListSize -= 3; clearLine(); printBlankSpaces(indent); printf(" Use ↑/↓ or k/j to select. Enter=Enqueue/Dequeue. " "Alt+Enter=Play.\n"); clearLine(); printBlankSpaces(indent); #ifndef __APPLE__ printf(" PgUp/PgDn: scroll. u: update, o: sort.\n"); clearLine(); printf("\n"); #else printf(" Fn+↑/↓: scroll. u: update, o: sort.\n"); clearLine(); printf("\n"); #endif } numTopLevelSongs = 0; FileSystemEntry *tmp = library->children; while (tmp != NULL) { if (!tmp->isDirectory) numTopLevelSongs++; tmp = tmp->next; } bool foundChosen = false; if (maxLibListSize <= 0) foundChosen = true; else foundChosen = displayTree(library, 0, maxLibListSize, maxNameWidth, state); if (!foundChosen) { chosenLibRow--; refresh = true; } for (int i = libIter - startLibIter; i < maxLibListSize; i++) { clearLine(); printf("\n"); } calcAndPrintLastRowAndErrorRow(ui, settings); if (!foundChosen && refresh) { gotoFirstLineFirstRow(); showLibrary(songData, state, settings); } } int calcVisualizerWidth() { int term_w, term_h; getTermSize(&term_w, &term_h); int visualizerWidth = (ABSOLUTE_MIN_WIDTH > preferredWidth) ? ABSOLUTE_MIN_WIDTH : preferredWidth; visualizerWidth = (visualizerWidth < textWidth && textWidth < term_w - 2) ? textWidth : visualizerWidth; visualizerWidth = (visualizerWidth > term_w - 2) ? term_w - 2 : visualizerWidth; visualizerWidth -= 1; return visualizerWidth; } void showTrackViewLandscape(int height, int width, float aspectRatio, AppSettings *settings, SongData *songdata, AppState *state, double elapsedSeconds) { TagSettings *metadata = NULL; int avgBitRate = 0; int metadataHeight = 4; int timeHeight = 1; if (songdata) { metadata = songdata->metadata; } int col = height * aspectRatio + 1; if (!state->uiSettings.coverEnabled || (songdata && songdata->cover == NULL)) col = 1; int term_w, term_h; getTermSize(&term_w, &term_h); int visualizerWidth = term_w - col; int row = height - metadataHeight - timeHeight - state->uiSettings.visualizerHeight - 3; if (row < 2) row = 2; if (refresh) { printCover(1, 1, height, songdata, &(state->uiSettings)); if (height > metadataHeight) printBasicMetadata(row, col, visualizerWidth - 1, metadata, &(state->uiSettings)); refresh = false; } if (songdata) { ma_uint32 sampleRate; ma_format format; avgBitRate = songdata->avgBitRate; getCurrentFormatAndSampleRate(&format, &sampleRate); if (height > metadataHeight + timeHeight) printTime(row + 4, col, elapsedSeconds, sampleRate, avgBitRate, state); } if (row > 0) printVisualizer(row + metadataHeight + 2, col, visualizerWidth, settings, elapsedSeconds, state); if (width - col > ABSOLUTE_MIN_WIDTH) { printErrorRow(row + metadataHeight + 2 + state->uiSettings.visualizerHeight, col, &(state->uiSettings)); printFooter(row + metadataHeight + 2 + state->uiSettings.visualizerHeight + 1, col, &(state->uiSettings), settings); } } void showTrackViewPortrait(int height, AppSettings *settings, SongData *songdata, AppState *state, double elapsedSeconds) { TagSettings *metadata = NULL; int avgBitRate = 0; int metadataHeight = 4; int row = height + 3; int col = indent; int visualizerWidth = calcVisualizerWidth(); if (refresh) { if (songdata) { metadata = songdata->metadata; } clearScreen(); printCoverCentered(songdata, &(state->uiSettings)); printBasicMetadata(row, col, visualizerWidth - 1, metadata, &(state->uiSettings)); refresh = false; } if (songdata) { ma_uint32 sampleRate; ma_format format; avgBitRate = songdata->avgBitRate; getCurrentFormatAndSampleRate(&format, &sampleRate); printTime(row + metadataHeight, col, elapsedSeconds, sampleRate, avgBitRate, state); } printVisualizer(row + metadataHeight + 2, col, visualizerWidth, settings, elapsedSeconds, state); calcAndPrintLastRowAndErrorRow(&(state)->uiSettings, settings); } void showTrackView(int width, int height, AppSettings *settings, SongData *songdata, AppState *state, double elapsedSeconds) { float aspect = getAspectRatio(); if (aspect == 0.0f) aspect = 1.0f; int correctedWidth = width / aspect; if (correctedWidth > height * 2) { showTrackViewLandscape(height, width, aspect, settings, songdata, state, elapsedSeconds); } else { showTrackViewPortrait(preferredHeight, settings, songdata, state, elapsedSeconds); } } 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(); 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; if (ui->trackTitleAsWindowTitle) setTerminalWindowTitle( songdata->metadata->title); } else { if (state->currentView == TRACK_VIEW) { state->currentView = LIBRARY_VIEW; clearScreen(); } ui->color.r = defaultColor; ui->color.g = defaultColor; ui->color.b = defaultColor; if (ui->trackTitleAsWindowTitle) setTerminalWindowTitle("kew"); } calcPreferredSize(ui); calcIndent(songdata); } int term_w, term_h; getTermSize(&term_w, &term_h); state->uiState.miniMode = false; 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, unshuffledPlaylist, &chosenRow, &(uis->chosenNodeId), state, settings); state->uiState.resetPlaylistDisplay = false; fflush(stdout); } else if (state->currentView == SEARCH_VIEW && refresh) { showSearch(songdata, &chosenSearchResultRow, ui, settings); refresh = false; fflush(stdout); } else if (state->currentView == LIBRARY_VIEW && refresh) { showLibrary(songdata, state, settings); fflush(stdout); } else if (state->currentView == TRACK_VIEW) { showTrackView(term_w, term_h, settings, 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/src/player_ui.h000066400000000000000000000042121507107350600146000ustar00rootroot00000000000000#ifndef PLAYER_H #define PLAYER_H #include #include "appstate.h" #include "common.h" #include "directorytree.h" #include "soundcommon.h" #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 extern int numProgressBars; extern bool fastForwarding; extern bool rewinding; extern int lastRowRow; extern int lastRowCol; extern int progressBarRow; extern int progressBarCol; extern int progressBarLength; extern int draggedProgressBarCol; extern double draggedPositionSeconds; extern bool draggingProgressBar; 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 getIndent(); 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 showTrack(void); void freeMainDirectoryTree(AppState *state); char *getLibraryFilePath(void); void resetChosenDir(void); void switchToNextView(void); void switchToPreviousView(void); void resetSearchResult(void); int getChosenRow(void); void setChosenRow(int row); #endif kew/src/playerops.c000066400000000000000000002200361507107350600146240ustar00rootroot00000000000000#include "playerops.h" #include "file.h" #include "player_ui.h" #include "search_ui.h" #include "settings.h" #include "songloader.h" #include "term.h" #include "theme.h" #include #include #include #include #include #include #include #include /* 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 #ifndef G_USEC_PER_SEC #define G_USEC_PER_SEC 1000000 #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 resetStartTime(void) { clock_gettime(CLOCK_MONOTONIC, &start_time); } void updatePlaybackPosition(double elapsedSeconds) { #ifndef __APPLE__ if (elapsedSeconds < 0.0) elapsedSeconds = 0.0; // Max safe seconds to avoid overflow when multiplied by 1,000,000 const double maxSeconds = (double)(LLONG_MAX / G_USEC_PER_SEC); if (elapsedSeconds > maxSeconds) elapsedSeconds = maxSeconds; 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__ if (newPositionSeconds < 0.0) newPositionSeconds = 0.0; const double maxSeconds = (double)(LLONG_MAX / G_USEC_PER_SEC); if (newPositionSeconds > maxSeconds) newPositionSeconds = maxSeconds; 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}")); GVariant *value_variant = g_variant_new_string(newValue); if (value_variant == NULL) { fprintf(stderr, "Failed to allocate GVariant for string property\n"); return; } g_variant_builder_add(&changed_properties_builder, "{sv}", propertyName, value_variant); GVariant *signal_variant = g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changed_properties_builder, NULL); if (signal_variant == NULL) { fprintf(stderr, "Failed to allocate GVariant for " "PropertiesChanged signal\n"); g_variant_builder_clear(&changed_properties_builder); return; } g_dbus_connection_emit_signal( connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", signal_variant, 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}")); GVariant *value_variant = g_variant_new_boolean(newValue); if (value_variant == NULL) { fprintf(stderr, "Failed to allocate GVariant for boolean property\n"); return; } g_variant_builder_add(&changed_properties_builder, "{sv}", propertyName, value_variant); GVariant *signal_variant = g_variant_new("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", &changed_properties_builder, NULL); if (signal_variant == NULL) { fprintf(stderr, "Failed to allocate GVariant for " "PropertiesChanged signal\n"); g_variant_builder_clear(&changed_properties_builder); return; } g_dbus_connection_emit_signal( connection, NULL, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged", signal_variant, 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(); } bool isValidAudioNode(Node *node) { if (!node) return false; if (node->id < 0) return false; if (!node->song.filePath || strnlen(node->song.filePath, MAXPATHLEN) == 0) return false; return true; } void play(Node *node) { if (!isValidAudioNode(node)) { fprintf(stderr, "Song is invalid.\n"); return; } currentSong = node; 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); return; } } 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) { resetClock(); if (currentSong != NULL) { seekPercentage(0); emitSeekedSignal(0.0); } } void prepareIfSkippedSilent(void) { if (hasSilentlySwitched) { skipping = true; hasSilentlySwitched = false; resetStartTime(); 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(UISettings *ui) { bool repeatEnabled = isRepeatEnabled(); bool repeatListEnabled = isRepeatListEnabled(); if (repeatEnabled) { setRepeatEnabled(false); setRepeatListEnabled(true); emitStringPropertyChanged("LoopStatus", "List"); ui->repeatState = 2; } else if (repeatListEnabled) { setRepeatEnabled(false); setRepeatListEnabled(false); emitStringPropertyChanged("LoopStatus", "None"); ui->repeatState = 0; } else { setRepeatEnabled(true); setRepeatListEnabled(false); emitStringPropertyChanged("LoopStatus", "Track"); ui->repeatState = 1; } if (appState.currentView != TRACK_VIEW) refresh = true; } void toggleNotifications(UISettings *ui, AppSettings *settings) { ui->allowNotifications = !ui->allowNotifications; c_strcpy(settings->allowNotifications, ui->allowNotifications ? "1" : "0", sizeof(settings->allowNotifications)); if (ui->allowNotifications) { clearScreen(); refresh = true; } } void toggleShuffle(UISettings *ui) { ui->shuffleEnabled = !isShuffleEnabled(); setShuffleEnabled(ui->shuffleEnabled); if (ui->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(unshuffledPlaylist, &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 strToLower(char *str) { if (str == NULL) return; for (; *str; ++str) { *str = tolower((unsigned char)*str); } } int loadTheme(AppState *appState, AppSettings *settings, const char *themeName, bool isAnsiTheme) { if (!appState || !themeName) return 0; char *configPath = getConfigPath(); if (!configPath) return 0; // Check if config directory exists struct stat st = {0}; if (stat(configPath, &st) == -1) { free(configPath); return 0; } // Build full theme filename char filename[NAME_MAX]; const char *extension = ".theme"; size_t themeNameLen = strlen(themeName); size_t extLen = strlen(extension); // Check if themeName already ends with ".theme" int hasExtension = (themeNameLen >= extLen && strcmp(themeName + themeNameLen - extLen, extension) == 0); if (hasExtension) { if (snprintf(filename, sizeof(filename), "%s", themeName) >= (int)sizeof(filename)) { fprintf(stderr, "Theme filename is too long\n"); setErrorMessage("Theme filename is too long"); free(configPath); return 0; } } else { if (snprintf(filename, sizeof(filename), "%s.theme", themeName) >= (int)sizeof(filename)) { fprintf(stderr, "Theme filename is too long\n"); setErrorMessage("Theme filename is too long"); free(configPath); return 0; } } // Build full themes directory path: configDir + "/themes" char themesDir[MAXPATHLEN]; if (snprintf(themesDir, sizeof(themesDir), "%s/themes", configPath) >= (int)sizeof(themesDir)) { fprintf(stderr, "Themes path is too long\n"); setErrorMessage("Themes path is too long"); free(configPath); return 0; } strToLower(filename); // Call the loader int loaded = loadThemeFromFile(themesDir, filename, &appState->uiSettings.theme); if (!loaded) { free(configPath); return 0; // failed to load } appState->uiSettings.themeIsSet = true; if (isAnsiTheme) { // Default ANSI theme: store in settings->ansiTheme snprintf(settings->ansiTheme, sizeof(settings->ansiTheme), "%s", themeName); } else { // Truecolor theme: store in settings->theme snprintf(settings->theme, sizeof(settings->theme), "%s", themeName); } free(configPath); return 1; } void cycleThemes(UISettings *ui, AppSettings *settings) { clearScreen(); char *configPath = getConfigPath(); if (!configPath) return; char themesPath[MAXPATHLEN]; snprintf(themesPath, sizeof(themesPath), "%s/themes", configPath); DIR *dir = opendir(themesPath); if (!dir) { perror("Failed to open themes directory"); return; } struct dirent *entry; char *themes[256]; int themeCount = 0; // Collect all *.theme files while ((entry = readdir(dir)) != NULL) { if (strstr(entry->d_name, ".theme")) { themes[themeCount++] = strdup(entry->d_name); } } closedir(dir); if (themeCount == 0) { setErrorMessage("No themes found."); free(configPath); return; } // Find the index of the current theme int currentIndex = -1; char currentFilename[NAME_MAX]; strncpy(currentFilename, ui->themeName, sizeof(currentFilename) - 1); currentFilename[sizeof(currentFilename) - 1] = '\0'; if (strlen(currentFilename) + 6 < sizeof(currentFilename)) strcat(currentFilename, ".theme"); for (int i = 0; i < themeCount; i++) { if (strcmp(themes[i], currentFilename) == 0) { currentIndex = i; break; } } // Get next theme (wrap around) int nextIndex = (currentIndex + 1) % themeCount; if (loadTheme(&appState, settings, themes[nextIndex], false)) { ui->colorMode = COLOR_MODE_THEME; snprintf(ui->themeName, sizeof(ui->themeName), "%s", themes[nextIndex]); char *dot = strstr(ui->themeName, ".theme"); if (dot) *dot = '\0'; } refresh = true; for (int i = 0; i < themeCount; i++) { free(themes[i]); } free(configPath); } void cycleColorMode(UISettings *ui) { clearScreen(); switch (ui->colorMode) { case COLOR_MODE_DEFAULT: ui->colorMode = COLOR_MODE_ALBUM; break; case COLOR_MODE_ALBUM: ui->colorMode = COLOR_MODE_THEME; break; case COLOR_MODE_THEME: ui->colorMode = COLOR_MODE_DEFAULT; break; } bool themeLoaded = false; switch (ui->colorMode) { case COLOR_MODE_DEFAULT: if (loadTheme(&appState, &settings, "default", true)) { themeLoaded = true; } break; case COLOR_MODE_ALBUM: themeLoaded = true; break; case COLOR_MODE_THEME: if (ui->themeName[0] != '\0' && loadTheme(&appState, &settings, ui->themeName, true)) { themeLoaded = true; } } if (!themeLoaded) { cycleColorMode(ui); } 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 markAsEnqueued(FileSystemEntry *root, char *path) { if (root == NULL) return false; if (path == NULL) return false; if (!root->isDirectory) { if (strcmp(root->fullPath, path) == 0) { root->isEnqueued = true; return true; } } else { FileSystemEntry *child = root->children; bool found = false; while (child != NULL) { found = markAsEnqueued(child, path); child = child->next; if (found) break; } if (found) { root->isEnqueued = true; return true; } } return false; } void markListAsEnqueued(FileSystemEntry *root, PlayList *playlist) { Node *node = playlist->head; for (int i = 0; i < playlist->count; i++) { if (node == NULL) break; if (node->song.filePath == NULL) break; markAsEnqueued(root, node->song.filePath); node = node->next; } root->isEnqueued = false; // Don't mark the absolute root } 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); if (addToList(unshuffledPlaylist, node) == -1) destroyNode(node); Node *node2 = NULL; createNode(&node2, child->fullPath, id); if (addToList(&playlist, node2) == -1) destroyNode(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 moveSongUp() { if (appState.currentView != PLAYLIST_VIEW) { return; } bool rebuild = false; int chosenRow = getChosenRow(); Node *node = findSelectedEntry(unshuffledPlaylist, 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(unshuffledPlaylist, 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(unshuffledPlaylist, 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(unshuffledPlaylist, node); Node *plNode = findSelectedEntryById(&playlist, node->id); if (!isShuffleEnabled()) moveDownList(&playlist, plNode); chosenRow++; chosenRow = (chosenRow >= unshuffledPlaylist->count) ? unshuffledPlaylist->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, unshuffledPlaylist); 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(unshuffledPlaylist, 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; } bool isContainedWithin(FileSystemEntry *entry, FileSystemEntry *containingEntry) { if (entry == NULL || containingEntry == NULL) return false; FileSystemEntry *tmp = entry->parent; while (tmp != NULL) { if (strcmp(tmp->fullPath, containingEntry->fullPath) == 0) return true; tmp = tmp->parent; } return false; } void autostartIfStopped(FileSystemEntry *firstEnqueuedEntry) { waitingForNext = true; audioData.endOfListReached = false; if (firstEnqueuedEntry != NULL) songToStartFrom = findPathInPlaylist(firstEnqueuedEntry->fullPath, &playlist); lastPlayedId = -1; } 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 && isContainedWithin(entry, chosenDir) && uis->allowChooseSongs == true) { // If the chosen directory is the same as the // entry's parent and it is open uis->openedSubDir = true; FileSystemEntry *tmpc = chosenDir->children; uis->numSongsAboveSubDir = 0; while (tmpc != NULL) { if (strcmp(entry->fullPath, tmpc->fullPath) == 0 || isContainedWithin(entry, tmpc)) 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) { autostartIfStopped(firstEnqueuedEntry); } if (shuffle) { shufflePlaylist(&playlist); songToStartFrom = NULL; } if (nextSongNeedsRebuilding) { reshufflePlaylist(); } return firstEnqueuedEntry; } void handleRemove(void) { if (appState.currentView == PLAYLIST_VIEW) { bool rebuild = false; Node *node = findSelectedEntry(unshuffledPlaylist, 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(unshuffledPlaylist, 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; } void addToFavoritesPlaylist(void) { if (currentSong == NULL) return; int id = currentSong->id; Node *node = NULL; if (findSelectedEntryById(favoritesPlaylist, id) != NULL) // Song is already in list return; createNode(&node, currentSong->song.filePath, id); addToList(favoritesPlaylist, node); } 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); else if (pathEndsWith(songData->filePath, "webm")) result = prepareNextWebmDecoder(songData); #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 (existsFile(filepath) >= 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 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; setSeekElapsed(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(unshuffledPlaylist, 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; setErrorMessage("Updating Library..."); 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); c_sleep(1000); // Don't refresh immediately or we risk the error message // not clearing 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(); setRawInputMode(); 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 = unshuffledPlaylist->head; while (nextInPlaylist != NULL) { nextInPlaylistID = nextInPlaylist->id; if (clearAll || nextInPlaylistID != currentID) { songToBeRemoved = nextInPlaylist; nextInPlaylist = nextInPlaylist->next; int id = songToBeRemoved->id; // Update Library if (songToBeRemoved != NULL) markAsDequeued(getLibrary(), songToBeRemoved->song.filePath); // Remove from Display playlist if (songToBeRemoved != NULL) deleteFromList(unshuffledPlaylist, songToBeRemoved); // Remove from Shuffle playlist Node *node2 = findSelectedEntryById(&playlist, id); if (node2 != NULL) deleteFromList(&playlist, node2); } else { 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/src/playerops.h000066400000000000000000000073541507107350600146370ustar00rootroot00000000000000 #ifndef PLAYEROPS_H #define PLAYEROPS_H #include "appstate.h" #include "playlist.h" #include "sound.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 resetStartTime(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(UISettings *ui); void toggleNotifications(UISettings *ui, AppSettings *settings); void toggleShuffle(UISettings *ui); void toggleAscii(AppSettings *settings, UISettings *ui); void cycleColorMode(UISettings *ui); void cycleThemes(UISettings *ui, AppSettings *settings); void toggleVisualizer(AppSettings *settings, UISettings *ui); void quit(void); int loadTheme(AppState *appState, AppSettings *settings, const char *themeName, bool isAnsiTheme); 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); 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 moveSongUp(); void moveSongDown(); void play(Node *song); void repeatList(); void skipToBegginningOfSong(void); void sortLibrary(void); void markListAsEnqueued(FileSystemEntry *root, PlayList *playlist); bool isContainedWithin(FileSystemEntry *entry, FileSystemEntry *containingEntry); void addToFavoritesPlaylist(void); void autostartIfStopped(FileSystemEntry *firstEnqueuedEntry); #endif kew/src/playlist.c000066400000000000000000000767031507107350600144610ustar00rootroot00000000000000#define _XOPEN_SOURCE 700 #define __USE_XOPEN_EXTENDED 1 #include "playlist.h" #include "file.h" #include "utils.h" #include #include #include #include #include #include /* playlist.c Playlist related functions. */ #define MAX_SEARCH_SIZE 256 #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif const char PLAYLIST_EXTENSIONS[] = "(m3u8?)$"; const char favoritesPlaylistName[] = "kew favorites.m3u"; const char lastUsedPlaylistName[] = "lastPlaylist.m3u"; // The playlist unshuffled as it appears in playlist view PlayList *unshuffledPlaylist = NULL; // The (sometimes shuffled) sequence of songs that will be played PlayList playlist = {NULL, NULL, 0, PTHREAD_MUTEX_INITIALIZER}; // The playlist from kew favorites .m3u PlayList *favoritesPlaylist = 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; } int addToList(PlayList *list, Node *newNode) { if (newNode == NULL) return 0; if (list->count >= MAX_FILES) return -1; 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; } return 0; } 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 == NULL || node == NULL || list->head == NULL) return NULL; Node *nextNode = node->next; // Adjust head and tail if (list->head == node) list->head = nextNode; if (list->tail == node) list->tail = node->prev; // Fix neighbors if (node->prev != NULL) node->prev->next = nextNode; if (nextNode != NULL) nextNode->prev = node->prev; // Free song file path string if allocated if (node->song.filePath != NULL) { free(node->song.filePath); node->song.filePath = NULL; } free(node); node = NULL; 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 } // Check for overflow before malloc if ((size_t)playlist->count > SIZE_MAX / sizeof(Node *)) { printf("Playlist too large to allocate.\n"); // atexit() will free up resources properly exit(1); } // Convert the linked list to an array Node **nodes = (Node **)malloc(playlist->count * sizeof(Node *)); if (nodes == NULL) { printf("Memory allocation error.\n"); // atexit() will free up resources properly exit(1); } 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 destroyNode(Node *node) { if (node == NULL) return; if (node->song.filePath != NULL) free(node->song.filePath); free(node); } void exitIfOverflow(int counter) { if (counter == INT_MAX) { fprintf(stderr, "Error: Node ID overflow. Max node limit reached.\n"); exit(1); } } void buildPlaylistRecursive(const char *directoryPath, const char *allowedExtensions, PlayList *playlist) { int res = isDirectory(directoryPath); if (res != 1 && res != -1 && directoryPath != NULL) { Node *node = NULL; exitIfOverflow(nodeIdCounter); createNode(&node, directoryPath, nodeIdCounter++); if (addToList(playlist, node) == -1) destroyNode(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) == 1) { 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; exitIfOverflow(nodeIdCounter); createNode(&node, filePath, nodeIdCounter++); if (addToList(playlist, node) == -1) destroyNode(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, FileSystemEntry *library) { 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); } if (songPath == NULL) continue; if (existsFile(songPath) < 0) continue; // Don't add songs that are already enqueued if (library != NULL) { FileSystemEntry *entry = findCorrespondingEntry(library, songPath); if (entry != NULL && entry->isEnqueued) { g_free(songPath); continue; } } 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 = MUSIC_FILE_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) == 0) { if (strcmp(argv[1], "list") == 0) { readM3UFile(buf, &playlist, NULL); } 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, const 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 loadPlaylist(const char *directory, const char *playlistName, PlayList *playlist) { char playlistPath[MAXPATHLEN]; size_t len = strnlen(directory, MAXPATHLEN); if (len <= 0) return; if (directory[len - 1] == '/') { snprintf(playlistPath, sizeof(playlistPath), "%s%s", directory, playlistName); } else { snprintf(playlistPath, sizeof(playlistPath), "%s/%s", directory, playlistName); } if (playlist == NULL) { printf("Failed to allocate memory for special playlist.\n"); exit(0); } playlist->count = 0; playlist->head = NULL; playlist->tail = NULL; readM3UFile(playlistPath, playlist, NULL); } void loadFavoritesPlaylist(const char *directory) { favoritesPlaylist = malloc(sizeof(PlayList)); loadPlaylist(directory, favoritesPlaylistName, favoritesPlaylist); } void loadLastUsedPlaylist(void) { char *configdir = getConfigPath(); loadPlaylist(configdir, lastUsedPlaylistName, &playlist); if (unshuffledPlaylist == NULL) { unshuffledPlaylist = malloc(sizeof(PlayList)); *unshuffledPlaylist = deepCopyPlayList(&playlist); } if (configdir) free(configdir); } void saveNamedPlaylist(const char *directory, const char *playlistName, const PlayList *playlist) { 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", playlistName); } else { snprintf(playlistPath + length, sizeof(playlistPath) - length, "%s", playlistName); } if (playlist != NULL) { writeM3UFile(playlistPath, playlist); } } void saveFavoritesPlaylist(const char *directory) { if (favoritesPlaylist != NULL && favoritesPlaylist->count > 0) { saveNamedPlaylist(directory, favoritesPlaylistName, favoritesPlaylist); } } void saveLastUsedPlaylist(void) { char *configdir = getConfigPath(); saveNamedPlaylist(configdir, lastUsedPlaylistName, unshuffledPlaylist); if (configdir) free(configdir); } void savePlaylist(const char *path, const PlayList *playlist) { if (path == NULL) { return; } if (playlist->head == NULL || playlist->head->song.filePath == NULL) return; writeM3UFile(path, playlist); } void exportCurrentPlaylist(const char *path) { char m3uFilename[MAXPATHLEN]; if (path == NULL || playlist.head == NULL) return; generateM3UFilename(path, playlist.head->song.filePath, m3uFilename, sizeof(m3uFilename)); savePlaylist(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); if (addToList(list, newNode) == -1) destroyNode(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) { if (filename == NULL) return 0; const char *extensions[] = {".m4a", ".aac", ".mp3", ".ogg", ".flac", ".wav", ".opus", ".webm"}; 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/src/playlist.h000066400000000000000000000045641507107350600144620ustar00rootroot00000000000000#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef __USE_XOPEN_EXTENDED #define __USE_XOPEN_EXTENDED #endif #include #include #include "directorytree.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 *unshuffledPlaylist; extern PlayList *favoritesPlaylist; extern int nodeIdCounter; Node *getListNext(Node *node); Node *getListPrev(Node *node); void createNode(Node **node, const char *directoryPath, int id); int addToList(PlayList *list, Node *newNode); Node *deleteFromList(PlayList *list, Node *node); void deletePlaylist(PlayList *playlist); void destroyNode(Node *node); 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, const PlayList *playlist); void saveNamedPlaylist(const char *directory, const char *playlistName, const PlayList *playlist); void exportCurrentPlaylist(const char *path); void saveLastUsedPlaylist(void); void loadLastUsedPlaylist(void); 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); void loadFavoritesPlaylist(const char *directory); void saveFavoritesPlaylist(const char *directory); int isMusicFile(const char *filename); void readM3UFile(const char *filename, PlayList *playlist, FileSystemEntry *library); kew/src/playlist_ui.c000066400000000000000000000210151507107350600151400ustar00rootroot00000000000000#include "playlist_ui.h" #include "common_ui.h" #include "songloader.h" #include "term.h" #include "utils.h" #include /* playlist_ui.c Playlist UI functions. */ int startIter = 0; int previousChosenSong = 0; #define MAX_TERM_WIDTH 1000 Node *determineStartNode(Node *head, int *foundAt, int listSize) { if (foundAt == NULL) { return head; } 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++; } return foundNode ? foundNode : head; } void preparePlaylistString(Node *node, char *buffer, int bufferSize) { if (node == NULL || buffer == NULL || node->song.filePath == NULL || bufferSize <= 0) { if (buffer && bufferSize > 0) buffer[0] = '\0'; return; } if (strnlen(node->song.filePath, MAXPATHLEN) >= MAXPATHLEN) { 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 && lastSlash < filePath + len) { c_strcpy(buffer, lastSlash + 1, bufferSize); buffer[bufferSize - 1] = '\0'; } else { // If no slash found or invalid pointer arithmetic, just copy // whole path safely or clear c_strcpy(buffer, filePath, bufferSize); buffer[bufferSize - 1] = '\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; if (termWidth < 0 || termWidth > MAX_TERM_WIDTH || indent < 0 || indent >= termWidth) return 0; int maxNameWidth = termWidth - indent - 12; if (maxNameWidth <= 0) return 0; PixelData rowColor = {defaultColor, defaultColor, defaultColor}; char *buffer = malloc(NAME_MAX + 1); if (!buffer) return 0; char *filename = malloc(NAME_MAX + 1); if (!filename) { free(buffer); return 0; } for (int i = startIter; node != NULL && i < startIter + maxListSize; i++) { if (!(ui->color.r == defaultColor && ui->color.g == defaultColor && ui->color.b == defaultColor)) rowColor = getGradientColor(ui->color, i - startIter, maxListSize, maxListSize / 2, 0.7f); preparePlaylistString(node, buffer, NAME_MAX); if (buffer[0] != '\0') { applyColor(ui->colorMode, ui->theme.playlist_rownum, rowColor); clearLine(); printBlankSpaces(indent); printf(" %d. ", i + 1); applyColor(ui->colorMode, ui->theme.playlist_title, rowColor); isSameNameAsLastTime = (previousChosenSong == chosenSong); if (!isSameNameAsLastTime) { resetNameScroll(); } filename[0] = '\0'; if (i == chosenSong) { previousChosenSong = chosenSong; *chosenNodeId = node->id; processNameScroll(buffer, filename, maxNameWidth, isSameNameAsLastTime); inverseText(); } else { processName(buffer, filename, maxNameWidth, true, true); } if (currentSong != NULL && currentSong->id == node->id) applyColor(ui->colorMode, ui->theme.playlist_playing, rowColor); if (i + 1 < 10) printf(" "); if (currentSong != NULL && currentSong->id == node->id && i == chosenSong) { inverseText(); } if (currentSong != NULL && currentSong->id == node->id && i != chosenSong) { printf("\e[4m"); } printf("%s\n", filename); numPrintedRows++; } node = node->next; resetColor(); } free(buffer); free(filename); return numPrintedRows; } void ensureChosenSongWithinLimits(int *chosenSong, PlayList *list) { if (*chosenSong >= list->count) { *chosenSong = list->count - 1; } if (*chosenSong < 0) { *chosenSong = 0; } } int determinePlaylistStart(int previousStartIter, int foundAt, int maxListSize, int *chosenSong, bool reset, bool endOfListReached) { int startIter = 0; startIter = (foundAt > -1 && (foundAt > startIter + maxListSize)) ? foundAt : startIter; if (previousStartIter <= foundAt && foundAt < previousStartIter + maxListSize) startIter = previousStartIter; if (*chosenSong < startIter) { startIter = *chosenSong; } if (*chosenSong > startIter + maxListSize - round(maxListSize / 2)) { startIter = *chosenSong - maxListSize + round(maxListSize / 2); } if (reset && !endOfListReached) { if (foundAt > maxListSize) startIter = previousStartIter = *chosenSong = foundAt; else startIter = *chosenSong = 0; } return startIter; } void moveStartNodeIntoPosition(int foundAt, Node **startNode) { // Go up to adjust the startNode 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 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; Node *startNode = determineStartNode(list->head, &foundAt, list->count); ensureChosenSongWithinLimits(chosenSong, list); startIter = determinePlaylistStart(startIter, foundAt, maxListSize, chosenSong, reset, audioData.endOfListReached); moveStartNodeIntoPosition(foundAt, &startNode); int printedRows = displayPlaylistItems(startNode, startIter, maxListSize, termWidth, indent, *chosenSong, chosenNodeId, ui); while (printedRows <= maxListSize) { clearLine(); printf("\n"); printedRows++; } return printedRows; } kew/src/playlist_ui.h000066400000000000000000000004461507107350600151520ustar00rootroot00000000000000#ifndef PLAYLIST_UI_H #define PLAYLIST_UI_H #include "appstate.h" #include "playlist.h" #include "soundcommon.h" int displayPlaylist(PlayList *list, int maxListSize, int indent, int *chosenSong, int *chosenNodeId, bool reset, AppState *state); #endif kew/src/search_ui.c000066400000000000000000000363011507107350600145500ustar00rootroot00000000000000#include "search_ui.h" #include "appstate.h" #include "common.h" #include "common_ui.h" #include "playlist.h" #include "soundcommon.h" #include "term.h" #include #include /* search_ui.c Search UI functions. */ #define MAX_SEARCH_LEN 32 int numSearchLetters = 0; int numSearchBytes = 0; typedef struct SearchResult { FileSystemEntry *entry; struct FileSystemEntry *parent; int distance; int groupDistance; } SearchResult; // Global variables to store results SearchResult *results = NULL; size_t resultsCount = 0; size_t resultsCapacity = 0; size_t terminalHeight = 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; } #define GROW_MARGIN 50 void reallocResults() { if (resultsCount >= resultsCapacity) { resultsCapacity = resultsCapacity == 0 ? 10 + GROW_MARGIN : resultsCapacity + GROW_MARGIN; results = realloc(results, resultsCapacity * sizeof(SearchResult)); } } void setResultFields(FileSystemEntry *entry, int distance, FileSystemEntry *parent) { results[resultsCount].distance = distance; results[resultsCount].entry = entry; results[resultsCount].parent = parent; } bool isDuplicate(const FileSystemEntry *entry) { for (size_t i = 0; i < resultsCount; i++) { const FileSystemEntry *other = results[i].entry; if (!entry->isDirectory) return false; if (entry == other) return true; } return false; } // Function to add a result to the global array void addResult(FileSystemEntry *entry, int distance) { if (numSearchLetters < minSearchLetters) return; if (resultsCount > terminalHeight * 10) return; if (entry->parent == NULL) // Root return; if (isDuplicate(entry)) return; reallocResults(); setResultFields(entry, distance, NULL); resultsCount++; if (entry->isDirectory) { if (entry->children && entry->parent != NULL && entry->parent->parent == NULL) { FileSystemEntry *child = entry->children; while (child) { if (child->isDirectory) { if (resultsCount > terminalHeight * 10) break; reallocResults(); setResultFields(child, distance, entry); resultsCount++; } child = child->next; } } } } // 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 calculateGroupDistances(void) { for (size_t i = 0; i < resultsCount; i++) { // Find top-level parent (entry with no parent, or root) FileSystemEntry *root = results[i].entry; while (root->parent != NULL) { root = root->parent; } // Find if this root appears in results, and use ITS distance // Otherwise use minimum distance among descendants int minDist = results[i].distance; for (size_t j = 0; j < resultsCount; j++) { FileSystemEntry *otherRoot = results[j].entry; while (otherRoot->parent != NULL) { otherRoot = otherRoot->parent; } if (otherRoot == root) { if (results[j].entry == root) { // The root itself is in results - use // its distance minDist = results[j].distance; break; } if (results[j].distance < minDist) { minDist = results[j].distance + 1; } } } // If root is in results, use only its distance for grouping // Otherwise use the best child distance results[i].groupDistance = minDist; } } static int ancestorCompare(const FileSystemEntry *A, const FileSystemEntry *B) { for (const FileSystemEntry *p = B->parent; p; p = p->parent) if (p == A) return -1; for (const FileSystemEntry *p = A->parent; p; p = p->parent) if (p == B) return 1; return 0; } int compareResults(const void *a, const void *b) { const SearchResult *A = a; const SearchResult *B = b; int rel = ancestorCompare(A->entry, B->entry); if (rel != 0) return rel; // Sort by best distance in the top-level group first if (A->groupDistance != B->groupDistance) return (A->groupDistance < B->groupDistance) ? -1 : 1; // If different parents, compare by hierarchy (path) if (A->entry->parent != B->entry->parent) { const FileSystemEntry *pA = A->entry; const FileSystemEntry *pB = B->entry; // Walk up to same depth int depthA = 0, depthB = 0; for (const FileSystemEntry *p = pA; p->parent; p = p->parent) depthA++; for (const FileSystemEntry *p = pB; p->parent; p = p->parent) depthB++; while (depthA > depthB) { pA = pA->parent; depthA--; } while (depthB > depthA) { pB = pB->parent; depthB--; } // Walk up together to find where they diverge while (pA->parent != pB->parent) { pA = pA->parent; pB = pB->parent; } // Compare by name at divergence point int cmp = strcmp(pA->name, pB->name); if (cmp != 0) return cmp; } // Within same parent: directories first if (A->entry->isDirectory != B->entry->isDirectory) return A->entry->isDirectory ? -1 : 1; // Then by individual distance if (A->distance != B->distance) return (A->distance < B->distance) ? -1 : 1; // Then by name int cmp = strcmp(A->entry->name, B->entry->name); if (cmp != 0) return cmp; return (A->entry < B->entry) ? -1 : (A->entry > B->entry); } void sortSearchResults(void) { calculateGroupDistances(); qsort(results, resultsCount, sizeof(SearchResult), compareResults); } void fuzzySearch(FileSystemEntry *root, int threshold) { int term_w, term_h; getTermSize(&term_w, &term_h); terminalHeight = term_h; freeSearchResults(); if (numSearchLetters > minSearchLetters) { fuzzySearchRecursive(root, searchText, threshold, collectResult); } sortSearchResults(); refresh = true; } int displaySearchBox(int indent, UISettings *ui) { applyColor(ui->colorMode, ui->theme.search_label, ui->color); clearLine(); printBlankSpaces(indent); printf(" [Search]: "); applyColor(ui->colorMode, ui->theme.search_query, defaultColorRGB); // Save cursor position printf("%s", searchText); printf("\033[s"); printf("â–ˆ\n"); return 0; } int addToSearchText(const char *str, UISettings *ui) { 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 } applyColor(ui->colorMode, ui->theme.search_label, ui->color); // 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("â–ˆ"); clearRestOfLine(); fflush(stdout); // Remove the character from the buffer numSearchBytes -= lastCharBytes; searchText[numSearchBytes] = '\0'; numSearchLetters--; return 0; } void applyColorAndFormat(bool isChosen, FileSystemEntry *entry, UISettings *ui, bool isPlaying) { if (isChosen) { currentSearchEntry = entry; if (entry->isEnqueued) { applyColor(ui->colorMode, isPlaying ? ui->theme.search_playing : ui->theme.search_enqueued, ui->color); printf("\x1b[7m * "); } else { printf(" \x1b[7m "); } } else { if (entry->isEnqueued) { applyColor(ui->colorMode, isPlaying ? ui->theme.search_playing : ui->theme.search_enqueued, ui->color); printf(" * "); } else printf(" "); } } 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; 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; clearLine(); printf("\n"); printedRows++; int nameWidth = maxNameWidth; int extraIndent = 0; for (size_t i = startSearchIter; i < resultsCount; i++) { if ((int)i >= maxListSize + startSearchIter - 1) break; applyColor(ui->colorMode, ui->theme.search_result, defaultColorRGB); clearLine(); // Indent sub dirs if (results[i].parent != NULL) extraIndent = 2; else if (!results[i].entry->isDirectory) extraIndent = 4; else extraIndent = 0; nameWidth = maxNameWidth - extraIndent; printBlankSpaces(indent + extraIndent); bool isChosen = (*chosenRow == (int)i); bool isCurrentSong = currentSong != NULL && strcmp(currentSong->song.filePath, results[i].entry->fullPath) == 0; applyColorAndFormat(isChosen, results[i].entry, ui, isCurrentSong); name[0] = '\0'; if (results[i].entry->isDirectory) { snprintf(name, nameWidth + 1, "[%s]", results[i].entry->name); } else { snprintf(name, nameWidth + 1, "%s", results[i].entry->name); } printf("%s\n", name); printedRows++; } applyColor(ui->colorMode, ui->theme.help, defaultColorRGB); while (printedRows < maxListSize) { clearLine(); 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/src/search_ui.h000066400000000000000000000006301507107350600145510ustar00rootroot00000000000000#include "appstate.h" #include "directorytree.h" int displaySearch(int maxListSize, int indent, int *chosenRow, int startSearchIter, UISettings *ui); int addToSearchText(const char *str, UISettings *ui); int removeFromSearchText(void); int getSearchResultsCount(void); void fuzzySearch(FileSystemEntry *root, int threshold); void freeSearchResults(void); FileSystemEntry *getCurrentSearchEntry(void); kew/src/settings.c000066400000000000000000002131421507107350600144460ustar00rootroot00000000000000#include "settings.h" #include "appstate.h" #include "file.h" #include "player_ui.h" #include "soundcommon.h" #include "utils.h" #include #include #include #include #include #include #include #include #include /* 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) { if (pairs == NULL || count <= 0) { return; } for (size_t i = 0; i < (size_t)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.replayGainCheckFirst, "0", sizeof(settings.replayGainCheckFirst)); c_strcpy(settings.visualizerBarWidth, "2", sizeof(settings.visualizerBarWidth)); c_strcpy(settings.visualizerBrailleMode, "0", sizeof(settings.visualizerBrailleMode)); c_strcpy(settings.progressBarElapsedEvenChar, "â”", sizeof(settings.progressBarElapsedEvenChar)); c_strcpy(settings.progressBarElapsedOddChar, "â”", sizeof(settings.progressBarElapsedOddChar)); c_strcpy(settings.progressBarApproachingEvenChar, "â”", sizeof(settings.progressBarApproachingEvenChar)); c_strcpy(settings.progressBarApproachingOddChar, "â”", sizeof(settings.progressBarApproachingOddChar)); c_strcpy(settings.progressBarCurrentEvenChar, "â”", sizeof(settings.progressBarCurrentEvenChar)); c_strcpy(settings.progressBarCurrentOddChar, "â”", sizeof(settings.progressBarCurrentOddChar)); c_strcpy(settings.saveRepeatShuffleSettings, "1", sizeof(settings.saveRepeatShuffleSettings)); c_strcpy(settings.trackTitleAsWindowTitle, "1", sizeof(settings.trackTitleAsWindowTitle)); #ifdef __APPLE__ // Visualizer looks wonky in default terminal but let's enable it // anyway. People need to switch c_strcpy(settings.visualizerEnabled, "1", sizeof(settings.visualizerEnabled)); c_strcpy(settings.colorMode, "0", sizeof(settings.colorMode)); #else c_strcpy(settings.visualizerEnabled, "1", sizeof(settings.visualizerEnabled)); c_strcpy(settings.colorMode, "1", sizeof(settings.colorMode)); #endif #ifdef __ANDROID__ c_strcpy(settings.hideLogo, "1", sizeof(settings.hideLogo)); #else c_strcpy(settings.hideLogo, "0", sizeof(settings.hideLogo)); #endif c_strcpy(settings.hideHelp, "0", sizeof(settings.hideHelp)); c_strcpy(settings.cacheLibrary, "-1", sizeof(settings.cacheLibrary)); c_strcpy(settings.visualizerHeight, "6", sizeof(settings.visualizerHeight)); c_strcpy(settings.visualizerColorType, "2", 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.switchNumberedSong, "G", sizeof(settings.switchNumberedSong)); c_strcpy(settings.cycleColorsDerivedFrom, "i", sizeof(settings.cycleColorsDerivedFrom)); c_strcpy(settings.cycleThemes, "t", sizeof(settings.cycleThemes)); c_strcpy(settings.toggleNotifications, "n", sizeof(settings.toggleNotifications)); 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.addToFavoritesPlaylist, ".", sizeof(settings.addToFavoritesPlaylist)); 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.hardShowKeys, "[17~", sizeof(settings.hardShowKeys)); c_strcpy(settings.hardShowKeysAlt, "[17~", sizeof(settings.hardShowKeysAlt)); #if defined(__ANDROID__) || defined(__APPLE__) c_strcpy(settings.showPlaylistAlt, "Z", sizeof(settings.showPlaylistAlt)); c_strcpy(settings.showTrackAlt, "C", sizeof(settings.showTrackAlt)); c_strcpy(settings.showLibraryAlt, "X", sizeof(settings.showLibraryAlt)); c_strcpy(settings.showSearchAlt, "V", sizeof(settings.showSearchAlt)); c_strcpy(settings.showKeysAlt, "B", sizeof(settings.showKeysAlt)); #endif c_strcpy(settings.hardShowTrack, "OS", sizeof(settings.hardShowTrack)); c_strcpy(settings.hardShowTrackAlt, "[[D", sizeof(settings.hardShowTrackAlt)); c_strcpy(settings.hardShowLibrary, "OR", sizeof(settings.hardShowLibrary)); c_strcpy(settings.hardShowLibraryAlt, "[[C", sizeof(settings.hardShowLibraryAlt)); c_strcpy(settings.hardShowSearch, "[15~", sizeof(settings.hardShowSearch)); c_strcpy(settings.hardShowSearchAlt, "[[E", sizeof(settings.hardShowSearchAlt)); c_strcpy(settings.nextPage, "[6~", sizeof(settings.nextPage)); c_strcpy(settings.prevPage, "[5~", sizeof(settings.prevPage)); c_strcpy(settings.hardRemove, "[3~", sizeof(settings.hardRemove)); c_strcpy(settings.hardRemove2, "[P", sizeof(settings.hardRemove2)); c_strcpy(settings.mouseLeftClick, "[<0", sizeof(settings.mouseLeftClick)); c_strcpy(settings.mouseMiddleClick, "[<1", sizeof(settings.mouseMiddleClick)); c_strcpy(settings.mouseRightClick, "[<2", sizeof(settings.mouseRightClick)); c_strcpy(settings.mouseScrollUp, "[<64", sizeof(settings.mouseScrollUp)); c_strcpy(settings.mouseScrollDown, "[<65", sizeof(settings.mouseScrollDown)); c_strcpy(settings.mouseAltScrollUp, "[<72", sizeof(settings.mouseAltScrollUp)); c_strcpy(settings.mouseAltScrollDown, "[<73", 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, "f", sizeof(settings.moveSongUp)); c_strcpy(settings.moveSongDown, "g", sizeof(settings.moveSongDown)); c_strcpy(settings.enqueueAndPlay, "^M", sizeof(settings.enqueueAndPlay)); 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.altQuit, "\x1B", sizeof(settings.altQuit)); c_strcpy(settings.hardClearPlaylist, "\b", sizeof(settings.hardClearPlaylist)); memcpy(settings.ansiTheme, "default", 8); if (pairs == NULL) { return settings; } bool foundCycleThemesSetting = false; 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, "theme") == 0) { snprintf(settings.theme, sizeof(settings.theme), "%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.cycleColorsDerivedFrom, sizeof(settings.cycleColorsDerivedFrom), "%s", pair->value); } else if (strcmp(lowercaseKey, "cyclethemes") == 0) { snprintf(settings.cycleThemes, sizeof(settings.cycleThemes), "%s", pair->value); foundCycleThemesSetting = true; } else if (strcmp(lowercaseKey, "togglenotifications") == 0) { snprintf(settings.toggleNotifications, sizeof(settings.toggleNotifications), "%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, "addtofavoritesplaylist") == 0) { snprintf(settings.addToFavoritesPlaylist, sizeof(settings.addToFavoritesPlaylist), "%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, "colormode") == 0) { snprintf(settings.colorMode, sizeof(settings.colorMode), "%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, "repeatstate") == 0) { snprintf(settings.repeatState, sizeof(settings.repeatState), "%s", pair->value); } else if (strcmp(lowercaseKey, "shuffleenabled") == 0) { snprintf(settings.shuffleEnabled, sizeof(settings.shuffleEnabled), "%s", pair->value); } else if (strcmp(lowercaseKey, "saverepeatshufflesettings") == 0) { snprintf(settings.saveRepeatShuffleSettings, sizeof(settings.saveRepeatShuffleSettings), "%s", pair->value); } else if (strcmp(lowercaseKey, "tracktitleaswindowtitle") == 0) { snprintf(settings.trackTitleAsWindowTitle, sizeof(settings.trackTitleAsWindowTitle), "%s", pair->value); } else if (strcmp(lowercaseKey, "replaygaincheckfirst") == 0) { snprintf(settings.replayGainCheckFirst, sizeof(settings.replayGainCheckFirst), "%s", pair->value); } else if (strcmp(lowercaseKey, "visualizerbarwidth") == 0) { snprintf(settings.visualizerBarWidth, sizeof(settings.visualizerBarWidth), "%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, "altquit") == 0) { snprintf(settings.altQuit, sizeof(settings.altQuit), "%s", pair->value); } else if (strcmp(lowercaseKey, "prevpage") == 0) { snprintf(settings.prevPage, sizeof(settings.prevPage), "%s", pair->value); } else if (strcmp(lowercaseKey, "nextpage") == 0) { snprintf(settings.nextPage, sizeof(settings.nextPage), "%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, "progressbarelapsedevenchar") == 0) { if (strcmp(pair->value, "") != 0) snprintf( settings.progressBarElapsedEvenChar, sizeof(settings.progressBarElapsedEvenChar), "%s", pair->value); } else if (strcmp(lowercaseKey, "progressbarelapsedoddchar") == 0) { if (strcmp(pair->value, "") != 0) snprintf( settings.progressBarElapsedOddChar, sizeof(settings.progressBarElapsedOddChar), "%s", pair->value); } else if (strcmp(lowercaseKey, "progressbarapproachingevenchar") == 0) { if (strcmp(pair->value, "") != 0) snprintf( settings.progressBarApproachingEvenChar, sizeof(settings .progressBarApproachingEvenChar), "%s", pair->value); } else if (strcmp(lowercaseKey, "progressbarapproachingoddchar") == 0) { if (strcmp(pair->value, "") != 0) snprintf( settings.progressBarApproachingOddChar, sizeof( settings.progressBarApproachingOddChar), "%s", pair->value); } else if (strcmp(lowercaseKey, "progressbarcurrentevenchar") == 0) { if (strcmp(pair->value, "") != 0) snprintf( settings.progressBarCurrentEvenChar, sizeof(settings.progressBarCurrentEvenChar), "%s", pair->value); } else if (strcmp(lowercaseKey, "progressbarcurrentoddchar") == 0) { if (strcmp(pair->value, "") != 0) snprintf( settings.progressBarCurrentOddChar, sizeof(settings.progressBarCurrentOddChar), "%s", pair->value); } else if (strcmp(lowercaseKey, "showkeysalt") == 0 && strcmp(pair->value, "N") != 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); if (!foundCycleThemesSetting) { // moveSongUp is no longer t, it needs to be changed c_strcpy(settings.moveSongUp, "f", sizeof(settings.moveSongUp)); } 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 *comment = strchr(line, '#'); // Remove comments if (comment != NULL) *comment = '\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->altQuit, 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->cycleColorsDerivedFrom, EVENT_CYCLECOLORMODE}; mappings[19] = (EventMapping){settings->addToFavoritesPlaylist, EVENT_ADDTOFAVORITESPLAYLIST}; 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->nextPage, EVENT_NEXTPAGE}; mappings[43] = (EventMapping){settings->prevPage, 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->moveSongUp, EVENT_MOVESONGUP}; mappings[57] = (EventMapping){settings->moveSongDown, EVENT_MOVESONGDOWN}; mappings[58] = (EventMapping){settings->enqueueAndPlay, EVENT_ENQUEUEANDPLAY}; mappings[59] = (EventMapping){settings->hardStop, EVENT_STOP}; mappings[60] = (EventMapping){settings->sortLibrary, EVENT_SORTLIBRARY}; mappings[61] = (EventMapping){settings->cycleThemes, EVENT_CYCLETHEMES}; mappings[62] = (EventMapping){settings->toggleNotifications, EVENT_TOGGLENOTIFICATIONS}; } 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; } int getBytesInFirstChar(const char *str) { if (str == NULL || str[0] == '\0') { return 0; } mbstate_t state; memset(&state, 0, sizeof(state)); wchar_t wc; int numBytes = mbrtowc(&wc, str, MB_CUR_MAX, &state); return numBytes; } 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 = strnlen(tmp, MAXPATHLEN); 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(); setlocale(LC_ALL, ""); // 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); int tmpNumBytes = getBytesInFirstChar(settings->progressBarElapsedEvenChar); if (tmpNumBytes != 0) settings->progressBarElapsedEvenChar[tmpNumBytes] = '\0'; tmpNumBytes = getBytesInFirstChar(settings->progressBarElapsedOddChar); if (tmpNumBytes != 0) settings->progressBarElapsedOddChar[tmpNumBytes] = '\0'; tmpNumBytes = getBytesInFirstChar(settings->progressBarApproachingEvenChar); if (tmpNumBytes != 0) settings->progressBarApproachingEvenChar[tmpNumBytes] = '\0'; tmpNumBytes = getBytesInFirstChar(settings->progressBarApproachingOddChar); if (tmpNumBytes != 0) settings->progressBarApproachingOddChar[tmpNumBytes] = '\0'; tmpNumBytes = getBytesInFirstChar(settings->progressBarCurrentEvenChar); if (tmpNumBytes != 0) settings->progressBarCurrentEvenChar[tmpNumBytes] = '\0'; tmpNumBytes = getBytesInFirstChar(settings->progressBarCurrentOddChar); if (tmpNumBytes != 0) settings->progressBarCurrentOddChar[tmpNumBytes] = '\0'; ui->allowNotifications = (settings->allowNotifications[0] == '1'); ui->coverEnabled = (settings->coverEnabled[0] == '1'); ui->coverAnsi = (settings->coverAnsi[0] == '1'); ui->visualizerEnabled = (settings->visualizerEnabled[0] == '1'); bool useConfigColors = (settings->useConfigColors[0] == '1'); ui->quitAfterStopping = (settings->quitAfterStopping[0] == '1'); ui->hideGlimmeringText = (settings->hideGlimmeringText[0] == '1'); ui->mouseEnabled = (settings->mouseEnabled[0] == '1'); ui->shuffleEnabled = (settings->shuffleEnabled[0] == '1'); ui->visualizerBrailleMode = (settings->visualizerBrailleMode[0] == '1'); ui->hideLogo = (settings->hideLogo[0] == '1'); ui->hideHelp = (settings->hideHelp[0] == '1'); ui->saveRepeatShuffleSettings = (settings->saveRepeatShuffleSettings[0] == '1'); ui->trackTitleAsWindowTitle = (settings->trackTitleAsWindowTitle[0] == '1'); int tmp = getNumber(settings->repeatState); if (tmp >= 0) ui->repeatState = tmp; tmp = getNumber(settings->colorMode); if (tmp >= 0 && tmp < 3) { ui->colorMode = tmp; } else { if (useConfigColors) ui->colorMode = COLOR_MODE_DEFAULT; else ui->colorMode = COLOR_MODE_ALBUM; } tmp = getNumber(settings->replayGainCheckFirst); if (tmp >= 0) ui->replayGainCheckFirst = 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->visualizerBarWidth); if (tmp >= 0) ui->visualizerBarWidth = tmp; tmp = getNumber(settings->visualizerColorType); if (tmp >= 0) ui->visualizerColorType = 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; getMusicLibraryPath(settings->path); snprintf(ui->themeName, sizeof(ui->themeName), "%s", settings->theme); free(configdir); } void setConfig(AppSettings *settings, UISettings *ui) { // Create the file path char *configdir = getConfigPath(); char *filepath = getConfigFilePath(configdir); setlocale(LC_ALL, ""); 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->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->trackTitleAsWindowTitle[0] == '\0') ui->trackTitleAsWindowTitle ? c_strcpy(settings->trackTitleAsWindowTitle, "1", sizeof(settings->trackTitleAsWindowTitle)) : c_strcpy(settings->trackTitleAsWindowTitle, "0", sizeof(settings->trackTitleAsWindowTitle)); snprintf(settings->repeatState, sizeof(settings->repeatState), "%d", ui->repeatState); ui->shuffleEnabled ? c_strcpy(settings->shuffleEnabled, "1", sizeof(settings->shuffleEnabled)) : c_strcpy(settings->shuffleEnabled, "0", sizeof(settings->shuffleEnabled)); if (settings->visualizerBarWidth[0] == '\0') snprintf(settings->visualizerBarWidth, sizeof(settings->visualizerBarWidth), "%d", ui->visualizerBarWidth); 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->visualizerColorType[0] == '\0') snprintf(settings->visualizerColorType, sizeof(settings->visualizerColorType), "%d", ui->visualizerColorType); if (settings->titleDelay[0] == '\0') snprintf(settings->titleDelay, sizeof(settings->titleDelay), "%d", ui->titleDelay); 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->replayGainCheckFirst[0] == '\0') snprintf(settings->replayGainCheckFirst, sizeof(settings->replayGainCheckFirst), "%d", ui->replayGainCheckFirst); 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, "# Configuration file for kew terminal music player.\n"); fprintf(file, "# Make sure that kew is not running before editing this " "file in order for changes to take effect.\n\n"); fprintf(file, "\n[miscellaneous]\n\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, "# Replay gain check first, can be either 0=track, " "1=album or 2=disabled.\n"); fprintf(file, "replayGainCheckFirst=%s\n\n", settings->replayGainCheckFirst); fprintf(file, "# Save Repeat and Shuffle Settings.\n"); fprintf(file, "saveRepeatShuffleSettings=%s\n\n", settings->saveRepeatShuffleSettings); fprintf(file, "repeatState=%s\n\n", settings->repeatState); fprintf(file, "shuffleEnabled=%s\n\n", settings->shuffleEnabled); fprintf(file, "# Set the window title to the title of the currently " "playing track\n"); fprintf(file, "trackTitleAsWindowTitle=%s\n\n", settings->trackTitleAsWindowTitle); fprintf(file, "\n[colors]\n\n"); fprintf(file, "# Theme's go in ~/.config/kew/themes (on Linux/FreeBSD/Android), \n"); fprintf(file, "# and ~/Library/Preferences/kew/themes (on macOS), \n"); fprintf(file, "theme=%s\n\n", settings->theme); fprintf(file, "# Color Mode is:\n"); fprintf(file, "# 0 = 16-bit color palette from default theme, \n"); fprintf(file, "# 1 = Colors derived from track cover, \n"); fprintf(file, "# 2 = Colors derived from TrueColor theme, \n\n"); fprintf(file, "# Color Mode:\n"); fprintf(file, "colorMode=%d\n\n", ui->colorMode); fprintf(file, "# Terminal color theme is default.theme in \n"); fprintf(file, "# ~/.config/kew/themes (on Linux/FreeBSD/Android), \n"); fprintf(file, "# and ~/Library/Preferences/kew/themes (on macOS).\n\n"); fprintf(file, "\n[track cover]\n\n"); fprintf(file, "coverEnabled=%s\n", settings->coverEnabled); fprintf(file, "coverAnsi=%s\n\n", settings->coverAnsi); fprintf(file, "\n[mouse]\n\n"); fprintf(file, "mouseEnabled=%s\n\n", settings->mouseEnabled); fprintf(file, "\n[visualizer]\n\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=lighten, 1=brightness depending on bar height, " "2=reversed, 3=reversed darken.\n"); fprintf(file, "visualizerColorType=%s\n\n", settings->visualizerColorType); fprintf(file, "# 0=Thin bars, 1=Bars twice the width, 2=Auto (depends " "on window size).\n"); fprintf(file, "visualizerBarWidth=%s\n\n", settings->visualizerBarWidth); fprintf(file, "\n[progress bar]\n\n"); fprintf(file, "# Progress bar in track view\n"); fprintf(file, "# The progress bar can be configured in many ways.\n"); fprintf(file, "# When copying the values below, be sure to include values " "that are empty spaces or things will get messed up.\n"); fprintf(file, "# Be sure to have the actual uncommented values last.\n"); fprintf( file, "# For instance use the below values for a pill muncher mode:\n\n"); fprintf(file, "#progressBarElapsedEvenChar= \n"); fprintf(file, "#progressBarElapsedOddChar= \n"); fprintf(file, "#progressBarApproachingEvenChar=•\n"); fprintf(file, "#progressBarApproachingOddChar=·\n"); fprintf(file, "#progressBarCurrentEvenChar=á—§\n"); fprintf(file, "#progressBarCurrentOddChar=á—§\n\n"); fprintf(file, "# To have a thick line: \n\n"); fprintf(file, "#progressBarElapsedEvenChar=â”\n"); fprintf(file, "#progressBarElapsedOddChar=â”\n"); fprintf(file, "#progressBarApproachingEvenChar=â”\n"); fprintf(file, "#progressBarApproachingOddChar=â”\n"); fprintf(file, "#progressBarCurrentEvenChar=â”\n"); fprintf(file, "#progressBarCurrentOddChar=â”\n\n"); fprintf(file, "# To have dots (the original): \n\n"); fprintf(file, "#progressBarElapsedEvenChar=â– \n"); fprintf(file, "#progressBarElapsedOddChar= \n"); fprintf(file, "#progressBarApproachingEvenChar==\n"); fprintf(file, "#progressBarApproachingOddChar= \n"); fprintf(file, "#progressBarCurrentEvenChar=â– \n"); fprintf(file, "#progressBarCurrentOddChar= \n\n"); fprintf(file, "# Current values: \n\n"); fprintf(file, "progressBarElapsedEvenChar=%s\n", settings->progressBarElapsedEvenChar); fprintf(file, "progressBarElapsedOddChar=%s\n", settings->progressBarElapsedOddChar); fprintf(file, "progressBarApproachingEvenChar=%s\n", settings->progressBarApproachingEvenChar); fprintf(file, "progressBarApproachingOddChar=%s\n", settings->progressBarApproachingOddChar); fprintf(file, "progressBarCurrentEvenChar=%s\n", settings->progressBarCurrentEvenChar); fprintf(file, "progressBarCurrentOddChar=%s\n\n", settings->progressBarCurrentOddChar); 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, "\n[key bindings]\n\n"); fprintf(file, "volumeUp=%s\n", settings->volumeUp); fprintf(file, "volumeUpAlt=%s\n", settings->volumeUpAlt); fprintf(file, "volumeDown=%s\n", settings->volumeDown); fprintf(file, "previousTrackAlt=%s\n", settings->previousTrackAlt); fprintf(file, "nextTrackAlt=%s\n", settings->nextTrackAlt); fprintf(file, "scrollUpAlt=%s\n", settings->scrollUpAlt); fprintf(file, "scrollDownAlt=%s\n", settings->scrollDownAlt); fprintf(file, "switchNumberedSong=%s\n", settings->switchNumberedSong); fprintf(file, "togglePause=%s\n", settings->togglePause); fprintf(file, "toggleNotifications=%s\n", settings->toggleNotifications); fprintf(file, "toggleColorsDerivedFrom=%s\n", settings->cycleColorsDerivedFrom); fprintf(file, "cycleThemes=%s\n", settings->cycleThemes); 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, "addToFavoritesPlaylist=%s\n", settings->addToFavoritesPlaylist); 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", settings->quit); fprintf(file, "altQuit=%s\n\n", settings->altQuit); 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, "showKeysAlt=%s\n\n", settings->showKeysAlt); fprintf(file, "# Keys for scrolling page up and page down:\n"); fprintf(file, "prevPage=%s\n", settings->prevPage); fprintf(file, "nextPage=%s\n\n", settings->nextPage); 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/src/settings.h000066400000000000000000000006601507107350600144520ustar00rootroot00000000000000#ifndef SETTINGS_H #define SETTINGS_H #include "events.h" #include "appstate.h" #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef NUM_KEY_MAPPINGS #define NUM_KEY_MAPPINGS 63 #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/src/songloader.c000066400000000000000000000327771507107350600147600ustar00rootroot00000000000000#include "songloader.h" #include "cache.h" #include "file.h" #include "imgfunc.h" #include "sound.h" #include "soundcommon.h" #include "stb_image.h" #include "tagLibWrapper.h" #include "utils.h" #include #include #include #include #include #include /* songloader.c This file should contain only functions related to loading song data. */ #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #define MAX_RECURSION_DEPTH 10 static guint track_counter = 0; void makeFilePath(const char *dirPath, char *filePath, size_t filePathSize, const struct dirent *entry) { if (dirPath == NULL || filePath == NULL || entry == NULL || filePathSize == 0) return; size_t dirLen = strnlen(dirPath, filePathSize); size_t nameLen = strnlen(entry->d_name, filePathSize); if (dirLen == filePathSize) { filePath[0] = '\0'; return; } size_t neededSize = dirLen + nameLen + 1; // +1 for '\0' if (dirPath[dirLen - 1] != '/') { neededSize += 1; // for the added '/' } if (neededSize > filePathSize) { filePath[0] = '\0'; return; } // Compose the path safely if (dirPath[dirLen - 1] == '/') { snprintf(filePath, filePathSize, "%s%s", dirPath, entry->d_name); } else { snprintf(filePath, filePathSize, "%s/%s", dirPath, entry->d_name); } // snprintf guarantees null termination if filePathSize > 0 } char *chooseAlbumArt(const char *dirPath, char **customFileNameArr, int size, int depth) { if (!dirPath || !customFileNameArr || size <= 0 || depth > MAX_RECURSION_DEPTH) { return NULL; } DIR *directory = opendir(dirPath); if (!directory) { return NULL; } struct dirent *entry; struct stat fileStat; char filePath[MAXPATHLEN]; char resolvedPath[MAXPATHLEN]; char *result = NULL; for (int i = 0; i < size && !result; i++) { rewinddir(directory); while ((entry = readdir(directory)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; int written = snprintf(filePath, sizeof(filePath), "%s/%s", dirPath, entry->d_name); if (written < 0 || written >= (int)sizeof(filePath)) { continue; // path too long } if (realpath(filePath, resolvedPath) == NULL) { continue; } if (strncmp(resolvedPath, dirPath, strlen(dirPath)) != 0) { continue; // outside allowed directory } if (stat(resolvedPath, &fileStat) == 0 && S_ISREG(fileStat.st_mode)) { if (strcmp(entry->d_name, customFileNameArr[i]) == 0) { result = strdup(resolvedPath); break; } } } } // Recursive search for directories if (!result) { rewinddir(directory); while ((entry = readdir(directory)) != NULL && !result) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; int written = snprintf(filePath, sizeof(filePath), "%s/%s", dirPath, entry->d_name); if (written < 0 || written >= (int)sizeof(filePath)) { continue; } if (realpath(filePath, resolvedPath) == NULL) { continue; } if (strncmp(resolvedPath, dirPath, strlen(dirPath)) != 0) { continue; } struct stat linkStat; if (lstat(resolvedPath, &linkStat) == 0) { if (S_ISLNK(linkStat.st_mode)) { continue; // skip symlink } if (S_ISDIR(linkStat.st_mode)) { result = chooseAlbumArt( resolvedPath, customFileNameArr, size, depth + 1); } } } } closedir(directory); return result; } char *findLargestImageFile(const char *directoryPath, char *largestImageFile, off_t *largestFileSize) { DIR *directory = opendir(directoryPath); if (directory == NULL) { fprintf(stderr, "Failed to open directory: %s\n", directoryPath); return largestImageFile; } struct dirent *entry; struct stat fileStats; char filePath[MAXPATHLEN]; char resolvedPath[MAXPATHLEN]; while ((entry = readdir(directory)) != NULL) { // Skip "." and ".." if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; // Construct file path safely int len = snprintf(filePath, sizeof(filePath), "%s/%s", directoryPath, entry->d_name); if (len < 0 || len >= (int)sizeof(filePath)) { // Path too long, skip continue; } // Resolve the real path if (realpath(filePath, resolvedPath) == NULL) { // Could not resolve, skip continue; } // Use lstat to avoid following symlinks if (lstat(resolvedPath, &fileStats) == -1) { continue; } if (S_ISLNK(fileStats.st_mode)) { // Ignore symlinks continue; } if (S_ISREG(fileStats.st_mode)) { // Validate extension 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)) { // Ensure non-negative file size and prevent // integer overflow if (fileStats.st_size < 0) continue; // Optional: impose max file size limit, e.g., // 100 MB const off_t MAX_FILE_SIZE = 100 * 1024 * 1024; if (fileStats.st_size > MAX_FILE_SIZE) continue; if (fileStats.st_size > *largestFileSize) { *largestFileSize = fileStats.st_size; // Free previous allocation if owned if (largestImageFile != NULL) { free(largestImageFile); largestImageFile = NULL; } largestImageFile = strdup(resolvedPath); if (largestImageFile == NULL) { fprintf(stderr, "Memory allocation " "failure\n"); // Return early or continue // depending on desired behavior break; } } } } } 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)); if (songdata->metadata == NULL) { songdata->hasErrors = true; return; } 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; char *fileArr[12] = { "front.png", "front.jpg", "front.jpeg", "folder.png", "folder.jpg", "folder.jpeg", "cover.png", "cover.jpg", "cover.jpeg", "f.png", "f.jpg", "f.jpeg", }; tmp = chooseAlbumArt(path, fileArr, 12, 0); if (tmp == NULL) { 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/src/songloader.h000066400000000000000000000024101507107350600147420ustar00rootroot00000000000000#include #include "appstate.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/src/sound.c000066400000000000000000000662611507107350600137460ustar00rootroot00000000000000#define MA_EXPERIMENTAL__DATA_LOOPING_AND_CHAINING #define MA_NO_ENGINE #define MINIAUDIO_IMPLEMENTATION #include "sound.h" #include /* 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; if (filePath == NULL) { return MA_ERROR; } 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, "webm")) { int result = prepareNextWebmDecoder(songData); if (result < 0) return -1; ma_webm *first = getFirstWebmDecoder(); ma_channel channelMap[MA_MAX_CHANNELS]; ma_webm_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; 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) { ma_device_uninit(device); 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) { if (!hasErrorMessage()) 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; } int webm_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 webm file.\n"); return -1; } ma_webm *webm = getFirstWebmDecoder(); 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 = webm_on_audio_frames; deviceConfig.pUserData = webm; 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 (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, "webm")) { 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_webm *decoder = getCurrentWebmDecoder(); getWebmFileInfo(filePath, &format, &channels, &sampleRate, channelMap); if (decoder != NULL) ma_webm_ds_get_data_format( decoder, &nFormat, &nChannels, &nSampleRate, nChannelMap, MA_MAX_CHANNELS); bool sameFormat = false; // FIXME: Gapless/chaining of decoders disabled for now // bool sameFormat = (decoder != NULL && (format == // decoder->format && // channels == nChannels // && sampleRate == // nSampleRate)); audioData.avgBitRate = 0; if (isRepeatEnabled() || !(sameFormat && currentImplementation == WEBM)) { setImplSwitchReached(); pthread_mutex_lock(&dataSourceMutex); setCurrentImplementationType(WEBM); cleanupPlaybackDevice(); resetAllDecoders(); resetAudioBuffer(); audioData.sampleRate = sampleRate; int result = webm_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; } void resumePlayback(void) { // If this was unpaused with no song loaded if (audioData.restart) { audioData.endOfListReached = false; } if (!ma_device_is_started(&device)) { if (ma_device_start(&device) != MA_SUCCESS) { createAudioDevice(); ma_device_start(&device); } } paused = false; stopped = false; if (appState.currentView != TRACK_VIEW) { refresh = true; } } kew/src/sound.h000066400000000000000000000024111507107350600137360ustar00rootroot00000000000000#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 "common.h" #ifndef USERDATA_STRUCT #define USERDATA_STRUCT typedef struct { SongData *songdataA; SongData *songdataB; bool songdataADeleted; bool songdataBDeleted; SongData *currentSongData; ma_uint64 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_uint64 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 resumePlayback(void); void cleanupAudioContext(void); #endif kew/src/soundbuiltin.c000066400000000000000000000324101507107350600153220ustar00rootroot00000000000000#include "soundbuiltin.h" #include #include #include /* 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) { if (pDataSource == NULL) { return MA_INVALID_ARGS; } 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 = 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; if (pDataSource == NULL) { return MA_INVALID_ARGS; } AudioData *audioData = (AudioData *)pDataSource; if (pFormat == NULL || pChannels == NULL || pSampleRate == NULL) { return MA_INVALID_ARGS; } *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) { if (pDataSource == NULL) { return MA_INVALID_ARGS; } 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); } bool isValidGain(double gain) { return gain > -50.0 && gain < 50.0 && !isnan(gain) && isfinite(gain); } static bool computeReplayGain(AudioData *audioData, double *outGainDb) { if (audioData == NULL || audioData->pUserData == NULL) return false; UserData *ud = audioData->pUserData; bool result = false; double gainDb = 0.0; if ((!ud->songdataADeleted && ud->currentSongData == ud->songdataA) || (!ud->songdataBDeleted && ud->currentSongData == ud->songdataB)) { SongData *song = ud->currentSongData; if (song != NULL && song->metadata != NULL) { double trackGain = song->metadata->replaygainTrack; double albumGain = song->metadata->replaygainAlbum; bool useTrackFirst = (ud->replayGainCheckFirst == 0); if (useTrackFirst && isValidGain(trackGain)) { gainDb = trackGain; result = true; } else if (isValidGain(albumGain)) { gainDb = albumGain; result = true; } else if (!useTrackFirst && isValidGain(trackGain)) { gainDb = trackGain; result = true; } } } if (result) { *outGainDb = gainDb; } return result; } static void applyGainToInterleavedFrames(void *rawFrames, ma_format format, ma_uint64 framesToRead, int channels, double gain) { if (gain == 1.0) { return; // No gain to apply } // Prevent multiplication overflow: if (channels <= 0) return; if (framesToRead > (UINT64_MAX / channels)) { // Overflow would happen return; } switch (format) { case ma_format_f32: { float *frames = (float *)rawFrames; for (ma_uint64 i = 0; i < framesToRead; ++i) { for (int ch = 0; ch < channels; ++ch) { ma_uint64 frameIndex = i * channels + ch; float originalSample = frames[frameIndex]; double sample = (double)originalSample; sample *= gain; frames[frameIndex] = (float)sample; } } break; } case ma_format_s16: { ma_int16 *frames = (ma_int16 *)rawFrames; for (ma_uint64 i = 0; i < framesToRead; ++i) { for (int ch = 0; ch < channels; ++ch) { ma_uint64 frameIndex = i * channels + ch; ma_int16 originalSample = frames[frameIndex]; double sample = (double)originalSample; sample *= gain; 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: { ma_int32 *frames = (ma_int32 *)rawFrames; for (ma_uint64 i = 0; i < framesToRead; ++i) { for (int ch = 0; ch < channels; ++ch) { ma_uint64 frameIndex = i * channels + ch; ma_int32 originalSample = frames[frameIndex]; double sample = (double)originalSample; sample *= gain; if (sample > 2147483647.0) sample = 2147483647.0; else if (sample < -2147483648.0) sample = -2147483648.0; frames[frameIndex] = (ma_int32)sample; } } break; } default: // Unsupported format break; } } static bool performSeekIfRequested(ma_decoder *decoder, AudioData *audioData) { if (!isSeekRequested()) return true; ma_uint64 totalFrames = audioData->totalFrames; double 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 result = ma_decoder_seek_to_pcm_frame(decoder, targetFrame); setSeekRequested(false); return result == MA_SUCCESS; } static bool shouldSwitch(AudioData *audioData, ma_uint64 framesToRead, ma_result result, ma_uint64 cursor) { return (((audioData->totalFrames != 0 && cursor >= audioData->totalFrames) || framesToRead == 0 || isSkipToNext() || result != MA_SUCCESS) && !isEOFReached()); } 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; // Step 1: Compute gain double gainDb = 0.0; bool gainAvailable = computeReplayGain(audioData, &gainDb); double gainFactor = gainAvailable ? dbToLinear(gainDb) : 1.0; while (framesRead < frameCount) { ma_uint64 remainingFrames = frameCount - framesRead; if (pthread_mutex_trylock(&dataSourceMutex) != 0) return; // Step 2: Handle file switching or state invalidation if (audioData == NULL || isImplSwitchReached()) { pthread_mutex_unlock(&dataSourceMutex); return; } if (audioData->switchFiles) { executeSwitch(audioData); pthread_mutex_unlock(&dataSourceMutex); break; } ma_decoder *decoder = getCurrentBuiltinDecoder(); if ((getCurrentImplementationType() != BUILTIN && !isSkipToNext()) || decoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } // Step 3: Get total frames if needed if (audioData->totalFrames == 0) { ma_data_source_get_length_in_pcm_frames( decoder, &(audioData->totalFrames)); } // Step 4: Seek if requested if (!performSeekIfRequested(decoder, audioData)) { pthread_mutex_unlock(&dataSourceMutex); return; } // Step 5: Read frames ma_uint64 framesToRead = 0; ma_decoder *firstDecoder = getFirstDecoder(); ma_uint64 cursor = 0; if (firstDecoder == NULL || isEOFReached()) { pthread_mutex_unlock(&dataSourceMutex); return; } ma_result result = callReadPCMFrames( firstDecoder, audioData->format, pFramesOut, framesRead, audioData->channels, remainingFrames, &framesToRead); ma_data_source_get_cursor_in_pcm_frames(decoder, &cursor); void *frameStart = (void *)((float *)pFramesOut + framesRead * audioData->channels); // Cast matches format // Step 6: Apply gain if (gainFactor != 1.0) { applyGainToInterleavedFrames( frameStart, audioData->format, framesToRead, audioData->channels, gainFactor); } // Step 7: Check for switch if (shouldSwitch(audioData, framesToRead, result, cursor)) { activateSwitch(audioData); pthread_mutex_unlock(&dataSourceMutex); continue; } // Step 8: Update state framesRead += framesToRead; setBufferSize(framesToRead); pthread_mutex_unlock(&dataSourceMutex); } // Step 9: Finalize setAudioBuffer(pFramesOut, framesRead, audioData->sampleRate, audioData->channels, audioData->format); 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/src/soundbuiltin.h000066400000000000000000000005751507107350600153360ustar00rootroot00000000000000#ifndef SOUNDBUILTIN_H #define SOUNDBUILTIN_H #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/src/soundcommon.c000066400000000000000000001675521507107350600151640ustar00rootroot00000000000000#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; int hopSize = 512; int fftSize = 2048; int prevFftSize = 0; int fftSizeMilliseconds = 45; 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 float 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; ma_webm *webmDecoders[MAX_DECODERS]; ma_webm *firstWebmDecoder; #ifdef USE_FAAD m4a_decoder *m4aDecoders[MAX_DECODERS]; m4a_decoder *firstM4aDecoder; #endif int decoderIndex = -1; int m4aDecoderIndex = -1; int opusDecoderIndex = -1; int vorbisDecoderIndex = -1; int webmDecoderIndex = -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); } void uninitWebmDecoder(void *decoder) { ma_webm_uninit((ma_webm *)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); resetDecoders((void **)webmDecoders, (void **)&firstWebmDecoder, MAX_DECODERS, &webmDecoderIndex, uninitWebmDecoder); #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 0; } 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 (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() == WEBM) { ma_webm *decoder = getCurrentWebmDecoder(); 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); } MA_API ma_result ma_webm_read_pcm_frames_wrapper(void *pDecoder, void *pFramesOut, size_t frameCount, size_t *pFramesRead) { ma_decoder *dec = (ma_decoder *)pDecoder; return ma_webm_read_pcm_frames((ma_webm *)dec->pUserData, pFramesOut, frameCount, (ma_uint64 *)pFramesRead); } MA_API ma_result ma_webm_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_webm_seek_to_pcm_frame((ma_webm *)dec->pUserData, frameIndex); } MA_API ma_result ma_webm_get_cursor_in_pcm_frames_wrapper(void *pDecoder, long long int *pCursor) { ma_decoder *dec = (ma_decoder *)pDecoder; return ma_webm_get_cursor_in_pcm_frames((ma_webm *)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 0; } 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 0; } 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; } int closestPowerOfTwo(int x) { int n = 1; while (n < x) n <<= 1; return n; } // Sign-extend s24 ma_int32 unpack_s24(const ma_uint8 *p) { ma_int32 sample = p[0] | (p[1] << 8) | (p[2] << 16); if (sample & 0x800000) sample |= ~0xFFFFFF; return sample; } void setAudioBuffer(void *buf, int numFrames, ma_uint32 sampleRate, ma_uint32 channels, ma_format format) { int bufIndex = 0; // Dynamically determine FFT and hop size float hopFraction = 0.25f; // 25% hop (75% overlap) // Compute power-of-two window/hop sizes in samples int wantFFTSamples = (int)(fftSizeMilliseconds * sampleRate / 1000.0f); fftSize = closestPowerOfTwo(wantFFTSamples); // 2048 or 4096 int wantHopSamples = (int)(fftSize * hopFraction); // 25% of window length hopSize = closestPowerOfTwo(wantHopSamples); // 256, 512, 1024 if (fftSize > MAX_BUFFER_SIZE) fftSize = MAX_BUFFER_SIZE; // Ensure hop is never >= window if (hopSize >= fftSize) hopSize = fftSize / 2; // fallback minimum overlap while (bufIndex < numFrames) { if (writeHead >= fftSize) break; int framesLeft = numFrames - bufIndex; int spaceLeft = fftSize - writeHead; int framesToCopy = framesLeft < spaceLeft ? framesLeft : spaceLeft; switch (format) { case ma_format_u8: { ma_uint8 *src = (ma_uint8 *)buf + bufIndex * channels; for (int i = 0; i < framesToCopy; ++i) { float sum = 0.0f; for (ma_uint32 ch = 0; ch < channels; ++ch) { // Convert 0..255 to -1..1 sum += ((float)src[i * channels + ch] - 128.0f) / 128.0f; } audioBuffer[writeHead++] = sum / channels; } break; } case ma_format_s16: { ma_int16 *src = (ma_int16 *)buf + bufIndex * channels; for (int i = 0; i < framesToCopy; ++i) { float sum = 0.0f; for (ma_uint32 ch = 0; ch < channels; ++ch) { sum += (float)src[i * channels + ch] / 32768.0f; } audioBuffer[writeHead++] = sum / channels; } break; } case ma_format_s24: { ma_uint8 *src = (ma_uint8 *)buf + bufIndex * channels * 3; for (int i = 0; i < framesToCopy; ++i) { float sum = 0.0f; for (ma_uint32 ch = 0; ch < channels; ++ch) { int idx = i * channels * 3 + ch * 3; int32_t s = unpack_s24(&src[idx]); sum += (float)s / 8388608.0f; } audioBuffer[writeHead++] = sum / channels; } break; } case ma_format_s32: { int32_t *src = (int32_t *)buf + bufIndex * channels; for (int i = 0; i < framesToCopy; ++i) { float sum = 0.0f; for (ma_uint32 ch = 0; ch < channels; ++ch) { sum += (float)src[i * channels + ch] / 2147483648.0f; } audioBuffer[writeHead++] = sum / channels; } break; } case ma_format_f32: { float *src = (float *)buf + bufIndex * channels; for (int i = 0; i < framesToCopy; ++i) { float sum = 0.0f; for (ma_uint32 ch = 0; ch < channels; ++ch) { sum += src[i * channels + ch]; } audioBuffer[writeHead++] = sum / channels; } break; } default: fprintf(stderr, "Unsupported format in setAudioBuffer!\n"); return; } bufIndex += framesToCopy; // Process full window(s), maintain overlap (hop) while (writeHead >= fftSize) { bufferReady = true; // let main loop know FFT is ready // Shift buffer for overlap (keep last fftSize-hopSize // samples) memmove(audioBuffer, audioBuffer + hopSize, sizeof(float) * (fftSize - hopSize)); writeHead -= hopSize; } } } void resetAudioBuffer(void) { memset(audioBuffer, 0, sizeof(ma_int32) * MAX_BUFFER_SIZE); writeHead = 0; bufferReady = false; } void *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 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 shutdownAndroid(void) { // Avoid race condition when shutting down memset(&device, 0, sizeof(device)); } void clearCurrentTrack(void) { if (ma_device_is_started(&device)) { // Stop the device (which stops playback) ma_device_stop(&device); } ma_data_source_set_next(currentDecoder, NULL); resetAllDecoders(); } void togglePausePlayback(void) { if (ma_device_is_started(&device)) { pausePlayback(); } else if (isPaused() || isStopped()) { if (isStopped()) { resetClock(); } 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); switchDecoder(&webmDecoderIndex); 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; } 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) { soundVolume += volumeChange; setVolume(soundVolume); return 0; } ma_uint64 lastCursor = 0; ma_result callReadPCMFrames(ma_data_source *pDataSource, ma_format format, void *pFramesOut, ma_uint64 framesRead, ma_uint32 channels, ma_uint64 remainingFrames, ma_uint64 *pFramesToRead) { ma_result result; switch (format) { case ma_format_u8: { ma_uint8 *pOut = (ma_uint8 *)pFramesOut; result = ma_data_source_read_pcm_frames( pDataSource, pOut + (framesRead * channels), remainingFrames, pFramesToRead); } break; case ma_format_s16: { ma_int16 *pOut = (ma_int16 *)pFramesOut; result = ma_data_source_read_pcm_frames( pDataSource, pOut + (framesRead * channels), remainingFrames, pFramesToRead); } break; case ma_format_s24: { ma_uint8 *pOut = (ma_uint8 *)pFramesOut; result = ma_data_source_read_pcm_frames( pDataSource, pOut + (framesRead * channels * 3), remainingFrames, pFramesToRead); } break; case ma_format_s32: { ma_int32 *pOut = (ma_int32 *)pFramesOut; result = ma_data_source_read_pcm_frames( pDataSource, pOut + (framesRead * channels), remainingFrames, pFramesToRead); } break; case ma_format_f32: { float *pOut = (float *)pFramesOut; result = ma_data_source_read_pcm_frames( pDataSource, pOut + (framesRead * channels), remainingFrames, pFramesToRead); } break; default: { result = MA_INVALID_ARGS; } break; } return result; } #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 = callReadPCMFrames( firstDecoder, m4a->format, 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, pAudioData->sampleRate, pAudioData->channels, pAudioData->format); 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 = callReadPCMFrames( firstDecoder, opus->format, 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, pAudioData->sampleRate, pAudioData->channels, pAudioData->format); 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 = callReadPCMFrames( firstDecoder, vorbis->format, 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, pAudioData->sampleRate, pAudioData->channels, pAudioData->format); 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; } void webm_read_pcm_frames(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { ma_webm *webm = (ma_webm *)pDataSource; AudioData *pAudioData = (AudioData *)webm->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_webm *decoder = getCurrentWebmDecoder(); if (pAudioData->totalFrames == 0) ma_data_source_get_length_in_pcm_frames( decoder, &(pAudioData->totalFrames)); if ((getCurrentImplementationType() != WEBM && !isSkipToNext()) || (decoder == NULL)) { pthread_mutex_unlock(&dataSourceMutex); return; } // Check if seeking is requested if (isSeekRequested()) { ma_uint64 totalFrames = 0; ma_webm_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_webm_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_webm *firstDecoder = getFirstWebmDecoder(); ma_uint64 cursor = 0; if (firstDecoder == NULL) { pthread_mutex_unlock(&dataSourceMutex); return; } if (isEOFReached()) { pthread_mutex_unlock(&dataSourceMutex); return; } result = callReadPCMFrames( firstDecoder, webm->format, 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, pAudioData->sampleRate, pAudioData->channels, pAudioData->format); if (pFramesRead != NULL) { *pFramesRead = framesRead; } } int prepareNextWebmDecoder(SongData *songData) { ma_webm *currentDecoder; if (songData == NULL) return -1; char *filepath = songData->filePath; if (webmDecoderIndex == -1) { currentDecoder = getFirstWebmDecoder(); } else { currentDecoder = webmDecoders[webmDecoderIndex]; } ma_uint32 sampleRate; ma_uint32 channels; ma_format format; ma_channel channelMap[MA_MAX_CHANNELS]; ma_webm_get_data_format(currentDecoder, &format, &channels, &sampleRate, channelMap, MA_MAX_CHANNELS); uninitPreviousDecoder((void **)webmDecoders, webmDecoderIndex, (uninit_func)uninitWebmDecoder); ma_webm *decoder = (ma_webm *)malloc(sizeof(ma_webm)); ma_result result = ma_webm_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_webm_get_data_format(decoder, &nformat, &nchannels, &nsampleRate, nchannelMap, MA_MAX_CHANNELS); bool sameFormat = (currentDecoder == NULL); // FIXME gapless playback disabled for webm // bool sameFormat = (currentDecoder == NULL || (format == nformat && // channels == nchannels && // sampleRate == // nsampleRate)); if (!sameFormat) { ma_webm_uninit(decoder, NULL); free(decoder); return 0; } if (firstWebmDecoder != NULL) { decoder->pReadSeekTellUserData = (AudioData *)firstWebmDecoder->pReadSeekTellUserData; } decoder->format = nformat; decoder->onRead = ma_webm_read_pcm_frames_wrapper; decoder->onSeek = ma_webm_seek_to_pcm_frame_wrapper; decoder->onTell = ma_webm_get_cursor_in_pcm_frames_wrapper; setNextDecoder((void **)webmDecoders, (void **)&decoder, (void **)&firstWebmDecoder, &webmDecoderIndex, (uninit_func)uninitWebmDecoder); if (songData != NULL) { if (decoder != NULL) { songData->duration = decoder->duration; } } if (currentDecoder != NULL && decoder != NULL) { if (!isEOFReached()) ma_data_source_set_next(currentDecoder, decoder); } return 0; } void getWebmFileInfo(const char *filename, ma_format *format, ma_uint32 *channels, ma_uint32 *sampleRate, ma_channel *channelMap) { ma_webm tmp; if (ma_webm_init_file(filename, NULL, NULL, &tmp) == MA_SUCCESS) { *sampleRate = tmp.sampleRate; *channels = tmp.channels; *format = tmp.format; ma_webm_uninit(&tmp, NULL); } (void)channelMap; } ma_webm *getFirstWebmDecoder(void) { return firstWebmDecoder; } ma_webm *getCurrentWebmDecoder(void) { if (webmDecoderIndex == -1) return getFirstWebmDecoder(); else return webmDecoders[webmDecoderIndex]; } void webm_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount) { AudioData *pDataSource = (AudioData *)pDevice->pUserData; ma_uint64 framesRead = 0; webm_read_pcm_frames(&(pDataSource->base), pFramesOut, frameCount, &framesRead); if (framesRead < frameCount) { ma_webm *webm = (ma_webm *)&(pDataSource->base); float *output = (float *)pFramesOut; memset(output + framesRead * webm->channels, 0, (frameCount - framesRead) * webm->channels * sizeof(float)); } (void)pFramesIn; } kew/src/soundcommon.h000066400000000000000000000154571507107350600151650ustar00rootroot00000000000000#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" #include "webm.h" #ifndef MAXPATHLEN #define MAXPATHLEN 4096 #endif #ifndef MAX_BUFFER_SIZE #define MAX_BUFFER_SIZE 32768 #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; int replayGainCheckFirst; SongData *currentSongData; ma_uint64 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_uint64 currentPCMFrame; ma_uint32 avgBitRate; bool switchFiles; int currentFileIndex; ma_uint64 totalFrames; bool endOfListReached; bool restart; } AudioData; #endif enum AudioImplementation { PCM, BUILTIN, VORBIS, OPUS, M4A, WEBM, NONE }; struct m4a_decoder; typedef struct m4a_decoder m4a_decoder; extern int hopSize; extern int fftSize; extern int prevFftSize; 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); void *getAudioBuffer(void); void setAudioBuffer(void *buf, int numSamples, ma_uint32 sampleRate, ma_uint32 channels, ma_format format); int32_t unpack_s24(const ma_uint8* p); 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 getWebmFileInfo(const char *filename, ma_format *format, ma_uint32 *channels, ma_uint32 *sampleRate, ma_channel *channelMap); int prepareNextWebmDecoder(SongData *songData); ma_webm *getCurrentWebmDecoder(void); ma_webm *getFirstWebmDecoder(void); void webm_on_audio_frames(ma_device *pDevice, void *pFramesOut, const void *pFramesIn, ma_uint32 frameCount); ma_result callReadPCMFrames( ma_data_source *pDataSource, ma_format format, void *pFramesOut, ma_uint64 framesRead, ma_uint32 channels, ma_uint64 remainingFrames, ma_uint64 *pFramesToRead); bool doesOSallowVolumeControl(); void shutdownAndroid(void); #endif kew/src/tagLibWrapper.cpp000066400000000000000000001404451507107350600157160ustar00rootroot00000000000000// 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 #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+/"; const uint32_t MAX_REASONABLE_SIZE = 100 * 1024 * 1024; // 100MB limit #if defined(TAGLIB_MAJOR_VERSION) && TAGLIB_MAJOR_VERSION >= 2 #define HAVE_COMPLEXPROPERTIES 1 #else #define HAVE_COMPLEXPROPERTIES 0 #endif std::vector decodeBase64(const std::string &encoded_string) { const size_t MAX_DECODED_SIZE = 100 * 1024 * 1024; // 100 MB const size_t MAX_ENCODED_SIZE = (MAX_DECODED_SIZE * 4) / 3 + 4; // Max base64 size + padding const std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; auto is_base64 = [&](unsigned char c) -> bool { return base64_chars.find(c) != std::string::npos; }; auto base64_index = [&](unsigned char c) -> unsigned char { auto pos = base64_chars.find(c); return pos != std::string::npos ? static_cast(pos) : 0; }; 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]; // Early check to prevent any overflow issues if (in_len > MAX_ENCODED_SIZE) { throw std::runtime_error("Base64 input too large: exceeds reasonable limit"); } // Rough estimate of decoded size size_t decoded_size = (in_len * 3) / 4; if (decoded_size > MAX_DECODED_SIZE) throw std::runtime_error("Base64 input too large: exceeds 100 MB limit"); std::vector decoded_data; try { decoded_data.reserve(decoded_size); } catch (const std::bad_alloc &) { throw std::runtime_error("Cannot allocate memory for base64 decoding"); } while (in_len-- && encoded_string[in_] != '=' && is_base64(encoded_string[in_])) { char_array_4[i++] = encoded_string[in_++]; if (i == 4) { for (i = 0; i < 4; i++) char_array_4[i] = base64_index(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]; if (decoded_data.size() + 3 > MAX_DECODED_SIZE) { throw std::runtime_error("Decoded data exceeds size limit during processing"); } for (i = 0; i < 3; i++) decoded_data.push_back(char_array_3[i]); i = 0; } } // Process remaining characters if (i > 0) { 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] = is_base64(char_array_4[j]) ? base64_index(char_array_4[j]) : 0; 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]; if (i > 1) { size_t remaining_bytes = i - 1; if (decoded_data.size() + remaining_bytes > MAX_DECODED_SIZE) { throw std::runtime_error("Decoded data exceeds size limit during final processing"); } for (size_t j = 0; j < i - 1; j++) decoded_data.push_back(char_array_3[j]); } } return decoded_data; } TagLib::StringList getOggFieldListCaseInsensitive(const TagLib::Ogg::XiphComment *comment, const std::string &fieldName) { if (!comment) { return TagLib::StringList(); } // Use TagLib::String for safer Unicode handling TagLib::String targetField(fieldName, TagLib::String::UTF8); TagLib::String lowerTargetField = targetField.upper(); // TagLib handles case conversion safely TagLib::Ogg::FieldListMap fieldListMap = comment->fieldListMap(); for (auto it = fieldListMap.begin(); it != fieldListMap.end(); ++it) { TagLib::String currentKey = it->first; if (currentKey.upper() == lowerTargetField) { return it->second; } } return TagLib::StringList(); } 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 buffer_size, size_t offset) { if (buffer == nullptr || offset + 4 > buffer_size) { // Handle error - throw exception, return error code, etc. throw std::runtime_error("Buffer overflow in read_uint32_be"); } 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; size_t dataSize = data.size(); auto readUInt32 = [&](uint32_t &value) -> bool { if (offset + 4 > dataSize) return false; value = (ptr[offset] << 24) | (ptr[offset + 1] << 16) | (ptr[offset + 2] << 8) | ptr[offset + 3]; offset += 4; return true; }; auto safeAdd = [](size_t a, uint32_t b) -> bool { return b <= SIZE_MAX - a; // Check if a + b would overflow }; uint32_t pictureType, mimeLength, descLength, width, height, depth, colors, dataLength; if (!readUInt32(pictureType) || !readUInt32(mimeLength)) return; if (mimeLength > MAX_REASONABLE_SIZE || offset + mimeLength > dataSize) return; // Check for overflow before adding if (!safeAdd(offset, mimeLength) || offset + mimeLength > dataSize) return; mimeType = std::string(reinterpret_cast(&ptr[offset]), mimeLength); offset += mimeLength; if (!readUInt32(descLength)) return; if (!safeAdd(offset, descLength) || offset + descLength > dataSize) return; offset += descLength; if (!readUInt32(width) || !readUInt32(height) || !readUInt32(depth) || !readUInt32(colors) || !readUInt32(dataLength)) return; if (!safeAdd(offset, dataLength) || offset + dataLength > dataSize) return; imageData.assign(&ptr[offset], &ptr[offset + dataLength]); } #if HAVE_COMPLEXPROPERTIES bool extractCoverArtFromOgg(const std::string &audioFilePath, const std::string &outputFileName) { TagLib::FileRef f(audioFilePath.c_str(), true); if (!f.file() || !f.file()->isOpen()) { std::cerr << "Error: Could not open file: " << audioFilePath << std::endl; return false; } auto pictures = f.file()->complexProperties("PICTURE"); if (pictures.isEmpty()) { std::cerr << "No cover art found in the file (via complexProperties).\n"; return false; } // Use the first picture found (usually the front cover) for (const auto &pic : pictures) { auto it_data = pic.find("data"); if (it_data == pic.end()) continue; // Skip if no data // Write the image data to a file std::ofstream outFile(outputFileName, std::ios::binary); if (!outFile) { std::cerr << "Error: Could not write to output file.\n"; return false; } TagLib::ByteVector bv = it_data->second.toByteVector(); outFile.write(bv.data(), bv.size()); outFile.close(); return true; } std::cerr << "No usable cover image found in complexProperties().\n"; return false; } #else bool 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 = getOggFieldListCaseInsensitive(xiphComment, "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 true; // Success } // Check COVERART and COVERARTMIME TagLib::StringList coverArtList = getOggFieldListCaseInsensitive(xiphComment, "COVERART"); TagLib::StringList coverArtMimeList = getOggFieldListCaseInsensitive(xiphComment, "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 } #endif bool looksLikeJpeg(const std::vector &data) { return data.size() > 4 && data[0] == 0xFF && data[1] == 0xD8 && data[data.size() - 2] == 0xFF && data[data.size() - 1] == 0xD9; } bool looksLikePng(const std::vector &data) { static const unsigned char pngHeader[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; if (data.size() < 12) return false; if (memcmp(data.data(), pngHeader, 8) != 0) return false; // Look for IEND within last 16 bytes for (size_t i = data.size() >= 16 ? data.size() - 16 : 0; i + 3 < data.size(); ++i) { if (memcmp(&data[i], "IEND", 4) == 0) return true; } return false; } bool looksLikeWebp(const std::vector &data) { return data.size() > 12 && memcmp(&data[0], "RIFF", 4) == 0 && memcmp(&data[8], "WEBP", 4) == 0; } bool isAudioOggStreamHeader(const ogg_packet &headerPacket) { if (headerPacket.bytes >= 7 && headerPacket.packet[0] == 0x01 && memcmp(headerPacket.packet + 1, "vorbis", 6) == 0) return true; if (headerPacket.bytes >= 8 && memcmp(headerPacket.packet, "OpusHead", 8) == 0) return true; if (headerPacket.bytes >= 4 && memcmp(headerPacket.packet, "fLaC", 4) == 0) return true; return false; } 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: " << audioFilePath << std::endl; return false; } ogg_sync_state oy; ogg_sync_init(&oy); ogg_page og; ogg_packet op; std::map streams; std::map isHeaderParsed; std::map isAudioStream; std::map> streamPackets; char *buffer; int bytes = 0; 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); // Lazily create state for new streams if (streams.find(serialNo) == streams.end()) { ogg_stream_state os; ogg_stream_init(&os, serialNo); streams[serialNo] = os; isHeaderParsed[serialNo] = false; isAudioStream[serialNo] = false; } ogg_stream_state &os = streams[serialNo]; ogg_stream_pagein(&os, &og); while (ogg_stream_packetout(&os, &op) == 1) { // Only decide on audio-ness for first packet if (!isHeaderParsed[serialNo]) { isAudioStream[serialNo] = isAudioOggStreamHeader(op); isHeaderParsed[serialNo] = true; if (isAudioStream[serialNo]) continue; } if (!isAudioStream[serialNo]) { // For image/video: collect all packets from this stream streamPackets[serialNo].insert( streamPackets[serialNo].end(), op.packet, op.packet + op.bytes); } } } if (bytes == 0) break; } // Clean up for (auto &entry : streams) ogg_stream_clear(&(entry.second)); ogg_sync_clear(&oy); fclose(oggFile); // Write out a valid image stream for (auto &kv : streamPackets) { const auto &data = kv.second; if (looksLikeJpeg(data) || looksLikePng(data) || looksLikeWebp(data)) { std::ofstream outFile(outputFileName, std::ios::binary); if (!outFile) { std::cerr << "Error: Could not write to output file." << std::endl; return false; } outFile.write(reinterpret_cast(data.data()), data.size()); outFile.close(); return true; } } std::cerr << "No embedded image stream found in file." << std::endl; return false; } 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 (strncasecmp(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(), pictureBlock.size(), offset); offset += 4; // Read MIME TYPE LENGTH unsigned int mimeTypeLength = read_uint32_be(pictureBlock.data(), pictureBlock.size(), 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(), pictureBlock.size(), 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(), pictureBlock.size(), offset); offset += 4; // Read HEIGHT read_uint32_be(pictureBlock.data(), pictureBlock.size(), offset); offset += 4; ; // Read COLOR DEPTH read_uint32_be(pictureBlock.data(), pictureBlock.size(), offset); offset += 4; // Read NUMBER OF COLORS read_uint32_be(pictureBlock.data(), pictureBlock.size(), offset); offset += 4; // Read DATA LENGTH unsigned int dataLength = read_uint32_be(pictureBlock.data(), pictureBlock.size(), 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 = extractCoverArtFromOgg(input_file, coverFilePath); if (!coverArtExtracted) { coverArtExtracted = extractCoverArtFromOggVideo(input_file, coverFilePath); } } else if (extension == "wav") { coverArtExtracted = extractCoverArtFromWav(input_file, coverFilePath); } if (coverArtExtracted) { return 0; } else { return -1; } } } kew/src/tagLibWrapper.h000066400000000000000000000014261507107350600153560ustar00rootroot00000000000000// 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/src/term.c000066400000000000000000000176501507107350600135630ustar00rootroot00000000000000#include "term.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include #include #include /* 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. */ const int MAX_TERMINAL_ROWS = 9999; void setTerminalColor(int color) { /* - 0: Black - 1: Red - 2: Green - 3: Yellow - 4: Blue - 5: Magenta - 6: Cyan - 7: White - 8: Bright Black (Gray) - 9: Bright Red - 10: Bright Green - 11: Bright Yellow - 12: Bright Blue - 13: Bright Magenta - 14: Bright Cyan - 15: Bright White */ if (color < -1 || color > 15) color = 7; // default to white if (color == -1) { // Default foreground printf("\033[39m"); } else if (color < 8) { // Normal colors (30–37) printf("\033[0;3%dm", color); } else { // Bright colors (90–97) printf("\033[0;9%dm", color - 8); } } void setTextColorRGB(int r, int g, int b) { if (r < 0 || r > 255) r = 255; if (g < 0 || g > 255) g = 255; if (b < 0 || b > 255) b = 255; printf("\033[0;38;2;%03u;%03u;%03um", (unsigned int)r, (unsigned int)g, (unsigned int)b); } void getTermSize(int *width, int *height) { struct winsize w; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == -1 || w.ws_row == 0 || w.ws_col == 0) { // Fallback for non-interactive environments (like Homebrew tests) *height = 24; // default terminal height *width = 80; // default terminal width return; } *height = (int)w.ws_row; *width = (int)w.ws_col; } void setNonblockingMode(void) { 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(void) { struct termios ttystate; tcgetattr(STDIN_FILENO, &ttystate); ttystate.c_lflag |= ICANON; tcsetattr(STDIN_FILENO, TCSANOW, &ttystate); } void saveCursorPosition(void) { printf("\033[s"); } void restoreCursorPosition(void) { printf("\033[u"); } void setDefaultTextColor(void) { printf("\033[0m"); } int isInputAvailable(void) { 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(void) { printf("\033[?25l"); } void showCursor(void) { printf("\033[?25h"); fflush(stdout); } void resetConsole(void) { // 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(void) { printf("\033[J"); } void clearLine(void) { printf("\033[2K"); } void clearRestOfLine(void) { printf("\033[K"); } void clearScreen(void) { 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 gotoFirstLineFirstRow(void) { printf("\033[H"); } void enableScrolling(void) { printf("\033[?7h"); } void disableTerminalLineInput(void) { setvbuf(stdout, NULL, _IOFBF, BUFSIZ); } void setRawInputMode(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) { if (numRows < 0 || numRows > MAX_TERMINAL_ROWS) return; printf("\033[%dA", numRows); printf("\033[0m"); } void cursorJumpDown(int numRows) { if (numRows < 0 || numRows > MAX_TERMINAL_ROWS) return; printf("\033[%dB", numRows); } int readInputSequence(char *seq, size_t seqSize) { if (seq == NULL || seqSize < 2) // Buffer needs at least 1 byte + null terminator return 0; char c; ssize_t bytesRead = read(STDIN_FILENO, &c, 1); if (bytesRead <= 0) return 0; // ASCII character (single byte, no continuation bytes) if ((c & 0x80) == 0) { if (seqSize < 2) // Make sure there's space for the null terminator return 0; seq[0] = c; seq[1] = '\0'; return 1; } // Determine the length of the UTF-8 sequence and validate the first // byte 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 return 0; // Invalid UTF-8 start byte if ((size_t)(additionalBytes + 1) >= seqSize) return 0; seq[0] = c; // Read the continuation bytes bytesRead = read(STDIN_FILENO, &seq[1], additionalBytes); if (bytesRead != additionalBytes) return 0; // Validate continuation bytes (0x80 <= byte <= 0xBF) for (int i = 1; i <= additionalBytes; ++i) { if ((seq[i] & 0xC0) != 0x80) return 0; // Invalid continuation byte } // Null terminate the string seq[additionalBytes + 1] = '\0'; return additionalBytes + 1; // Return the total length including the null terminator } int getIndentation(int textWidth) { int term_w, term_h; getTermSize(&term_w, &term_h); if (textWidth <= 0 || term_w <= 0) { return 0; } if (textWidth >= term_w) { textWidth = term_w; } int available_space = term_w - textWidth; int indent = (available_space / 2) + 1; return indent; } void enterAlternateScreenBuffer(void) { // Enter alternate screen buffer printf("\033[?1049h"); } void exitAlternateScreenBuffer(void) { // Exit alternate screen buffer printf("\033[?1049l"); } void enableTerminalMouseButtons(void) { // Enable program to accept mouse input as codes printf("\033[?1002h\033[?1006h"); } void disableTerminalMouseButtons(void) { // Disable program to accept mouse input as codes printf("\033[?1002l\033[?1006l"); } void setTerminalWindowTitle(char *title) { // Only change window title, no icon printf("\033]2;%s\007", title); } void saveTerminalWindowTitle(void) { // Save terminal window title on the stack printf("\033[22;0t"); } void restoreTerminalWindowTitle(void) { // Restore terminal window title from the stack printf("\033[23;0t"); } kew/src/term.h000066400000000000000000000023651507107350600135650ustar00rootroot00000000000000#ifndef TERM_H #include #define TERM_H #ifndef __USE_POSIX #define __USE_POSIX #endif #ifdef __GNU__ #define _BSD_SOURCE #endif void setTerminalColor(int color); void setTextColorRGB(int r, int g, int b); void getTermSize(int *width, int *height); int getIndentation(int textWidth); 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 clearLine(void); void clearRestOfLine(void); void gotoFirstLineFirstRow(void); void initResize(void); void disableTerminalLineInput(void); void setRawInputMode(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); void setTerminalWindowTitle(char *title); void saveTerminalWindowTitle(void); void restoreTerminalWindowTitle(void); #endif kew/src/theme.c000066400000000000000000000223301507107350600137050ustar00rootroot00000000000000 #include "theme.h" #include "common.h" #include #include #include #include #include typedef struct { const char *key; ColorValue *field; } ThemeMapping; PixelData hexToPixel(const char *hex) { PixelData p = {0, 0, 0}; if (hex[0] == '#') hex++; // skip # if (strlen(hex) == 6) { char r[3], g[3], b[3]; strncpy(r, hex, 2); r[2] = '\0'; strncpy(g, hex + 2, 2); g[2] = '\0'; strncpy(b, hex + 4, 2); b[2] = '\0'; p.r = (unsigned char)strtol(r, NULL, 16); p.g = (unsigned char)strtol(g, NULL, 16); p.b = (unsigned char)strtol(b, NULL, 16); } return p; } void trimWhitespace(char *str) { while (isspace((unsigned char)*str)) str++; char *end = str + strlen(str) - 1; while (end > str && isspace((unsigned char)*end)) *end-- = '\0'; memmove(str, str, strlen(str) + 1); } void removeComment(char *str) { char *p = str; while (*p) { if (*p == '#') { // If previous char is whitespace, treat as comment // start if (p == str || isspace((unsigned char)*(p - 1))) { *p = '\0'; break; } } p++; } } // Parse hex color safely (e.g. #aabbcc) int parseHexColor(const char *hex, PixelData *out) { if (!hex || strlen(hex) != 7 || hex[0] != '#') return 0; unsigned int r, g, b; if (sscanf(hex + 1, "%02x%02x%02x", &r, &g, &b) != 3) return 0; out->r = (unsigned char)r; out->g = (unsigned char)g; out->b = (unsigned char)b; return 1; } int parseColorValue(const char *value, ColorValue *out) { if (!value || !out) return 0; // Check if it's hex (#RRGGBB) if (value[0] == '#') { unsigned int r, g, b; if (sscanf(value, "#%02x%02x%02x", &r, &g, &b) != 3) { return 0; // failed to parse hex } out->type = COLOR_TYPE_RGB; out->rgb.r = (uint8_t)r; out->rgb.g = (uint8_t)g; out->rgb.b = (uint8_t)b; return 1; } // Otherwise, try integer for ANSI index char *endptr = NULL; errno = 0; long index = strtol(value, &endptr, 10); if (errno || endptr == value || *endptr != '\0') { return 0; // invalid number } if (index < -1 || index > 15) { return 0; // out of range for 16-color ANSI } out->type = COLOR_TYPE_ANSI; out->ansiIndex = (int8_t)index; return 1; } int loadThemeFromFile(const char *themesDir, const char *filename, Theme *currentTheme) { memset(currentTheme, 0, sizeof(Theme)); if (!themesDir || !filename) { fprintf(stderr, "Theme directory or filename is NULL.\n"); setErrorMessage("Theme directory or filename is NULL."); return 0; } char path[512]; if (snprintf(path, sizeof(path), "%s/%s", themesDir, filename) >= (int)sizeof(path)) { fprintf(stderr, "Theme path too long.\n"); return 0; } FILE *file = fopen(path, "r"); if (!file) { fprintf(stderr, "Failed to open theme file.\n"); setErrorMessage("Failed to open theme file."); return 0; } // Map of all known keys to Theme fields ThemeMapping mappings[] = { {"accent", ¤tTheme->accent}, {"text", ¤tTheme->text}, {"textDim", ¤tTheme->textDim}, {"textMuted", ¤tTheme->textMuted}, {"logo", ¤tTheme->logo}, {"header", ¤tTheme->header}, {"footer", ¤tTheme->footer}, {"help", ¤tTheme->help}, {"link", ¤tTheme->link}, {"nowplaying", ¤tTheme->nowplaying}, {"playlist_rownum", ¤tTheme->playlist_rownum}, {"playlist_title", ¤tTheme->playlist_title}, {"playlist_playing", ¤tTheme->playlist_playing}, {"trackview_title", ¤tTheme->trackview_title}, {"trackview_artist", ¤tTheme->trackview_artist}, {"trackview_album", ¤tTheme->trackview_album}, {"trackview_year", ¤tTheme->trackview_year}, {"trackview_time", ¤tTheme->trackview_time}, {"trackview_visualizer", ¤tTheme->trackview_visualizer}, {"trackview_lyrics", ¤tTheme->trackview_lyrics}, {"library_artist", ¤tTheme->library_artist}, {"library_album", ¤tTheme->library_album}, {"library_track", ¤tTheme->library_track}, {"library_enqueued", ¤tTheme->library_enqueued}, {"library_playing", ¤tTheme->library_playing}, {"search_label", ¤tTheme->search_label}, {"search_query", ¤tTheme->search_query}, {"search_result", ¤tTheme->search_result}, {"search_enqueued", ¤tTheme->search_enqueued}, {"search_playing", ¤tTheme->search_playing}, {"progress_filled", ¤tTheme->progress_filled}, {"progress_elapsed", ¤tTheme->progress_elapsed}, {"progress_empty", ¤tTheme->progress_empty}, {"progress_duration", ¤tTheme->progress_duration}, {"status_info", ¤tTheme->status_info}, {"status_warning", ¤tTheme->status_warning}, {"status_error", ¤tTheme->status_error}, {"status_success", ¤tTheme->status_success}}; const size_t mappingCount = sizeof(mappings) / sizeof(ThemeMapping); char line[512]; int lineNum = 0; int found = 0; while (fgets(line, sizeof(line), file)) { lineNum++; removeComment(line); trimWhitespace(line); if (strlen(line) == 0 || line[0] == '[') continue; // skip empty or section headers char *eq = strchr(line, '='); if (!eq) { continue; } *eq = '\0'; char *key = line; char *value = eq + 1; trimWhitespace(key); trimWhitespace(value); // Replace dots with underscores for (char *c = key; *c; c++) { if (*c == '.') *c = '_'; } for (size_t i = 0; i < mappingCount; ++i) { if (strcmp(key, "name") == 0) { // Copy theme name safely strncpy(currentTheme->theme_name, value, sizeof(currentTheme->theme_name) - 1); currentTheme->theme_name [sizeof(currentTheme->theme_name) - 1] = '\0'; found = 1; break; } if (strcmp(key, "author") == 0) { // Copy theme name safely strncpy(currentTheme->theme_author, value, sizeof(currentTheme->theme_author) - 1); currentTheme->theme_author [sizeof(currentTheme->theme_author) - 1] = '\0'; found = 1; break; } else if (strcmp(key, mappings[i].key) == 0) { ColorValue color; if (!parseColorValue(value, &color)) { fprintf(stderr, "Invalid color value at line " "%d: %s\n", lineNum, value); } else { *(mappings[i].field) = color; found = 1; } break; } } } fclose(file); return found; } kew/src/theme.h000066400000000000000000000036271507107350600137220ustar00rootroot00000000000000#ifndef PIXELDATA_STRUCT #define PIXELDATA_STRUCT #include #include typedef struct { unsigned char r; unsigned char g; unsigned char b; } PixelData; #endif #ifndef THEME_STRUCT #define THEME_STRUCT typedef enum { COLOR_TYPE_RGB, COLOR_TYPE_ANSI } ColorType; typedef struct { ColorType type; union { PixelData rgb; int8_t ansiIndex; // -1 to 15 for 16 colors + -1 = foreground }; } ColorValue; typedef struct { char theme_name[NAME_MAX]; char theme_author[NAME_MAX]; ColorValue accent; ColorValue text; ColorValue textDim; ColorValue textMuted; ColorValue logo; ColorValue header; ColorValue footer; ColorValue help; ColorValue link; ColorValue nowplaying; ColorValue playlist_rownum; ColorValue playlist_title; ColorValue playlist_playing; ColorValue trackview_title; ColorValue trackview_artist; ColorValue trackview_album; ColorValue trackview_year; ColorValue trackview_time; ColorValue trackview_visualizer; ColorValue trackview_lyrics; ColorValue library_artist; ColorValue library_album; ColorValue library_track; ColorValue library_enqueued; ColorValue library_playing; ColorValue search_label; ColorValue search_query; ColorValue search_result; ColorValue search_enqueued; ColorValue search_playing; ColorValue progress_filled; ColorValue progress_empty; ColorValue progress_elapsed; ColorValue progress_duration; ColorValue status_info; ColorValue status_warning; ColorValue status_error; ColorValue status_success; } Theme; #endif int loadThemeFromFile(const char *themesDir, const char *filename, Theme *currentTheme); kew/src/utils.c000066400000000000000000000447601507107350600137560ustar00rootroot00000000000000#include "utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* 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 uint32_t #include // For arc4random uint32_t arc4random_uniform(uint32_t upper_bound); int getRandomNumber(int min, int max) { return min + arc4random_uniform(max - min + 1); } #else #include #include int getRandomNumber(int min, int max) { static int seeded = 0; if (!seeded) { srand(time( NULL)); // Use srandom() to seed the random number generator seeded = 1; } return min + (rand() % (max - min + 1)); // Use random() instead of rand() } #endif void c_sleep(int milliseconds) { struct timespec ts; ts.tv_sec = milliseconds / 1000; // Seconds part // Ensure that the nanoseconds part is computed safely, and no overflow // happens ts.tv_nsec = (milliseconds % 1000) * 1000000; // Make sure that nanoseconds is within valid range if (ts.tv_nsec >= 1000000000) { ts.tv_sec += ts.tv_nsec / 1000000000; ts.tv_nsec %= 1000000000; } nanosleep(&ts, NULL); } void c_usleep(int microseconds) { if (microseconds < 0 || microseconds > 100000000) // Max 100 seconds { return; } struct timespec ts; ts.tv_sec = microseconds / 1000000; ts.tv_nsec = (microseconds % 1000000) * 1000; // Convert remaining microseconds to nanoseconds nanosleep(&ts, NULL); } void c_strcpy(char *dest, const char *src, size_t dest_size) { // Ensure the destination and source are valid, and dest_size is large // enough to hold at least one byte if (dest && src && dest_size > 0) { // Calculate the length of the source string, limited by // dest_size - 1 (for the null terminator) size_t src_length = strnlen(src, dest_size - 1); // Ensure we do not write beyond dest_size - 1 if (src_length >= dest_size) src_length = dest_size - 1; // Safely copy up to src_length bytes from src to dest memcpy(dest, src, src_length); // Null-terminate the destination string dest[src_length] = '\0'; } else if (dest && dest_size > 0) // If source is NULL, we clear the destination buffer { dest[0] = '\0'; } } char *stringToLower(const char *str) { if (str == NULL) { return NULL; } size_t length = strnlen(str, MAXPATHLEN); return g_utf8_strdown(str, length); } char *stringToUpper(const char *str) { if (str == NULL) { return NULL; } size_t length = strnlen(str, MAXPATHLEN); return g_utf8_strup(str, length); } char *c_strcasestr(const char *haystack, const char *needle, int maxScanLen) { if (!haystack || !needle || maxScanLen <= 0) return NULL; size_t needleLen = strnlen(needle, maxScanLen); if (needleLen == 0) return (char *)haystack; 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]; int ret = regexec(regex, ext, 1, pmatch, 0); if (ret == REG_NOMATCH) { return 1; } else if (ret == 0) { return 0; } else { fprintf(stderr, "match_regex: Regex match failed"); return 1; } } bool isValidUTF8(const char *str, size_t len) { size_t i = 0; while (i < len) { unsigned char c = str[i]; if (c <= 0x7F) // 1-byte ASCII character { i++; } else if ((c & 0xE0) == 0xC0) // 2-byte sequence { if (i + 1 >= len || (str[i + 1] & 0xC0) != 0x80) return false; i += 2; } else if ((c & 0xF0) == 0xE0) // 3-byte sequence { if (i + 2 >= len || (str[i + 1] & 0xC0) != 0x80 || (str[i + 2] & 0xC0) != 0x80) return false; i += 3; } else if ((c & 0xF8) == 0xF0) // 4-byte sequence { if (i + 3 >= len || (str[i + 1] & 0xC0) != 0x80 || (str[i + 2] & 0xC0) != 0x80 || (str[i + 3] & 0xC0) != 0x80) return false; i += 4; } else { return false; // Invalid UTF-8 } } return true; } void extractExtension(const char *filename, size_t ext_size, char *ext) { if (!filename || !ext || ext_size == 0) { if (ext && ext_size > 0) ext[0] = '\0'; return; } 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; size_t dot_pos = dot - filename + 1; // Copy the extension while checking for UTF-8 validity while (dot_pos + i < length && filename[dot_pos + i] != '\0' && j < ext_size - 1) { size_t char_size = 1; // Default to 1 byte (ASCII) unsigned char c = filename[dot_pos + i]; if ((c & 0x80) != 0) // Check if the character is multi-byte { if ((c & 0xE0) == 0xC0) // 2-byte sequence char_size = 2; else if ((c & 0xF0) == 0xE0) // 3-byte sequence char_size = 3; else if ((c & 0xF8) == 0xF0) // 4-byte sequence char_size = 4; else { break; // Invalid UTF-8 start byte } } // Ensure we don't overflow the destination buffer if (j + char_size >= ext_size) break; // Check if the character is valid UTF-8 if (isValidUTF8(&filename[dot_pos + i], char_size)) { // Copy the character to the extension buffer memcpy(ext + j, filename + dot_pos + i, char_size); j += char_size; i += char_size; } else { break; // Invalid UTF-8, stop copying } } // Null-terminate the extension 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) { if (!str || maxLen <= 0) { return; } // Find start (skip leading whitespace) char *start = str; while (*start && isspace(*start)) { start++; } // Handle case where string is all whitespace or empty size_t len = strnlen(start, maxLen - (start - str)); if (len == 0) { str[0] = '\0'; return; } // Find end (skip trailing whitespace) char *end = start + len - 1; while (end >= start && isspace(*end)) { end--; } // Null terminate *(end + 1) = '\0'; // Move trimmed string to beginning if needed if (start != str) { size_t trimmed_len = end - start + 1; memmove(str, start, trimmed_len + 1); // +1 for null terminator } } 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; } bool isValidFilename(const char *filename) { // Check for path traversal patterns if (strstr(filename, "..") != NULL) { return false; } // Check for path separators (works for UTF-8) if (strchr(filename, '/') != NULL || strchr(filename, '\\') != NULL) { return false; } // Don't allow starting with dot (hidden files) if (filename[0] == '.') { return false; } // Allow everything else (including UTF-8 Chinese characters) return true; } char *getFilePath(const char *filename) { if (filename == NULL || !isValidFilename(filename)) { return NULL; } // Also check it doesn't start with a dot (hidden files) if (filename[0] == '.') { 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; } int copyFile(const char *src, const char *dst) { // Validate inputs if (!src || !dst) { return -1; } // Check if source and destination are the same struct stat src_stat, dst_stat; if (stat(src, &src_stat) != 0) { return -1; } // Don't copy if destination exists and is the same file (same inode) if (stat(dst, &dst_stat) == 0) { if (src_stat.st_dev == dst_stat.st_dev && src_stat.st_ino == dst_stat.st_ino) { return -1; // Same file } } // Don't copy directories, symlinks, or special files if (!S_ISREG(src_stat.st_mode)) { return -1; } // Check file size is reasonable (prevent copying huge files) if (src_stat.st_size > 10 * 1024 * 1024) { // 10MB limit for theme files return -1; } // Open source file int src_fd = open(src, O_RDONLY); if (src_fd < 0) { return -1; } // Create destination with user read/write permissions int dst_fd = open(dst, O_WRONLY | O_CREAT | O_EXCL, 0600); if (dst_fd < 0) { // If file exists, try to open it (but don't use O_EXCL) dst_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0600); if (dst_fd < 0) { close(src_fd); return -1; } } // Copy data in chunks char buffer[8192]; ssize_t bytes_read, bytes_written; ssize_t total_written = 0; while ((bytes_read = read(src_fd, buffer, sizeof(buffer))) > 0) { bytes_written = write(dst_fd, buffer, bytes_read); if (bytes_written != bytes_read) { close(src_fd); close(dst_fd); unlink(dst); // Remove partial file on error return -1; } total_written += bytes_written; // Sanity check: make sure we're not writing more than expected if (total_written > src_stat.st_size) { close(src_fd); close(dst_fd); unlink(dst); return -1; } } if (bytes_read < 0) { close(src_fd); close(dst_fd); unlink(dst); // Remove partial file on error return -1; } // Sync to disk before closing if (fsync(dst_fd) != 0) { close(src_fd); close(dst_fd); unlink(dst); return -1; } close(src_fd); close(dst_fd); return 0; } kew/src/utils.h000066400000000000000000000023371507107350600137550ustar00rootroot00000000000000#ifndef UTILS_H #define UTILS_H #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L #endif #ifndef __USE_POSIX #define __USE_POSIX #endif #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); int copyFile(const char *src, const char *dst); #endif kew/src/visuals.c000066400000000000000000000523471507107350600143040ustar00rootroot00000000000000#include #include #include #include #include #include #include "appstate.h" #include "sound.h" #include "term.h" #include "utils.h" #include "common_ui.h" #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 isFftInitialized = false; float *fftInput = NULL; fftwf_complex *fftOutput = NULL; int bufferIndex = 0; ma_format format = ma_format_unknown; ma_uint32 sampleRate = 0; float barHeight[MAX_BARS] = {0.0f}; float displayMagnitudes[MAX_BARS] = {0.0f}; float smoothed[MAX_BARS] = {0.0f}; float magnitudes[MAX_BARS] = {0.0f}; float dBFloor = -60.0f; float dBCeil = -18.0f; float emphasis = 1.3f; float fastAttack = 0.6f; float decay = 0.14f; float slowAttack = 0.15f; int visualizerBarWidth = 2; int maxThinBarsInAutoMode = 20; void clearMagnitudes(int numBars, float *magnitudes) { for (int i = 0; i < numBars; i++) { magnitudes[i] = 0.0f; } } void applyBlackmanHarris(float *fftInput, int bufferSize) { if (!fftInput || bufferSize < 2) // Must be at least 2 to avoid division by zero return; const float alpha0 = 0.35875f; const float alpha1 = 0.48829f; const float alpha2 = 0.14128f; const float alpha3 = 0.01168f; float denom = (float)(bufferSize - 1); for (int i = 0; i < bufferSize; i++) { float fraction = (float)i / denom; 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; } } // Fill center freqs for 1/3-octave bands, given min/max freq and numBands void computeBandCenters(float minFreq, float sampleRate, int numBands, float *centerFreqs) { if (!centerFreqs || numBands <= 0 || minFreq <= 0 || sampleRate <= 0) return; float nyquist = sampleRate * 0.5f; float octaveFraction = 1.0f / 3.0f; // 1/3 octave float factor = powf(2.0f, octaveFraction); float f = minFreq; // Ensure we don't exceed the Nyquist frequency for (int i = 0; i < numBands; i++) { if (f > nyquist) { centerFreqs[i] = nyquist; // Clamp remaining bands at Nyquist } else { centerFreqs[i] = f; f *= factor; // Safeguard against overflow in case 'f' grows too // large if (f > nyquist) { f = nyquist; } } } } void fillEQBands(const fftwf_complex *fftOutput, int bufferSize, float sampleRate, float *bandDb, int numBands, const float *centerFreqs) { // Basic input checks if (!fftOutput || !bandDb || !centerFreqs || bufferSize <= 0 || numBands <= 0 || sampleRate <= 0.0f) return; // Guard against potential overflow in bin count computation if (bufferSize > INT_MAX - 1) return; int numBins = bufferSize / 2 + 1; // Safe binSpacing computation float binSpacing = sampleRate / (float)bufferSize; if (binSpacing <= 0.0f || !isfinite(binSpacing)) return; // Prevent division by zero in normalization float normFactor = (float)bufferSize; if (normFactor <= 0.0f) return; // Frequency window width for 1/3-octave bands const float width = powf(2.0f, 1.0f / 6.0f); // +/-1/6 octave // Pink noise correction const float correctionPerOctave = 3.0f; const float maxFreqForCorrection = 10000.0f; const float nyquist = sampleRate * 0.5f; // Make sure referenceFreq is safe float referenceFreq = fmaxf(centerFreqs[0], 1.0f); if (!isfinite(referenceFreq) || referenceFreq <= 0.0f) referenceFreq = 1.0f; for (int i = 0; i < numBands; i++) { float center = centerFreqs[i]; if (!isfinite(center) || center <= 0.0f || center > nyquist) { bandDb[i] = -INFINITY; continue; } float lo = center / width; float hi = center * width; // Avoid integer overflows in bin index computation int binLo = (int)ceilf(lo / binSpacing); int binHi = (int)floorf(hi / binSpacing); binLo = (binLo < 0) ? 0 : binLo; binHi = (binHi >= numBins) ? numBins - 1 : binHi; binHi = (binHi < binLo) ? binLo : binHi; float sumSq = 0.0f; int count = 0; for (int k = binLo; k <= binHi; k++) { if (k < 0 || k >= numBins) continue; float real = fftOutput[k][0] / normFactor; float imag = fftOutput[k][1] / normFactor; float mag = sqrtf(real * real + imag * imag); sumSq += mag * mag; count++; } float rms = (count > 0) ? sqrtf(sumSq / count) : 1e-9f; // Small nonzero floor bandDb[i] = 20.0f * log10f(rms); // Pink noise EQ compensation float freq = fminf(center, maxFreqForCorrection); float octavesAboveRef = log2f(freq / referenceFreq); float correction = fmaxf(octavesAboveRef, 0.0f) * correctionPerOctave; bandDb[i] += correction; } } int normalizeAudioSamples(const void *audioBuffer, float *fftInput, int bufferSize, int bitDepth) { if (bitDepth == 8) { const uint8_t *buf = (const uint8_t *)audioBuffer; for (int i = 0; i < bufferSize; ++i) fftInput[i] = ((float)buf[i] - 127.0f) / 128.0f; } else if (bitDepth == 16) { const int16_t *buf = (const int16_t *)audioBuffer; for (int i = 0; i < bufferSize; ++i) fftInput[i] = (float)buf[i] / 32768.0f; } else if (bitDepth == 24) { const uint8_t *buf = (const uint8_t *)audioBuffer; for (int i = 0; i < bufferSize; ++i) { int32_t sample = unpack_s24(buf + i * 3); fftInput[i] = (float)sample / 8388608.0f; } } else if (bitDepth == 32) { const float *buf = (const float *)audioBuffer; for (int i = 0; i < bufferSize; ++i) fftInput[i] = buf[i]; } else { // Unsupported bit depth return -1; } return 0; } void calcMagnitudes(int height, int numBars, void *audioBuffer, int bitDepth, float *fftInput, fftwf_complex *fftOutput, int fftSize, float *magnitudes, fftwf_plan plan, float *displayMagnitudes) { // Only execute when we get the signal that we have enough samples // (fftSize) if (!bufferReady) return; if (!audioBuffer) { fprintf(stderr, "Audio buffer is NULL.\n"); return; } bufferReady = false; normalizeAudioSamples(audioBuffer, fftInput, fftSize, bitDepth); // Apply Blackman Harris window function applyBlackmanHarris(fftInput, fftSize); // Compute fast fourier transform fftwf_execute(plan); // Clear previous magnitudes clearMagnitudes(MAX_BARS, magnitudes); float centerFreqs[numBars]; float minFreq = 25.0f; float audibleHalf = 10000.0f; float maxFreq = fmin(audibleHalf, 0.5f * sampleRate); float octaveFraction = 1.0f / 3.0f; int usedBars = floor(log2(maxFreq / minFreq) / octaveFraction) + 1; // How many bars are actually in use, given we // increase with 1/3 octave per bar // Compute center frequencies for EQ bands computeBandCenters(minFreq, maxFreq, numBars, centerFreqs); // Fill magnitudes for EQ bands from FFT output fillEQBands(fftOutput, fftSize, sampleRate, magnitudes, numBars, centerFreqs); // Map magnitudes (in dB) to bar heights with gating and emphasis // (pow/gated) for (int i = 0; i < usedBars; ++i) { float db = magnitudes[i]; if (db < dBFloor) db = dBFloor; if (db > dBCeil) db = dBCeil; float ratio = (db - dBFloor) / (dBCeil - dBFloor); ratio = powf(ratio, emphasis); if (ratio < 0.1f) barHeight[i] = 0.0f; // Gate out tiny bars else barHeight[i] = ratio * height; } float snapThreshold = 0.2f * height; // Smoothly update display magnitudes with attack/decay and snap // threshold for (int i = 0; i < usedBars; ++i) { float current = displayMagnitudes[i]; float target = barHeight[i]; float delta = target - current; if (delta > snapThreshold) displayMagnitudes[i] += delta * fastAttack; // SNAP on big hits else if (delta > 0) displayMagnitudes[i] += delta * slowAttack; else displayMagnitudes[i] += delta * decay; } } 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 getBitDepth(ma_format format) { 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; } return bitDepth; } void printSpectrum(int row, int col, UISettings *ui, int height, int numBars, int visualizerWidth, float *magnitudes) { PixelData color; if (ui->colorMode == COLOR_MODE_ALBUM) { color.r = ui->color.r; color.g = ui->color.g; color.b = ui->color.b; } else if (ui->colorMode == COLOR_MODE_THEME && ui->theme.trackview_visualizer.type == COLOR_TYPE_RGB) { color.r = ui->theme.trackview_visualizer.rgb.r; color.g = ui->theme.trackview_visualizer.rgb.g; color.b = ui->theme.trackview_visualizer.rgb.b; } int visualizerColorType = ui->visualizerColorType; bool brailleMode = ui->visualizerBrailleMode; PixelData tmp; bool isPlaying = !(isPaused() || isStopped()); for (int j = height; j > 0 && !isPlaying; j--) { printf("\033[%d;%dH", row, col); clearRestOfLine(); } for (int j = height; j > 0 && isPlaying; j--) { printf("\033[%d;%dH", row + height - j, col); if (color.r != 0 || color.g != 0 || color.b != 0) { if ((visualizerColorType == 0 || visualizerColorType == 2 || visualizerColorType == 3)) { if (visualizerColorType == 0) { tmp = increaseLuminosity( color, round(j * height * 4)); } else if (visualizerColorType == 2) { tmp = increaseLuminosity( color, round((height - j) * height * 4)); } else if (visualizerColorType == 3) { tmp = getGradientColor(color, j, height, 1, 0.6f); } } } if (ui->colorMode == COLOR_MODE_ALBUM) printf("\033[38;2;%d;%d;%dm", tmp.r, tmp.g, tmp.b); else if (ui->theme.trackview_visualizer.type == COLOR_TYPE_RGB) { printf("\033[38;2;%d;%d;%dm", tmp.r, tmp.g, tmp.b); } else applyColor(ui->colorMode, ui->theme.trackview_visualizer, tmp); for (int i = 0; i < numBars; i++) { if (ui->colorMode != COLOR_MODE_DEFAULT && 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)); if (visualizerBarWidth == 1 || (visualizerBarWidth == 2 && visualizerWidth > maxThinBarsInAutoMode)) printf("%s", getUpwardMotionChar( 10, brailleMode)); } else if (magnitudes[i] + 1 >= j) { int firstDecimalDigit = (int)(fmod(magnitudes[i] * 10, 10)); printf("%s", getUpwardMotionChar(firstDecimalDigit, brailleMode)); if (visualizerBarWidth == 1 || (visualizerBarWidth == 2 && visualizerWidth > maxThinBarsInAutoMode)) printf("%s", getUpwardMotionChar( firstDecimalDigit, brailleMode)); } else { printf(" "); if (visualizerBarWidth == 1 || (visualizerBarWidth == 2 && visualizerWidth > maxThinBarsInAutoMode)) printf(" "); } } } fflush(stdout); } void freeVisuals(void) { if (fftInput != NULL) { free(fftInput); fftInput = NULL; } if (fftOutput != NULL) { fftwf_free(fftOutput); fftOutput = NULL; } } void drawSpectrumVisualizer(int row, int col, int height, AppState *state) { int numBars = state->uiState.numProgressBars; int visualizerWidth = state->uiState.numProgressBars; visualizerBarWidth = state->uiSettings.visualizerBarWidth; if (visualizerBarWidth == 1 || (visualizerBarWidth == 2 && visualizerWidth > maxThinBarsInAutoMode)) numBars *= 0.67f; height -= 1; if (height <= 0 || numBars <= 0) { return; } if (numBars > MAX_BARS) numBars = MAX_BARS; if (fftSize != prevFftSize) { freeVisuals(); memset(displayMagnitudes, 0, sizeof(displayMagnitudes)); fftInput = (float *)malloc(sizeof(float) * fftSize); if (fftInput == NULL) { for (int i = 0; i <= height; i++) { printf("\n"); } return; } fftOutput = (fftwf_complex *)fftwf_malloc( sizeof(fftwf_complex) * fftSize); if (fftOutput == NULL) { fftwf_free(fftInput); fftInput = NULL; for (int i = 0; i <= height; i++) { printf("\n"); } return; } prevFftSize = fftSize; } fftwf_plan plan = fftwf_plan_dft_r2c_1d(fftSize, fftInput, fftOutput, FFTW_ESTIMATE); getCurrentFormatAndSampleRate(&format, &sampleRate); int bitDepth = getBitDepth(format); calcMagnitudes(height, numBars, getAudioBuffer(), bitDepth, fftInput, fftOutput, fftSize, magnitudes, plan, displayMagnitudes); printSpectrum(row, col, &(state->uiSettings), height, numBars, visualizerWidth, displayMagnitudes); fftwf_destroy_plan(plan); } kew/src/visuals.h000066400000000000000000000005061507107350600142770ustar00rootroot00000000000000 #include "appstate.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(int row, int col, int height, AppState *state); kew/src/webm.h000066400000000000000000001175761507107350600135630ustar00rootroot00000000000000#ifndef WEBM_H #define WEBM_H #ifdef __cplusplus extern "C" { #endif #include "miniaudio.h" #if !defined(MA_NO_WEBM) #include #include #include #endif typedef struct { ma_data_source_base ds; /* The webm 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; /* Will be f32 */ #if !defined(MA_NO_WEBM) ma_uint64 audioTrack; ma_uint64 cursorInPCMFrames; ma_uint64 seekTargetPCMFrame; ma_uint32 sampleRate; ma_uint32 channels; ma_uint64 lengthInPCMFrames; double duration; // Nestegg fields nestegg *ctx; unsigned int codec_id; nestegg_packet *currentPacket; unsigned int numFramesInPacket; unsigned int currentPacketFrame; ma_bool32 hasPacket; // Opus fields OpusDecoder *opusDecoder; // Vorbis fields vorbis_block vorbisBlock; vorbis_dsp_state vorbisDSP; vorbis_comment vorbisComment; vorbis_info vorbisInfo; ma_uint16 opusPreSkip; ma_uint16 preSkipLeft; ma_uint64 bufferLeftoverFrameCount; ma_uint64 bufferLeftoverFrameOffset; #endif } ma_webm; MA_API ma_result ma_webm_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, ma_webm *pWebm); MA_API ma_result ma_webm_init_file(const char *pFilePath, const ma_decoding_backend_config *pConfig, const ma_allocation_callbacks *pAllocationCallbacks, ma_webm *pWebm); MA_API void ma_webm_uninit(ma_webm *pOpus, const ma_allocation_callbacks *pAllocationCallbacks); MA_API ma_result ma_webm_read_pcm_frames(ma_webm *pWebm, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead); MA_API ma_result ma_webm_seek_to_pcm_frame(ma_webm *pWebm, ma_uint64 frameIndex); MA_API ma_result ma_webm_get_data_format(ma_webm *pOpus, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap); MA_API ma_result ma_webm_get_cursor_in_pcm_frames(ma_webm *pWebm, ma_uint64 *pCursor); MA_API ma_result ma_webm_get_length_in_pcm_frames(ma_webm *pWebm, ma_uint64 *pLength); #ifdef __cplusplus } #endif #endif #if defined(MINIAUDIO_IMPLEMENTATION) || defined(MA_IMPLEMENTATION) #define MAX_OPUS_CHANNELS 8 #define MAX_OPUS_SAMPLES 5760 // Maximum expected frame size static float opusLeftoverBuffer[MAX_OPUS_SAMPLES * MAX_OPUS_CHANNELS]; #define MAX_VORBIS_PACKET_FRAMES 4096 #define MAX_VORBIS_CHANNELS 8 float vorbisLeftoverBuffer[MAX_VORBIS_PACKET_FRAMES * MAX_VORBIS_CHANNELS]; static ma_result ma_webm_ds_read(ma_data_source *pDataSource, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { return ma_webm_read_pcm_frames((ma_webm *)pDataSource, pFramesOut, frameCount, pFramesRead); } static ma_result ma_webm_ds_seek(ma_data_source *pDataSource, ma_uint64 frameIndex) { return ma_webm_seek_to_pcm_frame((ma_webm *)pDataSource, frameIndex); } static ma_result ma_webm_ds_get_data_format(ma_data_source *pDataSource, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap) { return ma_webm_get_data_format((ma_webm *)pDataSource, pFormat, pChannels, pSampleRate, pChannelMap, channelMapCap); } static ma_result ma_webm_ds_get_cursor(ma_data_source *pDataSource, ma_uint64 *pCursor) { return ma_webm_get_cursor_in_pcm_frames((ma_webm *)pDataSource, pCursor); } static ma_result ma_webm_ds_get_length(ma_data_source *pDataSource, ma_uint64 *pLength) { return ma_webm_get_length_in_pcm_frames((ma_webm *)pDataSource, pLength); } static ma_data_source_vtable g_ma_webm_ds_vtable = { ma_webm_ds_read, ma_webm_ds_seek, ma_webm_ds_get_data_format, ma_webm_ds_get_cursor, ma_webm_ds_get_length, NULL, (ma_uint64)0}; static ma_result ma_webm_init_internal(const ma_decoding_backend_config *pConfig, ma_webm *pWebm) { ma_result result; ma_data_source_config dataSourceConfig; if (pWebm == NULL) { return MA_INVALID_ARGS; } MA_ZERO_OBJECT(pWebm); pWebm->format = ma_format_f32; // f32 by default. pWebm->seekTargetPCMFrame = (ma_uint64)-1; // Clear leftover buffer pWebm->bufferLeftoverFrameCount = 0; pWebm->bufferLeftoverFrameOffset = 0; if (pConfig != NULL && (pConfig->preferredFormat == ma_format_f32 || pConfig->preferredFormat == ma_format_s16)) { pWebm->format = pConfig->preferredFormat; } else { /* Getting here means something other than f32 and s16 was specified. Just leave this unset to use the default format. */ } dataSourceConfig = ma_data_source_config_init(); dataSourceConfig.vtable = &g_ma_webm_ds_vtable; result = ma_data_source_init(&dataSourceConfig, &pWebm->ds); if (result != MA_SUCCESS) { return result; /* Failed to initialize the base data source. */ } return MA_SUCCESS; } static int nestegg_io_read(void *buffer, size_t length, void *userdata) { ma_webm *webm = (ma_webm *)userdata; size_t bytesRead = 0; if (webm->onRead(webm->pReadSeekTellUserData, buffer, length, &bytesRead) == MA_SUCCESS) return (bytesRead == length) ? 1 : 0; return -1; } static int nestegg_io_seek(int64_t offset, int whence, void *userdata) { ma_webm *webm = (ma_webm *)userdata; ma_seek_origin origin; switch (whence) { case NESTEGG_SEEK_SET: origin = ma_seek_origin_start; break; case NESTEGG_SEEK_CUR: origin = ma_seek_origin_current; break; case NESTEGG_SEEK_END: origin = ma_seek_origin_end; break; default: return -1; } return (webm->onSeek(webm->pReadSeekTellUserData, offset, origin) == MA_SUCCESS) ? 0 : -1; } static int64_t nestegg_io_tell(void *userdata) { ma_webm *webm = (ma_webm *)userdata; ma_int64 pos = 0; return (webm->onTell(webm->pReadSeekTellUserData, &pos) == MA_SUCCESS) ? pos : -1; } double calcWebmDuration(nestegg *ctx) { double duration = 0.0f; uint64_t duration_ns = 0; if (nestegg_duration(ctx, &duration_ns) == 0) { duration = (double)duration_ns / 1e9; } return duration; } static int ma_webm_init_vorbis_decoder( nestegg *ctx, unsigned int audioTrack, ma_webm *pWebm) { unsigned char *id = NULL, *comment = NULL, *setup = NULL; size_t id_size = 0, comment_size = 0, setup_size = 0; ogg_packet header_packet; // Fetch header packets as delivered by WebM/Matroska. if (nestegg_track_codec_data(ctx, audioTrack, 0, &id, &id_size) != 0 || nestegg_track_codec_data(ctx, audioTrack, 1, &comment, &comment_size) != 0 || nestegg_track_codec_data(ctx, audioTrack, 2, &setup, &setup_size) != 0) { return -1; // invalid file or track } // Setup libvorbis structures. vorbis_info_init(&pWebm->vorbisInfo); vorbis_comment_init(&pWebm->vorbisComment); memset(&header_packet, 0, sizeof(header_packet)); // Header 1: ID header_packet.packet = id; header_packet.bytes = id_size; header_packet.b_o_s = 1; header_packet.e_o_s = 0; if (vorbis_synthesis_headerin(&pWebm->vorbisInfo, &pWebm->vorbisComment, &header_packet) != 0) goto fail; // Header 2: COMMENT header_packet.packet = comment; header_packet.bytes = comment_size; header_packet.b_o_s = 0; // header_packet.e_o_s remains 0 if (vorbis_synthesis_headerin(&pWebm->vorbisInfo, &pWebm->vorbisComment, &header_packet) != 0) goto fail; // Header 3: SETUP header_packet.packet = setup; header_packet.bytes = setup_size; // header_packet.b_o_s remains 0 if (vorbis_synthesis_headerin(&pWebm->vorbisInfo, &pWebm->vorbisComment, &header_packet) != 0) goto fail; // Setup decoder if (vorbis_synthesis_init(&pWebm->vorbisDSP, &pWebm->vorbisInfo) != 0) goto fail; if (vorbis_block_init(&pWebm->vorbisDSP, &pWebm->vorbisBlock) != 0) goto fail; pWebm->channels = pWebm->vorbisInfo.channels; pWebm->sampleRate = pWebm->vorbisInfo.rate; pWebm->format = ma_format_f32; return 0; // success fail: vorbis_block_clear(&pWebm->vorbisBlock); vorbis_dsp_clear(&pWebm->vorbisDSP); vorbis_comment_clear(&pWebm->vorbisComment); vorbis_info_clear(&pWebm->vorbisInfo); return -2; // error } MA_API ma_result ma_webm_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, ma_webm *pWebm) { ma_result result; (void)pAllocationCallbacks; result = ma_webm_init_internal(pConfig, pWebm); if (result != MA_SUCCESS) { return result; } if (onRead == NULL || onSeek == NULL) { return MA_INVALID_ARGS; /* onRead and onSeek are mandatory. */ } pWebm->onRead = onRead; pWebm->onSeek = onSeek; pWebm->onTell = onTell; pWebm->pReadSeekTellUserData = pReadSeekTellUserData; #if !defined(MA_NO_WEBM) nestegg_io io = {0}; // Adapter functions for nestegg io.read = nestegg_io_read; io.seek = nestegg_io_seek; io.tell = nestegg_io_tell; io.userdata = pWebm; nestegg *ctx = NULL; if (nestegg_init(&ctx, io, NULL, -1) < 0) { return MA_INVALID_FILE; } // Find Audio Track unsigned int num_tracks = 0; if (nestegg_track_count(ctx, &num_tracks) != 0) { nestegg_destroy(ctx); return MA_INVALID_FILE; } pWebm->audioTrack = (unsigned int)-1; pWebm->codec_id = -1; for (unsigned int i = 0; i < num_tracks; ++i) { unsigned int type = 0; type = nestegg_track_type(ctx, i); if (type == NESTEGG_TRACK_AUDIO) { pWebm->audioTrack = i; pWebm->codec_id = nestegg_track_codec_id(ctx, i); if (pWebm->codec_id == 0) { break; // first audio } } } if (pWebm->audioTrack == (unsigned int)-1) { nestegg_destroy(ctx); return MA_INVALID_FILE; } // Prepare decoder if (pWebm->codec_id == NESTEGG_CODEC_OPUS) { unsigned char *header = NULL; size_t header_size = 0; if (nestegg_track_codec_data(ctx, pWebm->audioTrack, 0, &header, &header_size) != 0 || header_size < 19 || memcmp(header, "OpusHead", 8) != 0) { nestegg_destroy(ctx); return MA_INVALID_FILE; } pWebm->sampleRate = 48000; pWebm->channels = header[9]; ma_uint16 preSkip = header[10] | (header[11] << 8); // Little-endian pWebm->opusPreSkip = preSkip; pWebm->preSkipLeft = preSkip; int opusErr = 0; pWebm->opusDecoder = opus_decoder_create(pWebm->sampleRate, pWebm->channels, &opusErr); if (!pWebm->opusDecoder) { nestegg_destroy(ctx); return MA_INVALID_FILE; } pWebm->format = ma_format_f32; } else if (pWebm->codec_id == NESTEGG_CODEC_VORBIS) { if (ma_webm_init_vorbis_decoder(ctx, pWebm->audioTrack, pWebm) != 0) { nestegg_destroy(ctx); return MA_INVALID_FILE; } } else { nestegg_destroy(ctx); return MA_NOT_IMPLEMENTED; } pWebm->ctx = ctx; pWebm->duration = calcWebmDuration(ctx); pWebm->seekTargetPCMFrame = (ma_uint64)(-1); return MA_SUCCESS; #else (void)pReadSeekTellUserData; (void)pConfig; (void)pWebm; return MA_NOT_IMPLEMENTED; #endif } int nread(void *buf, size_t len, void *ud) { FILE *f = (FILE *)ud; size_t r = fread(buf, 1, len, f); if (r == len) return 1; if (feof(f)) return 0; return -1; } int nseek(int64_t o, int w, void *ud) { FILE *f = (FILE *)ud; int wh; switch (w) { case NESTEGG_SEEK_SET: wh = SEEK_SET; break; case NESTEGG_SEEK_CUR: wh = SEEK_CUR; break; case NESTEGG_SEEK_END: wh = SEEK_END; break; default: return -1; } return fseek(f, (long)o, wh); } int64_t ntell(void *ud) { FILE *f = (FILE *)ud; return ftell(f); } MA_API ma_result ma_webm_init_file(const char *pFilePath, const ma_decoding_backend_config *pConfig, const ma_allocation_callbacks *pAllocationCallbacks, ma_webm *pWebm) { ma_result result; (void)pAllocationCallbacks; result = ma_webm_init_internal(pConfig, pWebm); if (result != MA_SUCCESS) { return result; } #if !defined(MA_NO_WEBM) FILE *fp = fopen(pFilePath, "rb"); if (!fp) return MA_INVALID_FILE; nestegg_io io = {nread, nseek, ntell, fp}; nestegg *ctx = NULL; if (nestegg_init(&ctx, io, NULL, -1) < 0) { fclose(fp); return MA_INVALID_FILE; } unsigned int num_tracks = 0; nestegg_track_count(ctx, &num_tracks); pWebm->audioTrack = (unsigned int)(-1); pWebm->codec_id = -1; for (unsigned int i = 0; i < num_tracks; ++i) { unsigned int type = 0; type = nestegg_track_type(ctx, i); if (type == NESTEGG_TRACK_AUDIO) { pWebm->audioTrack = i; pWebm->codec_id = nestegg_track_codec_id(ctx, i); break; } } if (pWebm->audioTrack == (unsigned int)(-1)) { nestegg_destroy(ctx); fclose(fp); return MA_ERROR; } // Fetch and handle header if (pWebm->codec_id == NESTEGG_CODEC_OPUS) { unsigned char *header = NULL; size_t header_size = 0; nestegg_track_codec_data(ctx, pWebm->audioTrack, 0, &header, &header_size); if (header_size < 19 || memcmp(header, "OpusHead", 8) != 0) { nestegg_destroy(ctx); fclose(fp); return MA_ERROR; } pWebm->channels = header[9]; ma_uint16 preSkip = header[10] | (header[11] << 8); // Little-endian pWebm->opusPreSkip = preSkip; pWebm->preSkipLeft = preSkip; int opusErr = 0; pWebm->opusDecoder = opus_decoder_create(48000, pWebm->channels, &opusErr); if (!pWebm->opusDecoder) { nestegg_destroy(ctx); fclose(fp); return MA_ERROR; } pWebm->format = ma_format_f32; pWebm->sampleRate = 48000; } else if (pWebm->codec_id == NESTEGG_CODEC_VORBIS) { if (ma_webm_init_vorbis_decoder(ctx, pWebm->audioTrack, pWebm) != 0) { nestegg_destroy(ctx); return MA_INVALID_FILE; } } else { nestegg_destroy(ctx); fclose(fp); return MA_NOT_IMPLEMENTED; } pWebm->ctx = ctx; pWebm->duration = calcWebmDuration(ctx); pWebm->seekTargetPCMFrame = (ma_uint64)(-1); return MA_SUCCESS; #else /* webm is disabled. */ (void)pFilePath; return MA_NOT_IMPLEMENTED; #endif } MA_API void ma_webm_uninit(ma_webm *pWebm, const ma_allocation_callbacks *pAllocationCallbacks) { if (pWebm == NULL) { return; } (void)pAllocationCallbacks; #if !defined(MA_NO_WEBM) { if (pWebm->codec_id == NESTEGG_CODEC_OPUS) { opus_decoder_destroy(pWebm->opusDecoder); pWebm->opusDecoder = NULL; } else if (pWebm->codec_id == NESTEGG_CODEC_VORBIS) { vorbis_block_clear(&pWebm->vorbisBlock); vorbis_dsp_clear(&pWebm->vorbisDSP); vorbis_comment_clear(&pWebm->vorbisComment); vorbis_info_clear(&pWebm->vorbisInfo); } if (pWebm->ctx) { nestegg_destroy(pWebm->ctx); pWebm->ctx = NULL; } } #else { /* webm is disabled. Should never hit this since initialization would have failed. */ MA_ASSERT(MA_FALSE); } #endif ma_data_source_uninit(&pWebm->ds); } MA_API ma_result ma_webm_read_pcm_frames(ma_webm *pWebm, void *pFramesOut, ma_uint64 frameCount, ma_uint64 *pFramesRead) { if (pFramesRead) *pFramesRead = 0; if (frameCount == 0 || pWebm == NULL) return MA_INVALID_ARGS; #if !defined(MA_NO_WEBM) ma_result result = MA_SUCCESS; ma_uint64 totalFramesRead = 0; ma_uint32 channels = pWebm->channels; float *f32Out = (float *)pFramesOut; float decodeBuf[MAX_OPUS_SAMPLES * MAX_OPUS_CHANNELS]; // Support up to 8 channels ma_uint64 seekTarget = (pWebm->seekTargetPCMFrame != (ma_uint64)-1) ? pWebm->seekTargetPCMFrame : 0; while (totalFramesRead < frameCount) { ma_uint64 framesNeeded = frameCount - totalFramesRead; // If there's a cached packet/frame in progress, decode that if (!pWebm->hasPacket) { nestegg_packet *pkt = NULL; // Next audio packet... while (nestegg_read_packet(pWebm->ctx, &pkt) > 0) { unsigned int track; nestegg_packet_track(pkt, &track); if (track == pWebm->audioTrack) { pWebm->currentPacket = pkt; pWebm->currentPacketFrame = 0; pWebm->numFramesInPacket = 0; nestegg_packet_count(pkt, &pWebm->numFramesInPacket); pWebm->hasPacket = MA_TRUE; break; } nestegg_free_packet(pkt); // not audio, discard } if (!pWebm->hasPacket) { result = MA_AT_END; // no more data break; } } // Decode remaining frames in this packet/frame nestegg_packet *pkt = pWebm->currentPacket; while (pWebm->currentPacketFrame < pWebm->numFramesInPacket && totalFramesRead < frameCount) { unsigned char *data = NULL; size_t dataSize = 0; nestegg_packet_data(pkt, pWebm->currentPacketFrame, &data, &dataSize); int nframes = 0; if (pWebm->codec_id == NESTEGG_CODEC_OPUS) { if (pWebm->bufferLeftoverFrameCount > 0) { ma_uint64 framesToCopy = pWebm->bufferLeftoverFrameCount < framesNeeded ? pWebm->bufferLeftoverFrameCount : framesNeeded; memcpy(f32Out + totalFramesRead * channels, opusLeftoverBuffer + pWebm->bufferLeftoverFrameOffset * channels, framesToCopy * channels * sizeof(float)); pWebm->bufferLeftoverFrameOffset += framesToCopy; totalFramesRead += framesToCopy; framesNeeded -= framesToCopy; pWebm->bufferLeftoverFrameCount -= framesToCopy; if (pWebm->bufferLeftoverFrameCount == 0) pWebm->bufferLeftoverFrameOffset = 0; if (framesNeeded == 0) break; } nframes = opus_decode_float(pWebm->opusDecoder, data, (opus_int32)dataSize, decodeBuf, 5760, 0); if (nframes < 0) { result = MA_ERROR; break; } ma_uint64 skipFrames = 0; ma_uint64 usableFrames = 0; // On first packets, discard enough to fulfill pre-skip value if (pWebm->preSkipLeft > 0) { if ((ma_uint64)nframes <= pWebm->preSkipLeft) { // All output is to be skipped pWebm->preSkipLeft -= (ma_uint16)nframes; pWebm->cursorInPCMFrames += nframes; // Don't copy anything to output buffer goto NextFrame; } else { // Skip part, keep rest skipFrames = pWebm->preSkipLeft; pWebm->preSkipLeft = 0; } } else if (seekTarget != (ma_uint64)-1 && pWebm->cursorInPCMFrames < seekTarget) { skipFrames = seekTarget - pWebm->cursorInPCMFrames; if (skipFrames > (ma_uint64)nframes) skipFrames = nframes; } usableFrames = (ma_uint64)nframes - skipFrames; if (usableFrames > frameCount - totalFramesRead) usableFrames = frameCount - totalFramesRead; // Only copy if there are any usable frames left if (usableFrames > 0) { memcpy( f32Out + totalFramesRead * channels, decodeBuf + skipFrames * channels, usableFrames * channels * sizeof(float)); totalFramesRead += usableFrames; } ma_uint64 framesUsed = skipFrames + usableFrames; ma_uint64 framesLeft = nframes - framesUsed; if (framesLeft > 0) { memcpy(opusLeftoverBuffer, decodeBuf + framesUsed * channels, framesLeft * channels * sizeof(float)); pWebm->bufferLeftoverFrameCount = framesLeft; pWebm->bufferLeftoverFrameOffset = 0; } else { pWebm->bufferLeftoverFrameCount = 0; pWebm->bufferLeftoverFrameOffset = 0; } // Always advance the PCM cursor by all decoded frames (skipped + copied) pWebm->cursorInPCMFrames += (ma_uint64)nframes; // If we've finished discarding, clear seek mode ("not discarding anymore") if (seekTarget != (ma_uint64)-1 && pWebm->cursorInPCMFrames >= seekTarget) { pWebm->seekTargetPCMFrame = (ma_uint64)-1; } NextFrame:; } else if (pWebm->codec_id == NESTEGG_CODEC_VORBIS) { ogg_packet oggPkt = {0}; oggPkt.packet = data; oggPkt.bytes = (long)dataSize; oggPkt.b_o_s = (pWebm->currentPacketFrame == 0) ? 1 : 0; oggPkt.e_o_s = 0; oggPkt.granulepos = -1; if (pWebm->bufferLeftoverFrameCount > 0) { ma_uint32 avail = pWebm->bufferLeftoverFrameCount - pWebm->bufferLeftoverFrameOffset; ma_uint32 toCopy = (frameCount - totalFramesRead) < avail ? (frameCount - totalFramesRead) : avail; memcpy( f32Out + totalFramesRead * channels, vorbisLeftoverBuffer + pWebm->bufferLeftoverFrameOffset * channels, toCopy * channels * sizeof(float)); pWebm->bufferLeftoverFrameOffset += toCopy; totalFramesRead += toCopy; if (pWebm->bufferLeftoverFrameOffset == pWebm->bufferLeftoverFrameCount) { pWebm->bufferLeftoverFrameCount = 0; pWebm->bufferLeftoverFrameOffset = 0; } if (totalFramesRead >= frameCount) break; // Buffer full } int ret = vorbis_synthesis(&pWebm->vorbisBlock, &oggPkt); if (ret == 0) { vorbis_synthesis_blockin(&pWebm->vorbisDSP, &pWebm->vorbisBlock); float **pcm; int framesAvail = vorbis_synthesis_pcmout(&pWebm->vorbisDSP, &pcm); if (framesAvail > 0) { ma_uint64 skipFrames = 0; if (seekTarget != (ma_uint64)-1 && pWebm->cursorInPCMFrames < seekTarget) { skipFrames = seekTarget - pWebm->cursorInPCMFrames; if (skipFrames > (ma_uint64)framesAvail) skipFrames = framesAvail; } ma_uint64 usableFrames = (ma_uint64)framesAvail - skipFrames; if (usableFrames > frameCount - totalFramesRead) usableFrames = frameCount - totalFramesRead; while (framesAvail > 0 && totalFramesRead < frameCount) { ma_uint64 framesToCopy = (frameCount - totalFramesRead) < (ma_uint64)framesAvail ? (frameCount - totalFramesRead) : (ma_uint64)framesAvail; // Interleave framesToCopy to output buffer directly for (ma_uint64 f = 0; f < framesToCopy; ++f) for (ma_uint32 c = 0; c < channels; ++c) f32Out[(totalFramesRead + f) * channels + c] = pcm[c][f]; totalFramesRead += framesToCopy; framesAvail -= framesToCopy; // If left-over decoded frames after output buffer fills, write to leftover if (framesAvail > 0) { for (ma_uint32 f = 0; f < (ma_uint64)framesAvail; ++f) for (ma_uint32 c = 0; c < channels; ++c) vorbisLeftoverBuffer[f * channels + c] = pcm[c][framesToCopy + f]; pWebm->bufferLeftoverFrameCount = (ma_uint64)framesAvail; pWebm->bufferLeftoverFrameOffset = 0; framesAvail = 0; // Don't call vorbis_synthesis_read or increment cursor yet, do after finished with all available data! } // Consume these frames, even if we buffered them vorbis_synthesis_read(&pWebm->vorbisDSP, framesToCopy + pWebm->bufferLeftoverFrameCount); // or just all at once depending on your loop pWebm->cursorInPCMFrames += (ma_uint64)(framesToCopy + pWebm->bufferLeftoverFrameCount); break; // Output full, let next call handle leftovers } // Always read/consume all frames we got (even those discarded) vorbis_synthesis_read(&pWebm->vorbisDSP, (int)framesAvail); pWebm->cursorInPCMFrames += (ma_uint64)framesAvail; // Done seeking? if (seekTarget != (ma_uint64)-1 && pWebm->cursorInPCMFrames >= seekTarget) { pWebm->seekTargetPCMFrame = (ma_uint64)-1; } } } } ++pWebm->currentPacketFrame; } if (pWebm->currentPacketFrame >= pWebm->numFramesInPacket) { if (pWebm->currentPacket) nestegg_free_packet(pWebm->currentPacket); pWebm->currentPacket = NULL; pWebm->hasPacket = MA_FALSE; } } if (totalFramesRead < frameCount) { memset(f32Out + totalFramesRead * channels, 0, (frameCount - totalFramesRead) * channels * sizeof(float)); } if (pFramesRead) *pFramesRead = totalFramesRead; if (result == MA_SUCCESS && totalFramesRead == 0) result = MA_AT_END; return result; #else { MA_ASSERT(MA_FALSE); (void)pFramesOut; (void)frameCount; (void)pFramesRead; return MA_NOT_IMPLEMENTED; } #endif } MA_API ma_result ma_webm_seek_to_pcm_frame(ma_webm *pWebm, ma_uint64 frameIndex) { if (!pWebm) return MA_INVALID_ARGS; // For Opus: 80ms preroll = 3840 @ 48000Hz ma_uint64 preroll = 0; ma_uint64 prerollFrame = frameIndex; ma_uint64 tstamp_ns = 0; if (pWebm->codec_id == NESTEGG_CODEC_OPUS) { preroll = (frameIndex > 3840) ? 3840 : frameIndex; prerollFrame = (frameIndex > preroll) ? (frameIndex - preroll) : 0; tstamp_ns = (prerollFrame * 1000000000ULL) / 48000; } else { prerollFrame = frameIndex; tstamp_ns = (prerollFrame * 1000000000ULL) / pWebm->sampleRate; } if (nestegg_track_seek(pWebm->ctx, pWebm->audioTrack, tstamp_ns) != 0) return MA_INVALID_OPERATION; // Reset packet and decoder state pWebm->hasPacket = MA_FALSE; if (pWebm->currentPacket) { nestegg_free_packet(pWebm->currentPacket); pWebm->currentPacket = NULL; } pWebm->currentPacketFrame = 0; pWebm->numFramesInPacket = 0; if (pWebm->codec_id == NESTEGG_CODEC_OPUS) opus_decoder_ctl(pWebm->opusDecoder, OPUS_RESET_STATE); else if (pWebm->codec_id == NESTEGG_CODEC_VORBIS) { vorbis_dsp_clear(&pWebm->vorbisDSP); vorbis_block_clear(&pWebm->vorbisBlock); vorbis_synthesis_init(&pWebm->vorbisDSP, &pWebm->vorbisInfo); vorbis_block_init(&pWebm->vorbisDSP, &pWebm->vorbisBlock); } pWebm->bufferLeftoverFrameCount = 0; pWebm->bufferLeftoverFrameOffset = 0; pWebm->cursorInPCMFrames = prerollFrame; pWebm->seekTargetPCMFrame = frameIndex; if (pWebm->seekTargetPCMFrame == 0) pWebm->preSkipLeft = pWebm->opusPreSkip; else pWebm->preSkipLeft = 0; return MA_SUCCESS; } MA_API ma_result ma_webm_get_data_format( ma_webm *pWebm, ma_format *pFormat, ma_uint32 *pChannels, ma_uint32 *pSampleRate, ma_channel *pChannelMap, size_t channelMapCap) { /* Defaults for safety. */ 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 (pWebm == NULL) return MA_INVALID_OPERATION; if (pFormat != NULL) *pFormat = pWebm->format; #if !defined(MA_NO_WEBM) { if (pChannels != NULL) { *pChannels = pWebm->channels; } if (pSampleRate != NULL) { *pSampleRate = pWebm->sampleRate; } if (pChannelMap != NULL) { if (pChannelMap != NULL) { ma_channel_map_init_standard( ma_standard_channel_map_vorbis, pChannelMap, channelMapCap, pWebm->channels); } } return MA_SUCCESS; } #else { MA_ASSERT(MA_FALSE); return MA_NOT_IMPLEMENTED; } #endif } MA_API ma_result ma_webm_get_cursor_in_pcm_frames(ma_webm *pWebm, ma_uint64 *pCursor) { if (pCursor == NULL || pWebm == NULL) { return MA_INVALID_ARGS; } #if !defined(MA_NO_WEBM) { *pCursor = pWebm->cursorInPCMFrames; return MA_SUCCESS; } #else { MA_ASSERT(MA_FALSE); return MA_NOT_IMPLEMENTED; } #endif } ma_uint64 calculate_length_in_pcm_frames(ma_webm *pWebm) { uint64_t duration_ns = 0; if (nestegg_duration(pWebm->ctx, &duration_ns) == 0 && duration_ns > 0) { // For Opus, duration_ns is always in 48kHz timebase per WebM spec if (pWebm->codec_id == NESTEGG_CODEC_OPUS) { // Convert nanoseconds to 48kHz PCM frames uint64_t total_frames_48k = (duration_ns * 48000ull) / 1000000000ull; // Subtract pre-skip and trimming (if known) uint64_t pre_skip = pWebm->opusPreSkip; if (total_frames_48k > pre_skip) total_frames_48k -= pre_skip; return total_frames_48k; } else { // For Vorbis and others, just use sampleRate return (ma_uint64)((duration_ns * (uint64_t)pWebm->sampleRate) / 1000000000ull); } } return 0; } MA_API ma_result ma_webm_get_length_in_pcm_frames(ma_webm *pWebm, ma_uint64 *pLength) { if (pLength == NULL || pWebm == NULL) { return MA_INVALID_ARGS; } #if !defined(MA_NO_WEBM) { if (pWebm->lengthInPCMFrames == 0) { pWebm->lengthInPCMFrames = calculate_length_in_pcm_frames(pWebm); } *pLength = pWebm->lengthInPCMFrames; return MA_SUCCESS; } #else { MA_ASSERT(MA_FALSE); return MA_NOT_IMPLEMENTED; } #endif } #endif kew/themes/000077500000000000000000000000001507107350600131355ustar00rootroot00000000000000kew/themes/army.theme000066400000000000000000000022731507107350600151350ustar00rootroot00000000000000# ============================================ # ARMY # ============================================ [theme] name=Army author=Ravachol # Core colors accent=#a89074 text=#d6cbb8 textDim=#b59d82 textMuted=#786b5a # General Colors logo=#786b5a header=#786b5a footer=#786b5a help=#bca890 link=#b59d82 nowplaying=#a89074 # Playlist View playlist.rownum=#786b5a playlist.title=#d6cbb8 playlist.playing=#b8a085 # Track View trackview.title=#d6cbb8 trackview.artist=#a89074 trackview.album=#b59d82 trackview.year=#b59d82 trackview.time=#bca890 trackview.visualizer=#2b2721 trackview.lyrics=#d6cbb8 # Library View library.artist=#786b5a library.album=#c8bfad library.track=#c8bfad library.enqueued=#8a8a70 library.playing=#b8a085 # Search search.label=#786b5a search.query=#a89074 search.result=#d6cbb8 search.enqueued=#8a8a70 search.playing=#b8a085 # Progress progress.filled=#786b5a progress.empty=#1f1d1a progress.elapsed=#3a3630 # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/bunker.theme000066400000000000000000000022771507107350600154570ustar00rootroot00000000000000# ============================================ # BUNKER # ============================================ [theme] name=Bunker author=Ravachol # Core colors accent=#7c8c9e text=#c8d3dc textDim=#8b99a8 textMuted=#5a6b7d # General Colors logo=#5a6b7d header=#5a6b7d footer=#5a6b7d help=#94a7bc link=#a8b8c8 nowplaying=#7c8c9e # Playlist View playlist.rownum=#5a6b7d playlist.title=#c8d3dc playlist.playing=#9daec0 # Track View trackview.title=#c8d3dc trackview.artist=#7c8c9e trackview.album=#8b99a8 trackview.year=#8b99a8 trackview.time=#94a7bc trackview.visualizer=#2a3441 trackview.lyrics=#c8d3dc # Library View library.artist=#5a6b7d library.album=#b4c1ce library.track=#b4c1ce library.enqueued=#8b99a8 library.playing=#9daec0 # Search search.label=#5a6b7d search.query=#7c8c9e search.result=#c8d3dc search.enqueued=#8b99a8 search.playing=#9daec0 # Progress progress.filled=#3d4a58 progress.empty=#1f2830 progress.elapsed=#5a6b7d # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/cyberpunk.theme000066400000000000000000000023251507107350600161650ustar00rootroot00000000000000# ============================================ # 1. CYBERPUNK (Neon Purple) # ============================================ [theme] name=Cyberpunk author=Ravachol # Core colors accent=#6957ce text=#e0e0ff textDim=#5994ce textMuted=#3a4e93 # General Colors logo=#5658b6 header=#2a2e5a footer=#2a2e5a help=#5994ce link=#ff00ff nowplaying=#ff0080 # Playlist View playlist.rownum=#3a4e93 playlist.title=#b8b8ff playlist.playing=#6957ce # Track View trackview.title=#e0e0ff trackview.artist=#5994ce trackview.album=#5658b6 trackview.year=#3a4e93 trackview.time=#6957ce trackview.visualizer=#5994ce trackview.lyrics=#e0e0ff # Library View library.artist=#6957ce library.album=#5994ce library.track=#b8b8ff library.enqueued=#5994ce library.playing=#6957ce # Search search.label=#5658b6 search.query=#e0e0ff search.result=#b8b8ff search.enqueued=#5994ce search.playing=#6957ce # Progress progress.filled=#6957ce progress.empty=#1a1e3a progress.elapsed=#6957ce # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/default.theme000066400000000000000000000040021507107350600156010ustar00rootroot00000000000000# Default kew Theme # # ANSI 16-Color Palette Theme # This theme will be used if you choose terminal profile colors (press i in kew to cycle color settings) # # The available colors are: # # 0: Black # 1: Red # 2: Green # 3: Yellow # 4: Blue # 5: Magenta # 6: Cyan # 7: White # 8: Bright Black (Gray) # 9: Bright Red # 10: Bright Green # 11: Bright Yellow # 12: Bright Blue # 13: Bright Magenta # 14: Bright Cyan # 15: Bright White # -1: Default terminal foreground name=Default author=Ravachol # Core Colors accent=13 # Bright Magenta text=-1 # Default terminal foreground textDim=8 # Bright Black / Gray textMuted=7 # White (dim) # General Colors logo=6 # Cyan header=6 # Cyan footer=#787878 # The old school gray, this works on most terminals help=11 # Bright Yellow link=14 # Bright Cyan nowplaying=-1 # Default foreground # Playlist View playlist.rownum=8 # Bright Black / Gray playlist.title=-1 # Default foreground playlist.playing=11 # Bright Yellow # Track View trackview.title=-1 # Default foreground trackview.artist=-1 # Default foreground trackview.album=-1 # Default foreground trackview.year=-1 # Default foreground trackview.time=-1 # Default foreground trackview.visualizer=-1 # Default foreground trackview.lyrics=-1 # Default foreground # Library View library.artist=6 # Cyan library.album=6 # Cyan library.track=-1 # Default foreground library.enqueued=11 # Bright Yellow library.playing=11 # Bright Yellow # Search search.label=11 # Bright Yellow search.query=-1 # Default foreground search.result=-1 # Default foreground search.enqueued=14 # Bright Cyan search.playing=11 # Bright Yellow # Progress progress.filled=8 # Bright Black / Gray progress.empty=0 # Black progress.elapsed=8 # Bright Black / Gray # Status status.info=-1 # Default foreground status.warning=11 # Bright Yellow status.error=9 # Bright Red status.success=10 # Bright Green kew/themes/forest.theme000066400000000000000000000023171507107350600154660ustar00rootroot00000000000000# ============================================ # FOREST (Natural Green) # ============================================ [theme] name=Forest author=Ravachol # Core colors accent=#5fb3a1 text=#d8ebe4 textDim=#9dc4bc textMuted=#4a7c70 # General Colors logo=#4a7c70 header=#4a7c70 footer=#4a7c70 help=#8fbc94 link=#87b96b nowplaying=#e76f51 # Playlist View playlist.rownum=#4a7c70 playlist.title=#d8ebe4 playlist.playing=#5fb3a1 # Track View trackview.title=#d8ebe4 trackview.artist=#5fb3a1 trackview.album=#8fbc94 trackview.year=#8fbc94 trackview.time=#f4a261 trackview.visualizer=#2d4a43 trackview.lyrics=#d8ebe4 # Library View library.artist=#4a7c70 library.album=#c8dcd1 library.track=#c8dcd1 library.enqueued=#87b96b library.playing=#5fb3a1 # Search search.label=#4a7c70 search.query=#5fb3a1 search.result=#d8ebe4 search.enqueued=#87b96b search.playing=#5fb3a1 # Progress progress.filled=#2d4a43 progress.empty=#1a2e29 progress.elapsed=#6a9a8a # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/gruvbox.theme000066400000000000000000000023161507107350600156570ustar00rootroot00000000000000# ============================================ # GRUVBOX (Warm Retro) # ============================================ [theme] name=Gruvbox author=Ravachol # Core colors accent=#fabd2f text=#dbc4a1 textDim=#d5c4a1 textMuted=#928374 # General Colors logo=#928374 header=#928374 footer=#928374 help=#83a598 link=#98971a nowplaying=#d79921 # Playlist View playlist.rownum=#928374 playlist.title=#dbc4a1 playlist.playing=#ebdbb2 # Track View trackview.title=#ebdbb2 trackview.artist=#bdae93 trackview.album=#a89984 trackview.year=#a89984 trackview.time=#a89984 trackview.visualizer=#665c54 trackview.lyrics=#ebdbb2 # Library View library.artist=#928374 library.album=#dbc4a1 library.track=#dbc4a1 library.enqueued=#98971a library.playing=#b8bb26 # Search search.label=#928374 search.query=#fe8019 search.result=#dbc4a1 search.enqueued=#98971a search.playing=#b8bb26 # Progress progress.filled=#665c54 progress.empty=#3c3836 progress.elapsed=#a89984 # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/gruvboxlight.theme000066400000000000000000000023401507107350600167040ustar00rootroot00000000000000# ============================================ # GRUVBOX LIGHT (Warm Retro Light) # ============================================ [theme] name=Gruvbox Light author=Ravachol # Core colors accent=#d65d0e text=#3c3836 textDim=#504945 textMuted=#7c6f64 # General Colors logo=#7c6f64 header=#7c6f64 footer=#7c6f64 help=#076678 link=#79740e nowplaying=#b57614 # Playlist View playlist.rownum=#7c6f64 playlist.title=#3c3836 playlist.playing=#af3a03 # Track View trackview.title=#282828 trackview.artist=#9d0006 trackview.album=#665c54 trackview.year=#665c54 trackview.time=#665c54 trackview.visualizer=#d5c4a1 trackview.lyrics=#282828 # Library View library.artist=#7c6f64 library.album=#3c3836 library.track=#3c3836 library.enqueued=#79740e library.playing=#8f3f71 # Search search.label=#7c6f64 search.query=#af3a03 search.result=#3c3836 search.enqueued=#79740e search.playing=#8f3f71 # Progress progress.filled=#d5c4a1 progress.empty=#fbf1c7 progress.elapsed=#665c54 # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/kew-theme-pack1.txt000066400000000000000000000006371507107350600165670ustar00rootroot00000000000000kew themes pack 1. Included in this pack: Dark Themes marianatrench monochrome gruvbox synthwave midnight forest tokyonight bunker army purple pewter ravachol cyberpunk Light Themes solarizedlight gruvboxlight paper To apply, run: kew theme These should be available under /themes here, in case you need to restore them to defaults: https://codeberg.org/ravachol/kew https://github.com/ravachol/kew kew/themes/marianatrench.theme000066400000000000000000000023171507107350600170000ustar00rootroot00000000000000# ============================================ # MARIANA TRENCH # ============================================ [theme] name=Mariana Trench author=Ravachol # Core colors accent=#7a92a8 text=#c8d5de textDim=#8599ab textMuted=#5a6e7d # General Colors logo=#5a6e7d header=#5a6e7d footer=#5a6e7d help=#92a5b5 link=#8599ab nowplaying=#7a92a8 # Playlist View playlist.rownum=#5a6e7d playlist.title=#c8d5de playlist.playing=#8fa4b6 # Track View trackview.title=#c8d5de trackview.artist=#7a92a8 trackview.album=#8599ab trackview.year=#8599ab trackview.time=#92a5b5 trackview.visualizer=#212c35 trackview.lyrics=#c8d5de # Library View library.artist=#5a6e7d library.album=#b5c4ce library.track=#b5c4ce library.enqueued=#7a9487 library.playing=#8fa4b6 # Search search.label=#5a6e7d search.query=#7a92a8 search.result=#c8d5de search.enqueued=#7a9487 search.playing=#8fa4b6 # Progress progress.filled=#2f3d48 progress.empty=#1a2329 progress.elapsed=#2f3d48 # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/midnight.theme000066400000000000000000000023221507107350600157630ustar00rootroot00000000000000============================================ 7. MIDNIGHT (Pure Minimalism) ============================================ [theme] name=Midnight author=Ravachol # Core colors accent=#ffffff text=#cccccc textDim=#888888 textMuted=#444444 # General Colors logo=#444444 header=#444444 footer=#444444 help=#888888 link=#aaaaaa nowplaying=#ffffff # Playlist View playlist.rownum=#444444 playlist.title=#cccccc playlist.playing=#ffffff # Track View trackview.title=#ffffff trackview.artist=#ffffff trackview.album=#888888 trackview.year=#888888 trackview.time=#cccccc trackview.visualizer=#222222 trackview.lyrics=#ffffff # Library View library.artist=#ffffff library.album=#888888 library.track=#cccccc library.enqueued=#aaaaaa library.playing=#ffffff # Search search.label=#888888 search.query=#cccccc search.result=#cccccc search.enqueued=#aaaaaa search.playing=#ffffff # Progress progress.filled=#ffffff progress.empty=#222222 progress.elapsed=#ffffff # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/monochrome.theme000066400000000000000000000023321507107350600163270ustar00rootroot00000000000000# ============================================ # MONOCHROME (Distraction-Free) # ============================================ [theme] name=Monochrome author=Ravachol # Core colors accent=#ffffff text=#e0e0e0 textDim=#b0b0b0 textMuted=#808080 # General Colors logo=#808080 header=#808080 footer=#808080 help=#c0c0c0 link=#b0b0b0 nowplaying=#ffffff # Playlist View playlist.rownum=#808080 playlist.title=#f0f0f0 playlist.playing=#ffffff # Track View trackview.title=#f0f0f0 trackview.artist=#ffffff trackview.album=#c0c0c0 trackview.year=#c0c0c0 trackview.time=#d0d0d0 trackview.visualizer=#f0f0f0 trackview.lyrics=#f0f0f0 # Library View library.artist=#808080 library.album=#e0e0e0 library.track=#e0e0e0 library.enqueued=#b0b0b0 library.playing=#ffffff # Search search.label=#808080 search.query=#ffffff search.result=#e0e0e0 search.enqueued=#b0b0b0 search.playing=#ffffff # Progress progress.filled=#202020 progress.empty=#101010 progress.elapsed=#707070 # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/paper.theme000066400000000000000000000023201507107350600152650ustar00rootroot00000000000000# ============================================ # PAPER (Soft Light Theme) # ============================================ [theme] name=Paper author=Ravachol # Core colors accent=#6b7f96 text=#5a5a5a textDim=#707070 textMuted=#8a8a8a # General Colors logo=#8a8a8a header=#8a8a8a footer=#8a8a8a help=#6b8f8f link=#7a8a70 nowplaying=#6b7f96 # Playlist View playlist.rownum=#8a8a8a playlist.title=#4a4a4a playlist.playing=#8a7a6b # Track View trackview.title=#4a4a4a trackview.artist=#6b7f96 trackview.album=#7a8a8a trackview.year=#7a8a8a trackview.time=#7a8a8a trackview.visualizer=#e8e8e8 trackview.lyrics=#4a4a4a # Library View library.artist=#8a8a8a library.album=#5a5a5a library.track=#5a5a5a library.enqueued=#7a8a70 library.playing=#8a7a6b # Search search.label=#8a8a8a search.query=#6b7f96 search.result=#5a5a5a search.enqueued=#7a8a70 search.playing=#8a7a6b # Progress progress.filled=#d0d0d0 progress.empty=#f5f5f5 progress.elapsed=#8a8a8a # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/pewter.theme000066400000000000000000000023221507107350600154660ustar00rootroot00000000000000# ============================================ # PEWTER (Blue-Gray Subtle) # ============================================ [theme] name=Pewter author=Ravachol # Core colors accent=#7a8a96 text=#c5ced6 textDim=#8d9ba5 textMuted=#556169 # General Colors logo=#556169 header=#556169 footer=#556169 help=#94a1ab link=#8d9ba5 nowplaying=#7a8a96 # Playlist View playlist.rownum=#556169 playlist.title=#c5ced6 playlist.playing=#8c9aa5 # Track View trackview.title=#c5ced6 trackview.artist=#7a8a96 trackview.album=#8d9ba5 trackview.year=#8d9ba5 trackview.time=#94a1ab trackview.visualizer=#1f262b trackview.lyrics=#c5ced6 # Library View library.artist=#556169 library.album=#b0b9c1 library.track=#b0b9c1 library.enqueued=#7a8f7f library.playing=#8c9aa5 # Search search.label=#556169 search.query=#7a8a96 search.result=#c5ced6 search.enqueued=#7a8f7f search.playing=#8c9aa5 # Progress progress.filled=#2d363c progress.empty=#181e22 progress.elapsed=#556169 # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/purple.theme000066400000000000000000000022771507107350600155000ustar00rootroot00000000000000# ============================================ # Purple # ============================================ [theme] name=Purple author=Ravachol # Core colors accent=#9d8ba8 text=#d8d0dc textDim=#a896b0 textMuted=#6d5f78 # General Colors logo=#6d5f78 header=#6d5f78 footer=#6d5f78 help=#b1a0bc link=#a896b0 nowplaying=#9d8ba8 # Playlist View playlist.rownum=#6d5f78 playlist.title=#d8d0dc playlist.playing=#b09cbd # Track View trackview.title=#d8d0dc trackview.artist=#9d8ba8 trackview.album=#a896b0 trackview.year=#a896b0 trackview.time=#b09cbd trackview.visualizer=#2a2531 trackview.lyrics=#d8d0dc # Library View library.artist=#6d5f78 library.album=#c5bcc9 library.track=#c5bcc9 library.enqueued=#8a9a87 library.playing=#b09cbd # Search search.label=#6d5f78 search.query=#9d8ba8 search.result=#d8d0dc search.enqueued=#8a9a87 search.playing=#b09cbd # Progress progress.filled=#362f3d progress.empty=#1d1921 progress.elapsed=#6d5f78 # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/ravachol.theme000066400000000000000000000023411507107350600157600ustar00rootroot00000000000000# ============================================ # RAVACHOL'S THEME (Red & Black) # ============================================ [theme] name=Ravachol's Theme author=Ravachol # Core colors accent=#e6607a text=#e0e0e0 textDim=#e6607a textMuted=#de2b4d # General Colors logo=#de2b4d header=#de2b4d footer=#5c5c5c help=#787878 link=#e6607a nowplaying=#e6607a # Playlist View playlist.rownum=#de2b4d playlist.title=#e0e0e0 playlist.playing=#e6607a # Track View trackview.title=#e0e0e0 trackview.artist=#e6607a trackview.album=#d82953 trackview.year=#d82953 trackview.time=#9b1831 trackview.visualizer=#de2b4d trackview.lyrics=#e0e0e0 # Library View library.artist=#de2b4d library.album=#e0e0e0 library.track=#e0e0e0 library.enqueued=#d82953 library.playing=#e6607a # Search search.label=#de2b4d search.query=#e6607a search.result=#e0e0e0 search.enqueued=#d82953 search.playing=#e6607a # Progress progress.filled=#de2b4d progress.empty=#e6607a progress.elapsed=#de2b4d # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/solarizedlight.theme000066400000000000000000000023471507107350600172130ustar00rootroot00000000000000# ============================================ # SOLARIZED LIGHT (High Contrast Light) # ============================================ [theme] name=Solarized Light author=Ravachol # Core colors accent=#268bd2 text=#657b83 textDim=#839496 textMuted=#93a1a1 # General Colors logo=#93a1a1 header=#93a1a1 footer=#93a1a1 help=#2aa198 link=#859900 nowplaying=#268bd2 # Playlist View playlist.rownum=#93a1a1 playlist.title=#586e75 playlist.playing=#cb4b16 # Track View trackview.title=#586e75 trackview.artist=#268bd2 trackview.album=#2aa198 trackview.year=#2aa198 trackview.time=#b58900 trackview.visualizer=#eee8d5 trackview.lyrics=#586e75 # Library View library.artist=#93a1a1 library.album=#657b83 library.track=#657b83 library.enqueued=#859900 library.playing=#cb4b16 # Search search.label=#93a1a1 search.query=#268bd2 search.result=#657b83 search.enqueued=#859900 search.playing=#cb4b16 # Progress progress.filled=#eee8d5 progress.empty=#fdf6e3 progress.elapsed=#93a1a1 # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/synthwave.theme000066400000000000000000000023241507107350600162120ustar00rootroot00000000000000# ============================================ # 2. SYNTHWAVE (Retro 80s) # ============================================ [theme] name=Synthwave author=Ravachol # Core colors accent=#ff6ec7 text=#9a9a9a textDim=#bd93f9 textMuted=#6272a4 # General Colors logo=#6272a4 header=#6272a4 footer=#6272a4 help=#8be9fd link=#ff6ec7 nowplaying=#ff6ec7 # Playlist View playlist.rownum=#6272a4 playlist.title=#9a9a9a playlist.playing=#ff6ec7 # Track View trackview.title=#ff6ec7 trackview.artist=#bd93f9 trackview.album=#bd93f9 trackview.year=#bd93f9 trackview.time=#44475a trackview.visualizer=#44475a trackview.lyrics=#ff6ec7 # Library View library.artist=#6272a4 library.album=#bd93f9 library.track=#9a9a9a library.enqueued=#bd93f9 library.playing=#ff6ec7 # Search search.label=#6272a4 search.query=#50fa7b search.result=#9a9a9a search.enqueued=#bd93f9 search.playing=#ff6ec7 # Progress progress.filled=#ff6ec7 progress.empty=#44475a progress.elapsed=#ffb86c # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals kew/themes/tokyonight.theme000066400000000000000000000023321507107350600163600ustar00rootroot00000000000000# ============================================ # 4. TOKYO NIGHT (Modern Dark) # ============================================ [theme] name=Tokyo Night author=Ravachol # Core colors accent=#7aa2f7 text=#c0caf5 textDim=#9aa5ce textMuted=#565f89 # General Colors logo=#565f89 header=#565f89 footer=#565f89 help=#7dcfff link=#9ece6a nowplaying=#ff9e64 # Playlist View playlist.rownum=#565f89 playlist.title=#c0caf5 playlist.playing=#7aa2f7 # Track View trackview.title=#c0caf5 trackview.artist=#7aa2f7 trackview.album=#bb9af7 trackview.year=#bb9af7 trackview.time=#7dcfff trackview.visualizer=#7aa2f7 trackview.lyrics=#c0caf5 # Library View library.artist=#7aa2f7 library.album=#bb9af7 library.track=#a9b1d6 library.enqueued=#7dcfff library.playing=#7aa2f7 # Search search.label=#bb9af7 search.query=#c0caf5 search.result=#a9b1d6 search.enqueued=#7dcfff search.playing=#7aa2f7 # Progress progress.filled=#7aa2f7 progress.empty=#292e42 progress.elapsed=#7dcfff # Status status.info=-1 # Default foreground status.warning=#787878 # The old school gray, this works on most terminals status.error=#787878 # The old school gray, this works on most terminals status.success=#787878 # The old school gray, this works on most terminals