├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CMakeLists.txt ├── README.md ├── UNLICENSE ├── assets └── icons │ ├── easyaudiosync.icns │ ├── easyaudiosync.ico │ ├── easyaudiosync.svg │ ├── easyaudiosync128.png │ ├── easyaudiosync256.png │ ├── easyaudiosync48.png │ ├── easyaudiosync64.png │ ├── icons.qrc │ └── windows.rc ├── cmake ├── cmake_uninstall.cmake.in └── vcpkg.cmake ├── config ├── easyaudiosync.desktop.in ├── easyaudiosync.manifest.in └── easync.hpp.in ├── docs ├── building.md └── settings.md ├── src ├── CMakeLists.txt ├── about.cpp ├── about.hpp ├── basic_lazy_file_sink.hpp ├── config.cpp ├── config.hpp ├── main.cpp ├── main.hpp ├── metadata.cpp ├── metadata.hpp ├── settings.cpp ├── settings.hpp ├── sync.cpp ├── sync.hpp ├── transcode.cpp ├── transcode.hpp ├── ui │ ├── about.ui │ ├── clean_dest_warning.ui │ ├── mainwindow.ui │ ├── settings.ui │ ├── settings_aac.ui │ ├── settings_file_handling.ui │ ├── settings_general.ui │ ├── settings_mp3.ui │ ├── settings_ogg_vorbis.ui │ ├── settings_opus.ui │ └── settings_transcoding.ui ├── util.cpp └── util.hpp ├── translations ├── README.md ├── en.ts ├── languages.hpp.in ├── languages.txt ├── source.ts └── translations.qrc.in └── vcpkg.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "**/**.md" 7 | 8 | pull_request: 9 | branches: 10 | - master 11 | paths-ignore: 12 | - "**/**.md" 13 | 14 | workflow_dispatch: 15 | 16 | defaults: 17 | run: 18 | shell: bash 19 | 20 | permissions: 21 | actions: none 22 | checks: none 23 | contents: write 24 | deployments: none 25 | issues: none 26 | packages: read 27 | pull-requests: none 28 | repository-projects: none 29 | security-events: none 30 | statuses: read 31 | 32 | env: 33 | VCPKG_COMMITTISH: 10b7a178346f3f0abef60cecd5130e295afd8da4 34 | 35 | jobs: 36 | build_windows: 37 | name: Windows 38 | runs-on: windows-2022 39 | strategy: 40 | fail-fast: false 41 | 42 | env: 43 | CMAKE_BUILD_TYPE: ${{startsWith(github.ref, 'refs/tags/') && 'Release' || 'RelWithDebInfo'}} 44 | CMAKE_GENERATOR: Visual Studio 17 2022 45 | VCPKG_TRIPLET: x64-windows 46 | NSIS_INSTALLER: ${{startsWith(github.ref, 'refs/tags/') && 'ON' || 'OFF'}} 47 | PKG_EXTENSION: ${{startsWith(github.ref, 'refs/tags/') && '.exe' || '.zip'}} 48 | 49 | steps: 50 | - name: Checkout Git repository 51 | uses: actions/checkout@v4 52 | with: 53 | fetch-depth: 0 54 | 55 | - name: Setup vcpkg 56 | uses: friendlyanon/setup-vcpkg@v1 57 | with: 58 | committish: ${{env.VCPKG_COMMITTISH}} 59 | 60 | - name: Setup Overlays 61 | uses: actions/checkout@v4 62 | with: 63 | repository: complexlogic/vcpkg 64 | ref: refs/heads/easyaudiosync 65 | path: build/overlays 66 | 67 | - name: Configure 68 | run: | 69 | mkdir C:/vcpkg_buildtrees 70 | cmake -S . -B build \ 71 | -G "${{env.CMAKE_GENERATOR}}" \ 72 | -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \ 73 | -DVCPKG_OVERLAY_PORTS=build/overlays/ports \ 74 | -DVCPKG_TARGET_TRIPLET=${{env.VCPKG_TRIPLET}} \ 75 | -DNSIS_INSTALLER=${{env.NSIS_INSTALLER}} \ 76 | -DVCPKG_INSTALL_OPTIONS="--x-buildtrees-root=c:/vcpkg_buildtrees" 77 | 78 | - name: Build 79 | run: cmake --build build --target package --config ${{env.CMAKE_BUILD_TYPE}} 80 | 81 | - name: Upload Package 82 | uses: actions/upload-artifact@v3 83 | with: 84 | name: Windows build 85 | path: build/*${{env.PKG_EXTENSION}} 86 | 87 | - name: Release 88 | uses: softprops/action-gh-release@v1 89 | if: startsWith(github.ref, 'refs/tags/') 90 | with: 91 | files: build/*${{env.PKG_EXTENSION}} 92 | 93 | build_macos: 94 | name: macOS 95 | runs-on: macos-12 96 | strategy: 97 | fail-fast: false 98 | matrix: 99 | config: 100 | - name: Intel 101 | OSX_ARCH: x86_64 102 | VCPKG_TRIPLET: x64-osx 103 | 104 | - name: Apple Silicon 105 | OSX_ARCH: arm64 106 | VCPKG_TRIPLET: arm64-osx 107 | 108 | steps: 109 | - name: Checkout Git repository 110 | uses: actions/checkout@v4 111 | with: 112 | fetch-depth: 0 113 | 114 | - name: Setup vcpkg 115 | uses: friendlyanon/setup-vcpkg@v1 116 | with: 117 | committish: ${{env.VCPKG_COMMITTISH}} 118 | cache-key: vcpkg-macOS-${{matrix.config.OSX_ARCH}}-${{github.sha}} 119 | cache-restore-keys: vcpkg-macOS-${{matrix.config.OSX_ARCH}}- 120 | 121 | - name: Setup Overlays 122 | uses: actions/checkout@v3 123 | with: 124 | repository: complexlogic/vcpkg 125 | ref: refs/heads/easyaudiosync 126 | path: build/overlays 127 | 128 | - name: Install Dependencies 129 | run: brew install nasm automake autoconf-archive ninja 130 | 131 | - name: Configure 132 | run: cmake -S . -B build 133 | -DCMAKE_BUILD_TYPE=Release 134 | -DCMAKE_OSX_ARCHITECTURES=${{matrix.config.OSX_ARCH}} 135 | -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" 136 | -DVCPKG_OVERLAY_PORTS=build/overlays/ports 137 | -DVCPKG_TARGET_TRIPLET=${{matrix.config.VCPKG_TRIPLET}} 138 | -DDMG=ON 139 | 140 | - name: Build 141 | run: cmake --build build --target dmg 142 | 143 | - name: Upload Package 144 | uses: actions/upload-artifact@v3 145 | with: 146 | name: macOS (${{matrix.config.name}}) build 147 | path: build/*.dmg 148 | 149 | - name: Release 150 | uses: softprops/action-gh-release@v1 151 | if: startsWith(github.ref, 'refs/tags/') 152 | with: 153 | files: build/*.dmg 154 | 155 | build_linux: 156 | name: Linux 157 | runs-on: ubuntu-latest 158 | strategy: 159 | fail-fast: false 160 | matrix: 161 | config: 162 | - name: Debian 163 | docker_image: debian:bookworm 164 | package_type: DEB 165 | package_ext: .deb 166 | 167 | - name: Fedora 168 | docker_image: fedora:39 169 | package_type: RPM 170 | package_ext: .rpm 171 | 172 | env: 173 | VCPKG_TRIPLET: x64-linux 174 | CMAKE_BUILD_TYPE: Release 175 | 176 | container: 177 | image: ${{matrix.config.docker_image}} 178 | 179 | steps: 180 | - name: Checkout Git repository 181 | uses: actions/checkout@v4 182 | with: 183 | fetch-depth: 0 184 | 185 | - name: "Install dependencies" 186 | run: | 187 | if [[ "${{matrix.config.name}}" == "Debian" ]]; then 188 | export DEBIAN_FRONTEND=noninteractive 189 | apt update && apt install -y curl zip unzip tar build-essential git cmake pkg-config python3 nasm qtbase5-dev qttools5-dev 190 | fi 191 | if [[ "${{matrix.config.name}}" == "Fedora" ]]; then 192 | dnf install -y curl zip unzip tar git make pkg-config gcc-c++ fedora-packager rpmdevtools cmake nasm qt5-qtbase-devel qt5-qttools-devel 193 | fi 194 | 195 | - name: Setup vcpkg 196 | uses: friendlyanon/setup-vcpkg@v1 197 | with: 198 | committish: ${{env.VCPKG_COMMITTISH}} 199 | cache-key: vcpkg-${{matrix.config.name}}-${{github.sha}} 200 | cache-restore-keys: vcpkg-${{matrix.config.name}}- 201 | 202 | - name: Setup Overlays 203 | uses: actions/checkout@v3 204 | with: 205 | repository: complexlogic/vcpkg 206 | ref: refs/heads/easyaudiosync 207 | path: build/overlays 208 | 209 | - name: Configure 210 | run: cmake -S . -B build 211 | -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" 212 | -DVCPKG_TARGET_TRIPLET=${{env.VCPKG_TRIPLET}} 213 | -DVCPKG_OVERLAY_PORTS=build/overlays/ports 214 | -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} 215 | -DCMAKE_INSTALL_PREFIX=/usr 216 | -DSTRIP_BINARY=ON 217 | -DQT_VERSION=5 218 | -DPACKAGE=${{matrix.config.package_type}} 219 | 220 | - name: Build 221 | run: cmake --build build --target package 222 | 223 | - name: Upload Package 224 | uses: actions/upload-artifact@v3 225 | with: 226 | name: ${{matrix.config.name}} build 227 | path: build/*${{matrix.config.package_ext}} 228 | 229 | - name: Release 230 | uses: softprops/action-gh-release@v1 231 | if: startsWith(github.ref, 'refs/tags/') 232 | with: 233 | files: build/*${{matrix.config.package_ext}} 234 | token: ${{secrets.ACTIONS_SECRET}} 235 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Source 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | workflow_dispatch: 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | permissions: 14 | actions: none 15 | checks: none 16 | contents: write 17 | deployments: none 18 | issues: none 19 | packages: read 20 | pull-requests: none 21 | repository-projects: none 22 | security-events: none 23 | statuses: read 24 | 25 | jobs: 26 | build_source: 27 | name: Build Source 28 | if: startsWith(github.ref, 'refs/tags/') 29 | runs-on: ubuntu-latest 30 | strategy: 31 | fail-fast: false 32 | 33 | steps: 34 | - name: Setup 35 | run: | 36 | sudo apt install -y tar 37 | REPO=${{github.repository}} 38 | REPO_TITLE=${REPO#*/} 39 | RELEASE_TITLE=${{github.event.release.name}} 40 | PACKAGE_TITLE=easyaudiosync-${RELEASE_TITLE#*v} 41 | 42 | echo "PACKAGE_TITLE=${PACKAGE_TITLE}" >> ${GITHUB_ENV} 43 | 44 | - name: Checkout Git repository 45 | uses: actions/checkout@v3 46 | with: 47 | path: ${{env.PACKAGE_TITLE}} 48 | 49 | - name: Package 50 | run: | 51 | ARCHIVE_NAME=${{env.PACKAGE_TITLE}}-source.tar.xz 52 | tar --lzma --exclude ${{env.PACKAGE_TITLE}}/.git -cf $ARCHIVE_NAME ${{env.PACKAGE_TITLE}} 53 | SHA256=$(shasum -b -a 256 < $ARCHIVE_NAME | cut -d ' ' -f1) 54 | echo "SHA256=${SHA256}" >> ${GITHUB_ENV} 55 | 56 | - name: Release 57 | uses: softprops/action-gh-release@master 58 | with: 59 | files: ./*.tar.xz 60 | append_body: true 61 | body: "**Source SHA256:** ${{env.SHA256}}" 62 | generate_release_notes: true 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.1.2 (2024-10-27) 2 | - Add more information to About page 3 | - Update dependencies for binaries 4 | 5 | ### v1.1.1 (2024-02-08) 6 | - Handle file extensions case insensitively 7 | 8 | ### v1.1 (2024-01-25) 9 | - Add support for SoX resampling engine 10 | - Add "Include extended tags" feature 11 | - Support duplicate keys for Vorbis comments 12 | - Make Opus settings persistent 13 | - Various minor bugfixes, stability improvements, and optimizations 14 | - Windows: support long paths 15 | 16 | ### v1.0 (2023-05-18) 17 | - Initial release 18 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.19) 2 | option(VCPKG "Build dependencies with vcpkg" OFF) 3 | if (VCPKG) 4 | include("${CMAKE_SOURCE_DIR}/cmake/vcpkg.cmake") 5 | endif () 6 | 7 | project("Easy Audio Sync" 8 | VERSION 1.1.2 9 | DESCRIPTION "Audio library syncing and conversion utility" 10 | HOMEPAGE_URL "https://github.com/complexlogic/EasyAudioSync" 11 | LANGUAGES CXX 12 | ) 13 | set(CMAKE_CXX_STANDARD 20) 14 | set(EXECUTABLE_NAME "easyaudiosync") 15 | set(RDNS_NAME "io.github.complexlogic.EasyAudioSync") 16 | include_directories(${PROJECT_BINARY_DIR}) 17 | add_compile_definitions("$<$:DEBUG>") 18 | set(EXECUTABLE_OUTPUT_PATH "${PROJECT_BINARY_DIR}") 19 | set(VS_STARTUP_PROJECT ${EXECUTABLE_NAME}) 20 | 21 | # Configure options 22 | if (WIN32 OR APPLE) 23 | set(QT_VERSION "6") 24 | else () 25 | set(QT_VERSION "5" CACHE STRING "Qt major version to use (5 or 6).") 26 | if (NOT (QT_VERSION STREQUAL "5" OR QT_VERSION STREQUAL "6")) 27 | message(FATAL_ERROR "Unsupported Qt version '${QT_VERSION}'. Only 5 and 6 are supported") 28 | endif () 29 | endif () 30 | if (APPLE) 31 | option(DMG "Make deployable DMG" OFF) 32 | endif () 33 | if (UNIX) 34 | option(STRIP_BINARY "Run strip on the binary" OFF) 35 | endif () 36 | option(PERSIST_GEOMETRY "Make window size and position persistent between runs" ON) 37 | if (PERSIST_GEOMETRY) 38 | add_compile_definitions(PERSIST_GEOMETRY) 39 | endif () 40 | option(FFDEBUG "FFmpeg debug messages" OFF) 41 | if (FFDEBUG) 42 | add_compile_definitions("FFDEBUG") 43 | endif () 44 | option(EXTRA_WARNINGS "Enable extra compiler warnings" OFF) 45 | if (EXTRA_WARNINGS) 46 | if (MSVC) 47 | add_compile_options(/W4 /WX) 48 | else () 49 | add_compile_options(-Wall -Wextra -Wpedantic -Wconversion) 50 | endif () 51 | endif () 52 | if (WIN32) 53 | option(NSIS_INSTALLER "Enable NSIS installer target" OFF) 54 | endif () 55 | 56 | # Qt setup 57 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 58 | set(CMAKE_AUTOMOC ON) 59 | set(CMAKE_AUTORCC ON) 60 | if (QT_VERSION STREQUAL "5") 61 | set(QT_VERSION_REQUIREMENT "5.15...<6") 62 | endif () 63 | find_package(Qt${QT_VERSION} ${QT_VERSION_REQUIREMENT} REQUIRED COMPONENTS Core Gui Widgets LinguistTools) 64 | 65 | if (UNIX) 66 | find_package(PkgConfig MODULE REQUIRED) 67 | find_package(Threads REQUIRED) 68 | pkg_check_modules(LIBAVFORMAT REQUIRED IMPORTED_TARGET libavformat>=59.27) 69 | pkg_check_modules(LIBAVCODEC REQUIRED IMPORTED_TARGET libavcodec>=59.37) 70 | pkg_check_modules(LIBSWRESAMPLE REQUIRED IMPORTED_TARGET libswresample>=4.7) 71 | pkg_check_modules(LIBAVFILTER REQUIRED IMPORTED_TARGET libavfilter>=8.44) 72 | pkg_check_modules(LIBAVUTIL REQUIRED IMPORTED_TARGET libavutil>=57.28) 73 | pkg_check_modules(FMT REQUIRED IMPORTED_TARGET fmt) 74 | pkg_check_modules(SPDLOG REQUIRED IMPORTED_TARGET spdlog) 75 | pkg_check_modules(TAGLIB REQUIRED IMPORTED_TARGET taglib>=1.12) 76 | if (STRIP_BINARY) 77 | find_program(STRIP strip REQUIRED) 78 | endif () 79 | if (APPLE AND DMG) 80 | find_program(MACDEPLOYQT macdeployqt PATHS "${VCPKG_INSTALLED_DIR}/x64-osx/tools/Qt${QT_VERSION}/bin" REQUIRED) 81 | endif () 82 | elseif (WIN32) 83 | find_path(FFMPEG_INCLUDE_DIR "libavformat/avformat.h" REQUIRED) 84 | find_library(LIBAVFORMAT avformat REQUIRED) 85 | find_library(LIBAVCODEC avcodec REQUIRED) 86 | find_library(LIBAVFILTER avfilter REQUIRED) 87 | find_library(LIBSWRESAMPLE swresample REQUIRED) 88 | find_library(LIBAVUTIL avutil REQUIRED) 89 | find_path(TAGLIB_INCLUDE_DIR "taglib/taglib.h" REQUIRED) 90 | find_library(TAGLIB tag REQUIRED) 91 | find_package(spdlog CONFIG REQUIRED) 92 | find_program(WINDEPLOYQT windeployqt.exe REQUIRED) 93 | if (NSIS_INSTALLER) 94 | find_program(NSIS makensis.exe REQUIRED) 95 | endif () 96 | endif () 97 | 98 | # Embed Git information 99 | find_package(Git QUIET) 100 | if (Git_FOUND) 101 | execute_process(COMMAND "${GIT_EXECUTABLE}" describe --long --tags 102 | WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" 103 | OUTPUT_VARIABLE GIT_OUTPUT 104 | RESULT_VARIABLE GIT_RESULT 105 | OUTPUT_STRIP_TRAILING_WHITESPACE 106 | ) 107 | if (GIT_RESULT EQUAL 0) 108 | string(REPLACE "-" ";" GIT_LIST "${GIT_OUTPUT}") 109 | list(GET GIT_LIST 1 COMMITS_SINCE_TAG) 110 | if (NOT COMMITS_SINCE_TAG STREQUAL "0") 111 | list(GET GIT_LIST 2 COMMIT_HASH) 112 | string(REPLACE "g" "" COMMIT_HASH "${COMMIT_HASH}") 113 | set(PROJECT_VERSION_GIT "${PROJECT_VERSION}-r${COMMITS_SINCE_TAG}-${COMMIT_HASH}") 114 | add_compile_definitions(PROJECT_VERSION_GIT=\"${PROJECT_VERSION_GIT}\") 115 | set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION_GIT}") 116 | endif () 117 | endif () 118 | endif () 119 | 120 | # Generate Windows application manifest and resource file 121 | if (WIN32) 122 | set(VERSION_M ${PROJECT_VERSION_MAJOR}) 123 | set(VERSION_N ${PROJECT_VERSION_MINOR}) 124 | if (PROJECT_VERSION_PATCH) 125 | set(VERSION_O ${PROJECT_VERSION_PATCH}) 126 | else () 127 | set(VERSION_O 0) 128 | endif() 129 | if (PROJECT_VERSION_TWEAK) 130 | set(VERSION_P ${PROJECT_VERSION_TWEAK}) 131 | else () 132 | set(VERSION_P 0) 133 | endif() 134 | configure_file(${PROJECT_SOURCE_DIR}/config/${EXECUTABLE_NAME}.manifest.in ${PROJECT_BINARY_DIR}/${EXECUTABLE_NAME}.manifest) 135 | endif() 136 | 137 | # Linux desktop and appstream 138 | if (UNIX AND NOT APPLE) 139 | configure_file("${PROJECT_SOURCE_DIR}/config/${EXECUTABLE_NAME}.desktop.in" "${PROJECT_BINARY_DIR}/${RDNS_NAME}.desktop") 140 | endif () 141 | 142 | add_subdirectory(src) 143 | configure_file("${PROJECT_SOURCE_DIR}/config/easync.hpp.in" "${PROJECT_BINARY_DIR}/easync.hpp") 144 | 145 | if (UNIX) 146 | configure_file( 147 | "${PROJECT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in" 148 | "${PROJECT_BINARY_DIR}/cmake_uninstall.cmake" 149 | IMMEDIATE @ONLY) 150 | add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${PROJECT_BINARY_DIR}/cmake_uninstall.cmake) 151 | endif () 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easy Audio Sync 2 | 1. [About](#about) 3 | 2. [Screenshots](#screenshots) 4 | 3. [Installation](#installation) 5 | 4. [Usage](#usage) 6 | 5. [Support](#support) 7 | 8 | ## About 9 | Easy Audio Sync is an audio library syncing and conversion utility. The intended use is syncing an audio library with many lossless files to a mobile device with limited storage. 10 | 11 | The program's design is inspired by the [rsync](https://github.com/WayneD/rsync) utility. It supports folder-based source to destination syncing, with added audio transcoding capability, and is GUI-based instead of CLI-based. 12 | 13 | ### Features 14 | - Custom FFmpeg-based audio transcoding engine 15 | - Multithreaded operation for fast conversions 16 | - 4 lossy output codecs supported: MP3, AAC, Ogg Vorbis, and Opus 17 | - Robust metadata parser ensures tags and cover art are correctly transferred when converting between different formats 18 | - ReplayGain volume adjustments 19 | - Destination folder cleaning (deleting files that no longer exist in the source) 20 | - Cross-platform Windows/macOS/Linux 21 | 22 | ## Screenshots 23 | ![Main Window](https://github.com/complexlogic/EasyAudioSync/assets/95071366/e32beb0c-2f07-4b39-a75f-93ed6226c014) 24 | 25 | ![Settings](https://github.com/complexlogic/EasyAudioSync/assets/95071366/9f4bcc67-995f-4f74-b533-13581e2bbd92) 26 | 27 | ## Installation 28 | Builds are available for Windows, macOS, and some Linux distributions. You can also build from source yourself, see [BUILDING](docs/building.md) 29 | 30 | ### Windows 31 | Easy Audio Sync is compatible with Windows 10 and later. Download the installer executable from the link below, run it, and follow the guided setup process: 32 | - [Easy Audio Sync v1.1.2 Installer EXE (x64)](https://github.com/complexlogic/EasyAudioSync/releases/download/v1.1.2/easyaudiosync-1.1.2-setup.exe) 33 | 34 | If Windows raises a SmartScreen warning when you try to run the executable, select More Info->Run Anyway. 35 | 36 | ### macOS 37 | *Note: I haven't tested these builds because I have no access to macOS hardware. Consider macOS support experimental, and open an issue on the [Issue Tracker](https://github.com/complexlogic/EasyAudioSync/issues) if you encounter any bugs.* 38 | 39 | Separate builds are available for Intel and Apple Silicon based Macs (both require macOS 11 or later): 40 | - [Easy Audio Sync v1.1.2 DMG (Intel)](https://github.com/complexlogic/EasyAudioSync/releases/download/v1.1.2/easyaudiosync-1.1.2-x86_64.dmg) 41 | - [Easy Audio Sync v1.1.2 DMG (Apple Silicon)](https://github.com/complexlogic/EasyAudioSync/releases/download/v1.1.2/easyaudiosync-1.1.2-arm64.dmg) 42 | 43 | These builds are not codesigned, and the macOS Gatekeeper will most likely block execution. To work around this, you can remove the quarantine bit using the command below: 44 | 45 | ```bash 46 | xattr -d com.apple.quarantine /path/to/easyaudiosync.dmg 47 | ``` 48 | 49 | Substitute `/path/to/easyaudiosync.dmg` with the actual path on your system. 50 | 51 | ### Linux 52 | 53 | #### Arch/Manjaro 54 | There is an [AUR package](https://aur.archlinux.org/packages/easyaudiosync) available, which can be installed using a helper such as yay: 55 | 56 | ```bash 57 | yay -S easyaudiosync 58 | ``` 59 | 60 | #### APT-based (Debian, Ubuntu) 61 | A .deb package is available on the release page. It was built on Debian Bullseye and is compatible with the most recent release of most `apt`-based distros (anything that ships GCC 12 or later). Execute the following commands to install: 62 | 63 | ```bash 64 | wget https://github.com/complexlogic/EasyAudioSync/releases/download/v1.1.2/easyaudiosync_1.1.2_amd64.deb 65 | sudo apt install ./easyaudiosync_1.1.2_amd64.deb 66 | ``` 67 | 68 | #### Fedora 69 | A .rpm package is available on the release page that is compatible with Fedora 39 and later. Execute the following commands to install: 70 | 71 | ```bash 72 | sudo dnf install https://github.com/complexlogic/EasyAudioSync/releases/download/v1.1.2/easyaudiosync-1.1.2-1.x86_64.rpm 73 | ``` 74 | 75 | ## Usage 76 | Easy Audio Sync operates based on a source folder and a destination folder, where the source folder contains the primary music library, and the destination folder is the desired output location. After selecting the source and destination folders, click the "Sync" button to start the sync. The program will recreate the source's entire subfolder structure in the destination, copying or transcoding files as specified in the settings. See the [settings documentation](docs/settings.md) for help on configuring the program's settings. 77 | 78 | The stop button next to the progress bar can be used stop the sync at any time. The worker threads are allowed to finish their current file operation before quitting to avoid leaving corrupted files in the destination. Consequently, it may take several seconds after the stop button is clicked for the sync to stop. 79 | 80 | ### Supported File Formats 81 | The following file format are supported, per-mode: 82 | 83 | | Codec | Copy | Transcode Input | Transcode Output | 84 | | -------------- | ------------------ | ------------------ | ------------------ | 85 | | FLAC | :heavy_check_mark: | :heavy_check_mark: | :x: | 86 | | ALAC | :heavy_check_mark: | :heavy_check_mark: | :x: | 87 | | Wavpack | :heavy_check_mark: | :heavy_check_mark: | :x: | 88 | | Monkey's Audio | :heavy_check_mark: | :heavy_check_mark: | :x: | 89 | | WAV | :heavy_check_mark: | :heavy_check_mark: | :x: | 90 | | MP3 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 91 | | AAC | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 92 | | Ogg Vorbis | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 93 | | Opus | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 94 | | Musepack | :heavy_check_mark: | :heavy_check_mark: | :x: | 95 | | WMA | :heavy_check_mark: | :x: | :x: | 96 | 97 | ### Supported Encoders 98 | The following encoders are supported for transcoding: 99 | 100 | | Codec | Encoder(s) | 101 | | ------------ | ------------------------------ | 102 | | MP3 | LAME | 103 | | AAC | Fraunhofer FDK AAC, libavcodec | 104 | | Ogg Vorbis | libvorbis | 105 | | Opus | libopus | 106 | 107 | ### General Tips 108 | This section gives usage tips for Easy Audio Sync 109 | 110 | #### Filesystem Differences 111 | If you run the program on Linux or Mac, have a destination folder that's on a removeable storage drive, and are getting transcoding errors, it may be because of filesystem incompatibilities. Most removable storage drives have Microsoft filesystems. In such filesystems, there are several illegal characters for paths that are allowable in Unix filesystems. 112 | 113 | The program will not remove the illegal characters for you, because there needs to be a one-to-one correspondence between source and destination file paths to support the "Clean Destination" feature. It is the user's responsibility to be aware of filesytem differences between the source and destination folders, and prepare accordingly. There are several tools that can rename files in your source directory such that they will be compatible with all major filesystems. 114 | 115 | #### Syncing to an Android Device 116 | The best way of syncing an Anroid device is to use a removable SD card. Physically remove the SD card from the device, connect it to your PC, perform the sync, then replace it in the device. This may be somewhat inconvenient, but it is the best possible option. Unfortunately, not all Android devices support removable SD cards, so this option may not be available to you. 117 | 118 | Most modern Android devies no longer support USB mass storage. The replacement is called Media Transfer Protocol (MTP). Easy Audio Sync does not include built-in support for MTP; it can only write to regular files. If you want to use the program with MTP, you will need to use it in combination with software that abstracts MTP and presents the device to the system as an ordinary storage drive, such as [go-mtpfs](https://github.com/hanwen/go-mtpfs) for Linux. 119 | 120 | In my experience, MTP is both slow and unreliable, and not worth using. If you can't use the SD card method, another option is to use a local directory on your PC as the destination, and then use a third party sync program to transfer the files to your device. A good option is [Syncthing](https://github.com/syncthing/syncthing). It supports network-based syncing, so you don't need to directly connect your device to your PC. The disadvantage of this method is that you will need to keep multiple versions of your music library on your PC, which uses more storage space. 121 | 122 | ## Support 123 | If you encounter any bugs, please open an issue on the issue tracker. For general help, use the Discussions page instead. 124 | 125 | ### Contributing 126 | Pull Requests will be reviewed for bug fixes and new features. 127 | 128 | The program is currently available in English only. See the [translation README](translations/README.md) if you would like to translate the program into your language. 129 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /assets/icons/easyaudiosync.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/EasyAudioSync/e1f459485cef450f3f7792c490aad94f63a86e63/assets/icons/easyaudiosync.icns -------------------------------------------------------------------------------- /assets/icons/easyaudiosync.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/EasyAudioSync/e1f459485cef450f3f7792c490aad94f63a86e63/assets/icons/easyaudiosync.ico -------------------------------------------------------------------------------- /assets/icons/easyaudiosync.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 16 | 20 | 24 | 25 | 26 | 33 | 36 | 42 | 48 | 49 | 51 | 55 | 59 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /assets/icons/easyaudiosync128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/EasyAudioSync/e1f459485cef450f3f7792c490aad94f63a86e63/assets/icons/easyaudiosync128.png -------------------------------------------------------------------------------- /assets/icons/easyaudiosync256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/EasyAudioSync/e1f459485cef450f3f7792c490aad94f63a86e63/assets/icons/easyaudiosync256.png -------------------------------------------------------------------------------- /assets/icons/easyaudiosync48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/EasyAudioSync/e1f459485cef450f3f7792c490aad94f63a86e63/assets/icons/easyaudiosync48.png -------------------------------------------------------------------------------- /assets/icons/easyaudiosync64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/complexlogic/EasyAudioSync/e1f459485cef450f3f7792c490aad94f63a86e63/assets/icons/easyaudiosync64.png -------------------------------------------------------------------------------- /assets/icons/icons.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | easyaudiosync256.png 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/icons/windows.rc: -------------------------------------------------------------------------------- 1 | IDI_ICON1 ICON "easyaudiosync.ico" 2 | -------------------------------------------------------------------------------- /cmake/cmake_uninstall.cmake.in: -------------------------------------------------------------------------------- 1 | if(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt") 2 | message(FATAL_ERROR "Cannot find install manifest: @CMAKE_BINARY_DIR@/install_manifest.txt") 3 | endif(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt") 4 | 5 | file(READ "@CMAKE_BINARY_DIR@/install_manifest.txt" files) 6 | string(REGEX REPLACE "\n" ";" files "${files}") 7 | foreach(file ${files}) 8 | message(STATUS "Uninstalling $ENV{DESTDIR}${file}") 9 | if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") 10 | exec_program( 11 | "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" 12 | OUTPUT_VARIABLE rm_out 13 | RETURN_VALUE rm_retval 14 | ) 15 | if(NOT "${rm_retval}" STREQUAL 0) 16 | message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") 17 | endif(NOT "${rm_retval}" STREQUAL 0) 18 | else(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") 19 | message(STATUS "File $ENV{DESTDIR}${file} does not exist.") 20 | endif(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") 21 | endforeach(file) 22 | -------------------------------------------------------------------------------- /cmake/vcpkg.cmake: -------------------------------------------------------------------------------- 1 | message("Setting up vcpkg...") 2 | include(FetchContent) 3 | FetchContent_Declare( 4 | vcpkg 5 | GIT_REPOSITORY https://github.com/microsoft/vcpkg.git 6 | GIT_SHALLOW TRUE 7 | SOURCE_DIR ${PROJECT_BINARY_DIR} 8 | ) 9 | FetchContent_Declare( 10 | vcpkg_overlay 11 | GIT_REPOSITORY https://github.com/complexlogic/vcpkg.git 12 | GIT_TAG origin/easyaudiosync 13 | GIT_SHALLOW TRUE 14 | SOURCE_DIR ${PROJECT_BINARY_DIR} 15 | ) 16 | FetchContent_MakeAvailable(vcpkg vcpkg_overlay) 17 | set(VCPKG_OVERLAY_PORTS "${CMAKE_BINARY_DIR}/_deps/vcpkg_overlay-src/ports") 18 | if (NOT VCPKG_TARGET_TRIPLET) 19 | if (WIN32) 20 | set (VCPKG_TARGET_TRIPLET "x64-windows") 21 | elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") 22 | execute_process(COMMAND uname -m COMMAND tr -d '\n' OUTPUT_VARIABLE CPU_ARCH) 23 | if (CPU_ARCH STREQUAL "x86_64") 24 | set(VCPKG_TARGET_TRIPLET "x64-linux") 25 | elseif (CPU_ARCH STREQUAL "aarch64") 26 | set(VCPKG_TARGET_TRIPLET "arm64-linux") 27 | else () 28 | message(FATAL_ERROR "Unsupported CPU architecture: ${CPU_ARCH}") 29 | endif () 30 | endif() 31 | endif () 32 | 33 | set(CMAKE_TOOLCHAIN_FILE "${CMAKE_BINARY_DIR}/_deps/vcpkg-src/scripts/buildsystems/vcpkg.cmake") 34 | -------------------------------------------------------------------------------- /config/easyaudiosync.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.5 3 | Type=Application 4 | Name=@CMAKE_PROJECT_NAME@ 5 | TryExec=@EXECUTABLE_NAME@ 6 | Exec=@EXECUTABLE_NAME@ 7 | Icon=@RDNS_NAME@ 8 | Categories=Qt;Audio;AudioVideo; 9 | Comment=@CMAKE_PROJECT_DESCRIPTION@ 10 | Comment[en]=@CMAKE_PROJECT_DESCRIPTION@ 11 | -------------------------------------------------------------------------------- /config/easyaudiosync.manifest.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | UTF-8 7 | true 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /config/easync.hpp.in: -------------------------------------------------------------------------------- 1 | #define PROJECT_NAME "@PROJECT_NAME@" 2 | #define PROJECT_VERSION "@PROJECT_VERSION@" 3 | #define EXECUTABLE_NAME "@EXECUTABLE_NAME@" -------------------------------------------------------------------------------- /docs/building.md: -------------------------------------------------------------------------------- 1 | # Building 2 | Easy Audio Sync builds natively on Unix and Windows, and features a cross-platform CMake build system. The following external dependencies are required: 3 | 4 | - Qt (see [below](#qt-version-requirements) for version requirements), specifically the following modules: 5 | - Core 6 | - GUI 7 | - Widgets 8 | - LinguistTools (build only, no runtime requirement) 9 | - FFmpeg ≥5.1, specifically the following libraries: 10 | - libavformat 11 | - libavcodec 12 | - libswresample 13 | - libavfilter 14 | - libavutil 15 | - TagLib ≥1.12 16 | - spdlog 17 | - fmt (spdlog is typically packaged with fmt as a dependency, so you likely won't need to explicitly install this) 18 | 19 | The source code is written in C++20, and as such requires a relatively modern compiler to build: 20 | 21 | - On Windows, use Visual Studio 2022 22 | - On Linux, use GCC 11 or later 23 | - On macOS, the latest available Xcode for your machine should work 24 | 25 | ### Qt Version Requirements 26 | The Windows and macOS versions must use Qt6. The Linux version can use either Qt5 or Qt6, with Qt5 being the default. If you want to build with Qt6 instead, pass `-DQT_VERSION=6` to `cmake`. If Qt5 is used, the minimum supported version is 5.15. 27 | 28 | ## Unix 29 | Before starting, make sure you have the development tools Git, CMake and pkg-config installed. Then, install the dependencies listed above per your package manager. On most systems, the Qt requirements can be satisfied by packages named `qt-base` and `qt-tools` or similar, with the later being a build-only requirement that can be uninstalled later. 30 | 31 | ### Building 32 | Clone the repo and create a build directory: 33 | 34 | ```bash 35 | git clone https://github.com/complexlogic/EasyAudioSync.git 36 | cd EasyAudioSync 37 | mkdir build && cd build 38 | ``` 39 | 40 | Generate the Makefile: 41 | 42 | ```bash 43 | cmake .. -DCMAKE_BUILD_TYPE=Release 44 | ``` 45 | 46 | Build and test the program: 47 | 48 | ```bash 49 | make 50 | ./easyaudiosync 51 | ``` 52 | 53 | Optionally, install to your system directories: 54 | 55 | ```bash 56 | sudo make install 57 | ``` 58 | 59 | By default, this will install the program with a prefix of `/usr/local`. If you want a different prefix, re-run the CMake generation step with `-DCMAKE_INSTALL_PREFIX=prefix`. 60 | 61 | ## Windows 62 | The Windows toolchain consists of Visual Studio and vcpkg in addition to Git and CMake. Before starting, make sure that Visual Studio is installed with C++ core desktop features and C++ CMake tools. The free Community Edition is sufficient. 63 | 64 | Clone the repo and create a build directory: 65 | 66 | ```bash 67 | git clone https://github.com/complexlogic/EasyAudioSync.git 68 | cd EasyAudioSync 69 | mkdir build 70 | cd build 71 | ``` 72 | 73 | Build the dependencies and generate the Visual Studio project files (this will take a long time): 74 | 75 | ```bash 76 | cmake .. -DVCPKG=ON 77 | ``` 78 | 79 | Build and test the program: 80 | 81 | ```bash 82 | cmake --build . --config Release 83 | .\Release\easyaudiosync.exe 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | This page contains documentation for Easy Audio Sync's configuration settings. 3 | 4 | 1. [General](#general) 5 | 2. [File Handling](#file-handling) 6 | 3. [Transcoding](#transcoding) 7 | 4. [MP3](#mp3) 8 | 5. [AAC](#aac) 9 | 6. [Ogg Vorbis](#ogg-vorbis) 10 | 7. [Opus](#opus) 11 | 12 | ## General 13 | The settings in this section control the general behavior of the program. 14 | 15 | #### Skip files that exist in destination 16 | When this setting is enabled, the program will check if each file in the source exists in the destination, and skip syncing files any that do. When transcoding, your [File Handling](#file-handling) settings will be taken into account when checking the destination. 17 | 18 | #### Except Files with Older Timestamps 19 | When this setting is enabled and the equivalent source file exists in the destination, the program will compare the files' modification timestamps. If the source file is newer than the destination file, the file will be synced again. This may happen, for example, if you update the metadata on the source file. 20 | 21 | #### Copy Non-audio Files 22 | When this setting is enabled, the program will copy all non-audio files in the source to the destination. For example, if you have artwork files, you may want these transferred to the destination along with the audio files. 23 | 24 | #### Clean Destination Directory 25 | When this setting is enabled, the program will check if each file in the destination exists in the source, and delete any files that do not. Your [File Handling](#file-handling) settings will be taken into account when checking the source. After deleting files, the program will walk the destination directory tree from the bottom to the top and delete any empty directories. 26 | 27 | The files are deleted permanently without any way to recover them. You will not be prompted for confirmation before the files are deleted. **Don't use this setting if your destination contains files other than those created by a previous sync.** 28 | 29 | Misuse of this feature can cause unwanted data loss, so it's important to understand how it works before enabling it. When the enable box is checked, the program will present you with a dialog explaining how the feature works and ask you acknowledge by checking a box to proceed. 30 | 31 | #### Language 32 | Sets the language of the program 33 | 34 | #### Minimum Log Level 35 | Sets the log level of the program. Leave this as "Error", unless you're experiencing issues. 36 | 37 | #### Abort on Errors 38 | Causes the sync to stop whenever an error is encountered. Normally, the program will try to continue when errors occur. 39 | 40 | ## File Handling 41 | The settings in this section determine how files in the source are handled when synced to the destination. The codecs in the leftmost column represent the input files from the source. With the radio buttons, choose whether to copy or transcode files of that particular format. If transcode is selected, pick an output codec from the combo box on the right. 42 | 43 | The usual strategy when syncing a library to mobile is to transcode lossless files to a lossy format, and copy files that are already in a lossy format. 44 | 45 | ## Transcoding 46 | The settings in this section determine how files are transcoded. Here are a few general notes about transcoding: 47 | - Bit rate is the amount of data that is used to encode an audio signal. In "lossy" encoding, more bitrate results in better audio quality, up to a subjective point called "transparency", where a lossy-encoded signal sounds indistinguishable from its source signal. 48 | - CBR refers to "Constant Bit Rate" and VBR refers to "Variable Bit Rate". 49 | - In CBR, the same amount of data is used to encode a signal at all points in time. 50 | - In VBR, the amount of data varies based on the complexity of the source at a given point in time. 51 | - Generally, VBR is superior to CBR because it results in a higher quality at the same average bitrate. 52 | - The bitrate settings in Easy Audio Sync are normalized to stereo (2 channel) files. The actual encoded bitrate will depend on the output channel layout. This normalization system allows the bitrate to scale, resulting in the same quality regardless of the channel layout. For example, if the bit rate is selected as 128 kbps: 53 | - If the source is mono, the actual bitrate will be 128 * (1/2) = 64 kbps. 54 | - If the source is a 5 channel surround, the actual bitrate will be 128 * (5/2) = 320 kbps. 55 | 56 | The following settings are available in the transcoding section: 57 | 58 | #### Copy Metadata 59 | When enabled, the program will copy the source's metadata "tags" when transcoding. 60 | 61 | There is a significant amount of fragmentation in the audio metadata ecosystem due to vague or non-existent standards. Internally, the program uses the [same tag mappings as MusicBrainz Picard](https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html). The MusicBrainz Picard project is one of the most authoritative references on audio metadata. You will have the best possible results if your source library is tagged with Picard. 62 | 63 | #### Include Extended Tags 64 | By default, the program only copies "official" tags to the output files. This setting will enable the transfer of custom tags. 65 | 66 | There are a few tags that have not been implemented. 67 | - Ratings - due to complexities between the various formats 68 | - Anything related to the input file itself rather than the audio it contains, such as original filename, encoder, encoder settings, etc. It's not appropriate to transfer these when creating a new file 69 | - Original date - again, due to complexities caused by format differences 70 | 71 | #### Copy Cover Art 72 | When enabled, the program will copy the source's embedded cover art when transcoding. 73 | 74 | #### Downsample High Resolution Audio 75 | When enabled, it sets the maximum sampling rate in output files to 48 kHz: 76 | - If the source sampling rate is 48 kHz or less, the sampling rate will be preserved to the output (assuming it's supported by the encoder). 77 | - If the source sampling rate is greater than 48 kHz, the audio will be downsampled to 48 kHz in the output. 78 | 79 | #### Downmix Multi-channel Audio 80 | When enabled, it sets the maximum number of channels in output files to 2: 81 | - If the source has 2 channels or fewer, the channel layout will be preserved to the output 82 | - If the source has more than 2 channels, the audio will be downmixed to 2 channels in the output. 83 | 84 | #### Resampling Enginer 85 | This setting controls the resampling engine that is used when converting between sampling rates. SW is the "native" FFmpeg resampler, and SoX is an external resampler that some believe to be of higher quality. You can expect a slight performance penalty when using SoX. It's about 10% in my testing, but your results will vary depending on your hardware, library composition, and other program settings. 86 | 87 | #### Number of CPU Threads 88 | This sets the number of threads that are used to transcode. Can be maximum (provided by OS at runtime), or a specific number. More threads will yield faster transcode times. 89 | 90 | #### ReplayGain 91 | This setting reads the ReplayGain metadata tags in the source file, and adjusts the volume of output file's encoded audio stream by the gain value. Both track level and album level tags are supported. If album level is set but not available in the file, it will fall back to track level. After adjusting the volume, the ReplayGain metadata will be stripped from the output file. 92 | 93 | Note that this setting only applies to transcoded files; copied files are not modified in any way. If your [File Handling](#file-handling) settings contain a mixture of transcoding and copying, you will need to take that into account. 94 | 95 | ### MP3 96 | The settings in this section pertain to transcoding MP3 files 97 | 98 | #### Encoder Preset 99 | This setting controls the bitrate of the encoder. Both CBR and VBR are supported. VBR is generally preferable, but there are a few legacy players that support CBR MP3s only. 100 | 101 | Refer to the [HydrogenAudio LAME Recommendations](https://wiki.hydrogenaud.io/index.php/LAME#Recommended_encoder_settings) for guidance on the preset selection. Transparency for VBR is usually between V4 and V2, depending on the listener and the environment 102 | 103 | #### ID3 Version 104 | If [Copy Metadata](#copy-metadata) is enabled, this setting controls the version of the ID3 tags. There are a few players that still don't support 2.4, so it's recommended to stick with 2.3 unless you have a specific reason to prefer 2.4. 105 | 106 | ### AAC 107 | The settings in this section pertain to transcoding AAC files. 108 | 109 | There are two AAC encoders available: Fraunhofer FDK AAC and libavcodec AAC. Fraunhofer FDK AAC is superior to libavcodec AAC in both quality and speed, and you should use it if it's available to you. The reason libavcodec AAC is supported is because Fraunhofer FDK AAC is less widely available due to legal uncertainties over the Fraunhofer Society's patent assertions. Despite this, the official Windows and macOS builds as well as the Linux packages all include support for the Fraunhofer FDK AAC encoder. 110 | 111 | All transcoded AAC files will be of Low Complexity object type (AAC-LC), regardless of the encoder or preset chosen. 112 | 113 | #### Encoder 114 | This combo box allows you to select which encoder is used for AAC files. The program checks for the availability at startup. You may not have both encoders available depending on your build configuration. 115 | 116 | #### Fraunhofer FDK AAC 117 | The settings in this section control the Fraunhofer FDK AAC encoder 118 | 119 | ##### Encoder Preset 120 | This setting controls the bitrate of the encoder. Both CBR and VBR are supported. The VBR modes Low, Medium, and High correspond to the encoder VBR modes 3, 4, and 5, respectively. Refer to the [Hydrogen Audio page for Fraunhofer FDK AAC](https://wiki.hydrogenaud.io/index.php?title=Fraunhofer_FDK_AAC#Bitrate_Modes) for more information. 121 | 122 | ##### Enable Afterburner 123 | The afterburner feature increases encoding quality at the cost of encoding effort. It's recommended to keep this enabled. 124 | 125 | #### Libavcodec AAC Encoder Preset 126 | This setting controls the bit rate of the libavcodec AAC encoder. Currently, only CBR mode is available; VBR in the libavcodec AAC encoder is considered experimental. 127 | 128 | ### Ogg Vorbis 129 | The settings in this section pertain to transcoding Ogg Vorbis files. 130 | 131 | #### Encoding Quality 132 | This controls the quality of the encoded audio. The value is on the scale of -1 to 10, with higher values resulting in better quality. Reasonable values are in the range of 3-8. Refer to the [HydrogenAudio Ogg Vorbis recommendations](https://wiki.hydrogenaud.io/index.php?title=Recommended_Ogg_Vorbis#Recommended_Encoder_Settings) for guidance on the quality setting. 133 | 134 | ### Opus 135 | The settings in this section pertain to transcoding Opus files 136 | 137 | #### Encoder Preset 138 | This setting controls the bitrate of the encoder. VBR is supported only. Refer to the [HydrogenAudio Opus page](https://wiki.hydrogenaud.io/index.php?title=Opus) for guidance on bitrate selection. 139 | 140 | #### File Extension 141 | This setting controls the file extension of the outputted files. The .opus file extension is preferable, but some older devices support .ogg only. 142 | 143 | #### Convert ReplayGain Tags to R128 Format 144 | Opus files are governed by [RFC 7845](https://datatracker.ietf.org/doc/html/rfc7845), which specifies an alternative loudness normalization scheme to ReplayGain where track and album gain are stored in `R128_TRACK_GAIN` and `R128_ALBUM_GAIN` tags, respectively. If you want to follow the RFC 7845 scheme, this setting will convert the format of any ReplayGain tags from the source files. 145 | 146 | #### Adjust Gain By 147 | The RFC 7845 requires that the gains are referenced to -23 LUFS. If your source files were not referenced to -23 LUFS, you can use this setting to adjust the gain with a constant value. For example, if your source files are normalized to the ReplayGain 2.0 standard -18 LUFS, you can set this to -5 dB to re-reference them to -23 LUFS. 148 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # C++ sources 2 | set(CXX_SOURCES 3 | main.cpp main.hpp 4 | sync.cpp sync.hpp 5 | transcode.cpp transcode.hpp 6 | metadata.cpp metadata.hpp 7 | config.cpp config.hpp 8 | util.cpp util.hpp 9 | settings.cpp settings.hpp 10 | about.cpp about.hpp 11 | ) 12 | set(ICON_RESOURCE ${PROJECT_SOURCE_DIR}/assets/icons/icons.qrc) 13 | 14 | # UI 15 | set(UI_FILES 16 | mainwindow.ui 17 | settings.ui 18 | settings_general.ui 19 | settings_file_handling.ui 20 | settings_transcoding.ui 21 | settings_mp3.ui 22 | settings_aac.ui 23 | settings_ogg_vorbis.ui 24 | settings_opus.ui 25 | clean_dest_warning.ui 26 | about.ui 27 | ) 28 | foreach (FILE ${UI_FILES}) 29 | set(UI_PATHS ${UI_PATHS} ${CMAKE_CURRENT_SOURCE_DIR}/ui/${FILE}) 30 | endforeach () 31 | 32 | # Translations 33 | file(STRINGS "${PROJECT_SOURCE_DIR}/translations/languages.txt" LANGUAGES) 34 | foreach(LANG ${LANGUAGES}) 35 | list(GET LANG 0 CODE) 36 | list(GET LANG 1 NAME) 37 | list(APPEND TS_FILES "${PROJECT_SOURCE_DIR}/translations/${CODE}.ts") 38 | list(APPEND LANGS " { \"${CODE}\", \"${NAME}\" },\n") 39 | endforeach () 40 | list(LENGTH LANGS NB_LANGS) 41 | string(REPLACE ";" "" LANG_PAIRS ${LANGS}) 42 | configure_file("${PROJECT_SOURCE_DIR}/translations/languages.hpp.in" "${PROJECT_BINARY_DIR}/languages.hpp") 43 | 44 | qt_add_translation(QM_FILES "${TS_FILES}") 45 | foreach (FILE ${QM_FILES}) 46 | get_filename_component(BASENAME ${FILE} NAME) 47 | string(APPEND TRANSLATION_FILES " ${BASENAME}\n") 48 | endforeach () 49 | configure_file(${PROJECT_SOURCE_DIR}/translations/translations.qrc.in ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc) 50 | set (SOURCE_FILES ${CXX_SOURCES} ${ICON_RESOURCE} ${CMAKE_CURRENT_BINARY_DIR}/translations.qrc) 51 | qt_wrap_ui(SOURCE_FILES ${UI_PATHS}) 52 | string(TIMESTAMP BUILD_DATE "%Y-%m-%d") 53 | add_compile_definitions("BUILD_DATE=\"${BUILD_DATE}\"") 54 | if (UNIX) 55 | if (APPLE) 56 | set(MACOS_ICON "${PROJECT_SOURCE_DIR}/assets/icons/${EXECUTABLE_NAME}.icns") 57 | set_source_files_properties("${MACOS_ICON}" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") 58 | list(APPEND SOURCE_FILES "${MACOS_ICON}") 59 | endif () 60 | add_executable(${EXECUTABLE_NAME} ${SOURCE_FILES}) 61 | target_link_libraries(${EXECUTABLE_NAME} 62 | Qt${QT_VERSION}::Core 63 | Qt${QT_VERSION}::Widgets 64 | Qt${QT_VERSION}::Gui 65 | PkgConfig::LIBAVFORMAT 66 | PkgConfig::LIBAVCODEC 67 | PkgConfig::LIBSWRESAMPLE 68 | PkgConfig::LIBAVFILTER 69 | PkgConfig::LIBAVUTIL 70 | PkgConfig::FMT 71 | PkgConfig::SPDLOG 72 | PkgConfig::TAGLIB 73 | Threads::Threads 74 | ) 75 | 76 | if (STRIP_BINARY) 77 | add_custom_command(TARGET ${EXECUTABLE_NAME} 78 | POST_BUILD 79 | COMMAND ${STRIP} "${PROJECT_BINARY_DIR}/${EXECUTABLE_NAME}" 80 | ) 81 | endif () 82 | if (APPLE) 83 | if (DMG) 84 | set_target_properties(${EXECUTABLE_NAME} PROPERTIES 85 | MACOSX_BUNDLE ON 86 | MACOSX_BUNDLE_EXECUTABLE_NAME "${EXECUTABLE_NAME}" 87 | MACOSX_BUNDLE_INFO_STRING "${PROJECT_DESCRIPTION}" 88 | MACOSX_BUNDLE_ICON_FILE "${EXECUTABLE_NAME}.icns" 89 | MACOSX_BUNDLE_GUI_IDENTIFIER "${RDNS_NAME}" 90 | MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_VERSION}" 91 | MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}" 92 | MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}" 93 | MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}" 94 | MACOSX_BUNDLE_COPYRIGHT "Public Domain" 95 | 96 | ) 97 | install(CODE "include(BundleUtilities)\nfixup_bundle(\"${PROJECT_BINARY_DIR}/${EXECUTABLE_NAME}.app\" \"\" \"\")") 98 | add_custom_target(my_install COMMAND ${CMAKE_COMMAND} --build . --target install WORKING_DIRECTORY "${PROJECT_BINARY_DIR}") 99 | add_custom_target(dmg 100 | COMMAND mv "${EXECUTABLE_NAME}.app" "${PROJECT_NAME}.app" # fixup_bundle won't accept app names with spaces so need to manually rename 101 | COMMAND "${MACDEPLOYQT}" "${PROJECT_NAME}.app" -dmg 102 | COMMAND mv "${PROJECT_NAME}.dmg" "${EXECUTABLE_NAME}-${PROJECT_VERSION}-${CMAKE_OSX_ARCHITECTURES}.dmg" 103 | WORKING_DIRECTORY "${PROJECT_BINARY_DIR}") 104 | add_dependencies(dmg my_install) 105 | endif () 106 | else () 107 | install(TARGETS ${EXECUTABLE_NAME} DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") 108 | set (DATA_DIR "${CMAKE_INSTALL_PREFIX}/share") 109 | install(FILES "${PROJECT_BINARY_DIR}/${RDNS_NAME}.desktop" DESTINATION "${DATA_DIR}/applications") 110 | foreach (ICON_SIZE 48 64 128 256) 111 | install(FILES "${PROJECT_SOURCE_DIR}/assets/icons/${EXECUTABLE_NAME}${ICON_SIZE}.png" 112 | DESTINATION "${DATA_DIR}/icons/hicolor/${ICON_SIZE}x${ICON_SIZE}/apps" 113 | RENAME "${RDNS_NAME}.png" 114 | ) 115 | endforeach () 116 | install (FILES "${PROJECT_SOURCE_DIR}/assets/icons/${EXECUTABLE_NAME}.svg" 117 | DESTINATION "${DATA_DIR}/icons/hicolor/scalable/apps" 118 | RENAME "${RDNS_NAME}.svg" 119 | ) 120 | 121 | if (PACKAGE) 122 | set(CPACK_PACKAGE_DESCRIPTION_SUMMARY ${PROJECT_DESCRIPTION}) 123 | set(CPACK_PACKAGE_NAME ${EXECUTABLE_NAME}) 124 | set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) 125 | set(CPACK_GENERATOR ${PACKAGE}) 126 | if (PACKAGE STREQUAL "DEB") 127 | set(CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT") 128 | set(CPACK_DEBIAN_PACKAGE_DEPENDS "qtbase5, libc6 (>=2.36), libstdc++6 (>=12.2)") 129 | set(CPACK_DEBIAN_PACKAGE_MAINTAINER "complexlogic") 130 | set(CPACK_DEBIAN_PACKAGE_SECTION "utils") 131 | set(CPACK_DEBIAN_ARCHIVE_TYPE "gnutar") 132 | set(CPACK_DEBIAN_COMPRESSION_TYPE "gzip") 133 | set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") 134 | elseif (PACKAGE STREQUAL "RPM") 135 | set(CPACK_RPM_FILE_NAME "RPM-DEFAULT") 136 | set(CPACK_RPM_PACKAGE_LICENSE "BSD") 137 | set(CPACK_RPM_PACKAGE_GROUP "Applications/Multimedia") 138 | set(CPACK_RPM_PACKAGE_AUTOREQPROV 0) 139 | set(CPACK_RPM_PACKAGE_REQUIRES "qt5-qtbase, glibc >= 2.38, libstdc++ >= 13.2") 140 | endif () 141 | if (PROJECT_VERSION_GIT) 142 | set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION_GIT}") 143 | endif () 144 | include(CPack) 145 | endif () 146 | endif () 147 | 148 | # Windows build 149 | elseif (WIN32) 150 | set(WINDOWS_RESOURCE "${PROJECT_SOURCE_DIR}/assets/icons/windows.rc") 151 | set(MANIFEST_FILE "${PROJECT_BINARY_DIR}/${EXECUTABLE_NAME}.manifest") 152 | add_executable(${EXECUTABLE_NAME} WIN32 ${SOURCE_FILES} ${MANIFEST_FILE} ${WINDOWS_RESOURCE}) 153 | add_custom_command(TARGET ${EXECUTABLE_NAME} 154 | POST_BUILD 155 | COMMAND ${WINDEPLOYQT} --no-translations --no-network ${PROJECT_BINARY_DIR}/$ 156 | ) 157 | target_compile_options(${EXECUTABLE_NAME} PUBLIC "/Zc:preprocessor") 158 | add_compile_definitions(_CRT_SECURE_NO_WARNINGS) 159 | target_include_directories(${EXECUTABLE_NAME} PUBLIC 160 | ${FFMPEG_INCLUDE_DIR} 161 | ${TAGLIB_INCLUDE_DIR} 162 | ) 163 | target_link_libraries(${EXECUTABLE_NAME} 164 | Qt::Core 165 | Qt::Gui 166 | Qt::Widgets 167 | ${LIBAVFORMAT} 168 | ${LIBAVCODEC} 169 | ${LIBAVFILTER} 170 | ${LIBSWRESAMPLE} 171 | ${LIBAVUTIL} 172 | ${TAGLIB} 173 | spdlog::spdlog 174 | ) 175 | 176 | install(DIRECTORY "${PROJECT_BINARY_DIR}/$/" DESTINATION .) 177 | 178 | # Copy the Visual C++ runtime DLLs in case user doesn't have them installed 179 | set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) 180 | include(InstallRequiredSystemLibraries) 181 | foreach(required_lib vcruntime140.dll vcruntime140_1.dll msvcp140.dll msvcp140_1.dll msvcp140_2.dll) 182 | foreach(system_lib ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS}) 183 | string(FIND ${system_lib} ${required_lib} found_lib) 184 | if (NOT found_lib EQUAL -1) 185 | install(FILES ${system_lib} DESTINATION .) 186 | endif () 187 | endforeach () 188 | endforeach () 189 | if (NSIS_INSTALLER) 190 | set(CPACK_NSIS_INSTALLED_ICON_NAME "${PROJECT_SOURCE_DIR}/assets/icons/${EXECUTABLE_NAME}.ico") 191 | set(CPACK_PACKAGE_INSTALL_DIRECTORY ${PROJECT_NAME}) 192 | set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) 193 | set(CPACK_NSIS_IGNORE_LICENSE_PAGE ON) 194 | set(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${PROJECT_NAME}.lnk' '$INSTDIR\\\\${EXECUTABLE_NAME}.exe' ") 195 | set(CPACK_NSIS_DELETE_ICONS_EXTRA "Delete '$SMPROGRAMS\\\\$START_MENU\\\\${PROJECT_NAME}.lnk'") 196 | set(CPACK_NSIS_MUI_ICON "${PROJECT_SOURCE_DIR}/assets/icons/${EXECUTABLE_NAME}.ico") 197 | set(CPACK_GENERATOR "NSIS") 198 | set(CPACK_NSIS_MANIFEST_DPI_AWARE ON) 199 | set(CPACK_NSIS_COMPRESSOR "lzma") 200 | set(CPACK_PACKAGE_FILE_NAME "${EXECUTABLE_NAME}-${PROJECT_VERSION}-setup") 201 | else () 202 | set(CPACK_GENERATOR "ZIP") 203 | set(CPACK_PACKAGE_NAME ${EXECUTABLE_NAME}) 204 | endif () 205 | if (PROJECT_VERSION_GIT) 206 | set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION_GIT}") 207 | endif () 208 | include(CPack) 209 | endif () 210 | 211 | # Make lupdate target for Qt 6 212 | if (${Qt${QT_VERSION}_VERSION} VERSION_GREATER_EQUAL 6.7) 213 | qt_add_lupdate( 214 | TS_FILES ${TS_FILES} "${PROJECT_SOURCE_DIR}/translations/source.ts" 215 | SOURCE_TARGETS "${EXECUTABLE_NAME}" 216 | LUPDATE_TARGET "${EXECUTABLE_NAME}_lupdate" 217 | ) 218 | endif () 219 | -------------------------------------------------------------------------------- /src/about.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | extern "C" { 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | } 14 | 15 | #include 16 | #if TAGLIB_MAJOR_VERSION > 1 17 | #include 18 | #endif 19 | 20 | #include "ui_about.h" 21 | #include "config.hpp" 22 | #include "about.hpp" 23 | #include 24 | 25 | #define PROGRAM_VER "

%1

" 26 | #define ABOUT_HTML "

%1

%2

" 27 | 28 | AboutDialog::AboutDialog(const Config &config, const QPixmap &icon, QWidget *parent) : 29 | QDialog(parent), 30 | ui(std::make_unique()) 31 | { 32 | ui->setupUi(this); 33 | ui->label->setPixmap(icon); 34 | ui->tabs->setCurrentIndex(0); 35 | ui->program_ver->setText(QString(PROGRAM_VER).arg(QString(tr("Version %1")).arg( 36 | #ifdef PROJECT_VERSION_GIT 37 | PROJECT_VERSION_GIT 38 | #else 39 | PROJECT_VERSION 40 | #endif 41 | ))); 42 | 43 | ui->about_label->setText(QString(ABOUT_HTML) 44 | .arg(tr("Easy Audio Sync is an audio library syncing and conversion utility. " 45 | "The intended use is syncing an audio library with many lossless files to a mobile device with limited storage.")) 46 | .arg(tr("See the %1 for more information.") 47 | .arg(QString("%1") 48 | .arg(tr("GitHub page")) 49 | ) 50 | ) 51 | ); 52 | 53 | // Library Versions 54 | ui->qt_ver->setText(qVersion()); 55 | ffmpeg_version(avformat_version, ui->lavf_ver); 56 | ffmpeg_version(avcodec_version, ui->lavc_ver); 57 | ffmpeg_version(avfilter_version, ui->lavfilter_ver); 58 | ffmpeg_version(swresample_version, ui->lswr_ver); 59 | ffmpeg_version(avutil_version, ui->lavu_ver); 60 | 61 | #if TAGLIB_MAJOR_VERSION > 1 62 | { 63 | const auto tversion = TagLib::runtimeVersion(); 64 | ui->formLayout->addRow( 65 | new QLabel("TagLib:", this), 66 | new QLabel( 67 | QString("%1.%2%3") 68 | .arg(tversion.majorVersion()) 69 | .arg(tversion.minorVersion()) 70 | .arg(tversion.patchVersion() ? QString(".%1").arg(tversion.patchVersion()) : ""), 71 | this 72 | ) 73 | ); 74 | } 75 | #endif 76 | 77 | // Encoders 78 | #define SUPPORT_FEATURE(feature, label) ui->label->setText(feature ? tr("Yes") : tr( "No")) 79 | SUPPORT_FEATURE(config.has_feature.lame, support_lame); 80 | SUPPORT_FEATURE(config.has_feature.fdk_aac, support_fdk_aac); 81 | SUPPORT_FEATURE(config.has_feature.lavc_aac, support_lavc_aac); 82 | SUPPORT_FEATURE(config.has_feature.libvorbis, support_libvorbis); 83 | SUPPORT_FEATURE(config.has_feature.libopus, support_libopus); 84 | SUPPORT_FEATURE(config.has_feature.soxr, support_soxr); 85 | 86 | // Build info 87 | ui->build_date->setText(BUILD_DATE); 88 | #if defined(__GNUC__) && !defined(__clang__) 89 | ui->compiler->setText(QString("GCC %1.%2" 90 | #if __GNUC_PATCHLEVEL__ > 0 91 | ".%3" 92 | #endif 93 | ) 94 | .arg(__GNUC__) 95 | .arg(__GNUC_MINOR__) 96 | #if __GNUC_PATCHLEVEL__ > 0 97 | .arg(__GNUC_PATCHLEVEL__) 98 | #endif 99 | ); 100 | #endif 101 | 102 | #if defined(__clang__) 103 | ui->compiler->setText(QString( 104 | #ifdef __apple_build_version__ 105 | "Apple " 106 | #endif 107 | "Clang %1.%2.%3").arg(__clang_major__).arg( __clang_minor__).arg(__clang_patchlevel__)); 108 | #endif 109 | 110 | #ifdef _MSC_VER 111 | ui->compiler->setText(QString("Microsoft C/C++ %1").arg(QString::number(_MSC_VER / 100.0f, 'f', 2))); 112 | #endif 113 | 114 | } 115 | AboutDialog::~AboutDialog() = default; 116 | 117 | void AboutDialog::ffmpeg_version(unsigned(*fn)(), QLabel *label) 118 | { 119 | int ffver = fn(); 120 | label->setText(QString("%1.%2.%3").arg(AV_VERSION_MAJOR(ffver)).arg(AV_VERSION_MINOR(ffver)).arg(AV_VERSION_MICRO(ffver))); 121 | } 122 | -------------------------------------------------------------------------------- /src/about.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | struct Config; 9 | namespace Ui { 10 | class AboutDialog; 11 | } 12 | class QLabel; 13 | class QPixmap; 14 | 15 | class AboutDialog : public QDialog 16 | { 17 | Q_OBJECT 18 | 19 | private: 20 | std::unique_ptr ui; 21 | 22 | void ffmpeg_version(unsigned(*fn)(), QLabel *label); 23 | 24 | public: 25 | void closeEvent(QCloseEvent *event) override { event->accept(); } 26 | explicit AboutDialog(const Config &config, const QPixmap &icon, QWidget *parent = nullptr); 27 | ~AboutDialog() override; 28 | }; 29 | -------------------------------------------------------------------------------- /src/basic_lazy_file_sink.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // spdlog Copyright(c) 2015-2018 Gabi Melman, see https://github.com/gabime/spdlog 3 | // this is a simple lazy variant of https://github.com/gabime/spdlog/blob/v1.x/include/spdlog/sinks/basic_file_sink.h 4 | // 5 | 6 | #pragma once 7 | 8 | #ifndef SPDLOG_H 9 | #include 10 | #endif 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | namespace spdlog { 20 | namespace sinks { 21 | /* 22 | * Trivial lazy initialized file sink with single file as target 23 | */ 24 | template 25 | class basic_lazy_file_sink final : public base_sink 26 | { 27 | public: 28 | explicit basic_lazy_file_sink(const filename_t &filename, bool truncate = false) 29 | { 30 | filename_ = filename; 31 | truncate_ = truncate; 32 | } 33 | 34 | protected: 35 | void sink_it_(const details::log_msg &msg) override 36 | { 37 | if(file_helper_.filename().empty()) 38 | { 39 | // not yet initialized, do it now 40 | try 41 | { 42 | file_helper_.open(filename_, truncate_); 43 | } 44 | catch (const spdlog_ex&) 45 | { 46 | 47 | } 48 | } 49 | 50 | 51 | if(!file_helper_.filename().empty()) 52 | { 53 | //fmt::memory_buffer formatted; 54 | //sink::formatter_->format(msg, formatted); 55 | memory_buf_t formatted; 56 | base_sink::formatter_->format(msg, formatted); 57 | file_helper_.write(formatted); 58 | } 59 | } 60 | 61 | void flush_() override 62 | { 63 | file_helper_.flush(); 64 | } 65 | 66 | private: 67 | filename_t filename_; 68 | bool truncate_; 69 | details::file_helper file_helper_; 70 | }; 71 | 72 | using basic_lazy_file_sink_mt = basic_lazy_file_sink; 73 | using basic_lazy_file_sink_st = basic_lazy_file_sink; 74 | 75 | } // namespace sinks 76 | 77 | // 78 | // factory functions 79 | // 80 | template 81 | inline std::shared_ptr basic_lazy_logger_mt(const std::string &logger_name, const filename_t &filename, bool truncate = false) 82 | { 83 | return Factory::template create(logger_name, filename, truncate); 84 | } 85 | 86 | template 87 | inline std::shared_ptr basic_lazy_logger_st(const std::string &logger_name, const filename_t &filename, bool truncate = false) 88 | { 89 | return Factory::template create(logger_name, filename, truncate); 90 | } 91 | 92 | } // namespace spdlog 93 | -------------------------------------------------------------------------------- /src/config.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | extern "C" { 13 | #include 14 | #include 15 | #include 16 | } 17 | 18 | #include "config.hpp" 19 | #include "util.hpp" 20 | 21 | #define HAS_ENCODER(e) avcodec_find_encoder_by_name(e) != nullptr 22 | 23 | void Config::check_features() 24 | { 25 | has_feature.lame = HAS_ENCODER("libmp3lame"); 26 | has_feature.fdk_aac = HAS_ENCODER("libfdk_aac"); 27 | has_feature.lavc_aac = HAS_ENCODER("aac"); 28 | has_feature.libvorbis = HAS_ENCODER("libvorbis"); 29 | has_feature.libopus = HAS_ENCODER("libopus"); 30 | if (has_feature.lame) 31 | codec_support.insert(Codec::MP3); 32 | if (has_feature.lavc_aac || has_feature.fdk_aac) 33 | codec_support.insert(Codec::AAC); 34 | if (has_feature.libvorbis) 35 | codec_support.insert(Codec::VORBIS); 36 | if (has_feature.libopus) 37 | codec_support.insert(Codec::OPUS); 38 | 39 | // Check for SoX resampler 40 | SwrContext *swr_ctx = nullptr; 41 | AVChannelLayout ch; 42 | av_channel_layout_default(&ch, 2); 43 | if (!swr_alloc_set_opts2(&swr_ctx, &ch, AV_SAMPLE_FMT_S16, 44100, &ch, AV_SAMPLE_FMT_S16, 48000, 0, nullptr)) { 44 | av_opt_set_int(swr_ctx, "resampler", SWR_ENGINE_SOXR, 0); 45 | if (swr_init(swr_ctx) == 0) { 46 | has_feature.soxr = true; 47 | resampling_engine.add("soxr", "SoX", ResamplingEngine::SOXR); 48 | } 49 | swr_free(&swr_ctx); 50 | } 51 | } 52 | 53 | std::pair Config::get_file_handling(const std::filesystem::path &path) const 54 | { 55 | Codec in_codec = codec_from_path(path); 56 | auto it = std::find_if(file_handling.cbegin(), 57 | file_handling.cend(), 58 | [&](const auto &i){return i.in_codec == in_codec; } 59 | ); 60 | return it == file_handling.end() ? std::make_pair(Action::COPY, Codec::NONE) : std::make_pair(it->action, it->out_codec); 61 | } 62 | 63 | Codec Config::get_input_codec(Codec out_codec) const 64 | { 65 | auto it = std::find_if(file_handling.cbegin(), 66 | file_handling.cend(), 67 | [&](const auto &i){return i.out_codec == out_codec;} 68 | ); 69 | assert(it != file_handling.end()); 70 | return it == file_handling.end() ? Codec::NONE : it->in_codec; 71 | } 72 | 73 | // Determine the output file extension based on the output codec, and possibly 74 | // the input path 75 | std::string Config::get_output_ext(Codec codec, const std::filesystem::path &path) const 76 | { 77 | static const std::vector> codecs { 78 | {Codec::MP3, ".mp3"}, 79 | {Codec::FLAC, ".flac"}, 80 | {Codec::VORBIS, ".ogg"}, 81 | {Codec::WMA, ".wma"}, 82 | {Codec::WAV, ".wav"}, 83 | {Codec::WAVPACK, ".wv"}, 84 | {Codec::APE, ".ape"}, 85 | {Codec::MPC, ".mpc"} 86 | }; 87 | 88 | if (codec == Codec::OPUS) 89 | return opus.ext; 90 | if (codec == Codec::AAC || codec == Codec::ALAC) { 91 | if (filetype_from_path(path) == FileType::MP4) 92 | return path.extension().string(); 93 | else 94 | return std::string(".m4a"); 95 | } 96 | auto it = std::find_if(codecs.begin(), codecs.end(), [&](auto i){return i.first == codec;}); 97 | if (it != codecs.end()) 98 | return it->second; 99 | else 100 | return std::string(); 101 | } 102 | 103 | std::string Config::get_encoder_name(Codec codec) const 104 | { 105 | auto get_encoder = [=](const std::pair &e){ return e.first == codec; }; 106 | auto it = std::find_if(encoders.begin(), encoders.end(), get_encoder); 107 | assert(it != encoders.end()); 108 | return it == encoders.end() ? std::string() : it->second; 109 | } 110 | 111 | bool Config::set_encoder_name(Codec codec, const std::string &encoder) 112 | { 113 | auto set_encoder = [=](const std::pair &e){ return e.first == codec; }; 114 | auto it = std::find_if(encoders.begin(), encoders.end(), set_encoder); 115 | assert(it != encoders.end()); 116 | if (it != encoders.end()) { 117 | it->second = encoder; 118 | return true; 119 | } 120 | return false; 121 | } 122 | 123 | void Config::load(const QSettings &settings) 124 | { 125 | // General 126 | skip_existing = settings.value("skip_existing", skip_existing).toBool(); 127 | sync_newer = settings.value("sync_newer", sync_newer).toBool(); 128 | copy_nonaudio = settings.value("copy_nonaudio", copy_nonaudio).toBool(); 129 | clean_dest = settings.value("clean_dest", clean_dest).toBool(); 130 | QString lang = settings.value("language").toString(); 131 | if (!lang.isEmpty()) { 132 | auto it = std::find_if(langs.begin(), 133 | langs.end(), 134 | [&](const auto &i) { return i.first == lang; } 135 | ); 136 | if (it != langs.end()) 137 | this->lang = lang; 138 | } 139 | 140 | log_level.set(settings.value("log_level").toString()); 141 | abort_on_error = settings.value("abort_on_error", abort_on_error).toBool(); 142 | 143 | // Transcoding 144 | copy_metadata = settings.value("copy_metadata", copy_metadata).toBool(); 145 | extended_tags = settings.value("extended_tags", extended_tags).toBool(); 146 | copy_artwork = settings.value("copy_artwork", copy_artwork).toBool(); 147 | resampling_engine.set(settings.value("resampling_engine").toString()); 148 | downsample_hi_res = settings.value("downsample_hi_res", downsample_hi_res).toBool(); 149 | downmix_multichannel = settings.value("downsample_hi_res", downmix_multichannel).toBool(); 150 | rg_mode.set(settings.value("replaygain_mode").toString()); 151 | cpu_max_threads = settings.value("cpu_max_threads", cpu_max_threads).toBool(); 152 | int threads = settings.value("cpu_threads", cpu_threads).toInt(); 153 | int max_threads = std::thread::hardware_concurrency(); 154 | if (threads > max_threads) 155 | threads = max_threads; 156 | else if (threads < 1) 157 | threads = 1; 158 | cpu_threads = threads; 159 | 160 | // File handling 161 | static const std::pair transcode_inputs[] { 162 | {"flac", Codec::FLAC}, {"alac", Codec::ALAC}, {"wv", Codec::WAVPACK}, {"ape", Codec::APE}, 163 | {"wav", Codec::WAV}, {"mp3", Codec::MP3}, {"aac", Codec::AAC}, {"vorbis", Codec::VORBIS}, 164 | {"opus", Codec::OPUS}, {"mpc", Codec::MPC}, {"mp3", Codec::MP3}, {"aac", Codec::AAC}, 165 | {"vorbis", Codec::VORBIS}, {"opus", Codec::OPUS} 166 | }; 167 | const std::array, 4> transcode_outputs {{ 168 | {"mp3", Codec::MP3}, {"aac", Codec::AAC}, {"vorbis", Codec::VORBIS}, {"opus", Codec::OPUS} 169 | }}; 170 | 171 | for (const auto &[name, codec] : transcode_inputs) { 172 | QString val = settings.value("action_" + name).toString(); 173 | Action action = (!val.isEmpty() && val == "transcode") ? Action::TRANSCODE : Action::COPY; 174 | Codec in_codec = codec; // Clang bugworkaround (capturing structured binding) 175 | auto it = std::find_if(file_handling.begin(), file_handling.end(), [&](const auto &i) {return i.in_codec == in_codec;}); 176 | if (it != file_handling.end()) { 177 | it->action = action; 178 | val = settings.value("transcode_" + name).toString(); 179 | if (!val.isEmpty()) { 180 | auto it2 = std::find_if(transcode_outputs.cbegin(), 181 | transcode_outputs.cend(), 182 | [&](const auto &i){ return i.first == val; } 183 | ); 184 | Codec out_codec = it2 == transcode_outputs.cend() ? Codec::MP3 : it2->second; 185 | it->out_codec = out_codec; 186 | } 187 | } 188 | } 189 | 190 | // MP3 191 | mp3.preset.set(settings.value("mp3_preset").toString()); 192 | mp3.id3v2_version = settings.value("mp3_id3v2_version", 3).toInt(); 193 | 194 | // AAC 195 | QString aac_encoder = settings.value("aac_encoder").toString(); 196 | if (aac_encoder.isEmpty()) 197 | set_encoder_name(Codec::AAC, has_feature.fdk_aac ? "libfdk_aac" : "aac"); 198 | else 199 | set_encoder_name(Codec::AAC, aac_encoder == "libfdk_aac" && has_feature.fdk_aac ? "libfdk_aac" : "aac"); 200 | aac.fdk.preset.set(settings.value("aac_fdk_preset").toString()); 201 | aac.fdk.afterburner = settings.value("aac_fdk_afterburner", aac.fdk.afterburner).toBool(); 202 | aac.lavc.preset.set(settings.value("aac_lavc_preset").toString()); 203 | 204 | // Ogg 205 | int ogg_quality = settings.value("ogg_quality", ogg.quality).toInt(); 206 | if (ogg_quality >= -1 && ogg_quality <= 10) 207 | ogg.quality = ogg_quality; 208 | 209 | // Opus 210 | opus.preset.set(settings.value("opus_preset").toString()); 211 | QString opus_ext = settings.value("opus_ext").toString(); 212 | if (!opus_ext.isEmpty()) 213 | opus.ext = opus_ext == ".ogg" ? ".ogg" : ".opus"; 214 | opus.convert_r128 = settings.value("opus_convert_r128", opus.convert_r128).toBool(); 215 | int r128_adjustment = settings.value("opus_r128_adjustment", opus.r128_adjustment).toInt(); 216 | if (abs(r128_adjustment) <= 10) 217 | opus.r128_adjustment = r128_adjustment; 218 | 219 | } 220 | -------------------------------------------------------------------------------- /src/config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include "util.hpp" 15 | 16 | #define MP3_DEFAULT_BITRATE 256000 17 | #define MP3_DEFAULT_QUALITY 2 18 | #define AAC_DEFAULT_BITRATE 128000 19 | #define AAC_DEFAULT_QUALITY 5 20 | #define VORBIS_DEFAULT_QUALITY 4 21 | #define OPUS_DEFAULT_BITRATE 128000 22 | 23 | class ComboMapBase { 24 | public: 25 | virtual const char* get_title(int i) const = 0; 26 | virtual size_t size() const = 0; 27 | }; 28 | template 29 | class ComboMap : public ComboMapBase { 30 | public: 31 | const char* get_title(int i) const override { return std::get<1>(*(map.begin() + i)); } 32 | size_t size() const override { return map.size(); } 33 | const QString& get_current_key() const { return std::get<0>(*current); } 34 | const T& get_current_value() const { return std::get<2>(*current); } 35 | int get_current_index() const { return (int) (current - map.begin()); } 36 | void set(const QString &key) { 37 | if (key.isEmpty()) 38 | return; 39 | auto it = find(key); 40 | if (it != map.end()) 41 | current = it; 42 | } 43 | void set(int i) { current = map.begin() + i; } 44 | bool add(const QString &key, const char *name, const T& item) { 45 | if (find(key) != map.end()) 46 | return false; 47 | int i = get_current_index(); 48 | map.push_back({key, name, item}); 49 | current = map.begin() + i; 50 | return true; 51 | } 52 | ComboMap(const std::vector> &&map, const QString &initial) 53 | : map(map), current(find(initial)) {} 54 | 55 | private: 56 | std::vector> map; 57 | typename std::vector>::iterator current; 58 | typename std::vector>::iterator find(const QString &key) { 59 | return std::find_if(map.begin(), map.end(), [&](const auto &i) { return key == std::get<0>(i); }); 60 | } 61 | }; 62 | 63 | struct Config { 64 | 65 | // MP3 settings 66 | struct MP3 { 67 | enum Mode { 68 | CBR, 69 | VBR 70 | }; 71 | struct Preset { 72 | Mode mode; 73 | int64_t bit_rate; 74 | int quality; 75 | }; 76 | int id3v2_version = 3; 77 | ComboMap preset { 78 | { 79 | {"vbr_low", QT_TRANSLATE_NOOP("SettingsMP3", "VBR Low (V6)"), {Mode::VBR, MP3_DEFAULT_BITRATE, 6}}, 80 | {"vbr_medium", QT_TRANSLATE_NOOP("SettingsMP3", "VBR Medium (V4)"), {Mode::VBR, MP3_DEFAULT_BITRATE, 4}}, 81 | {"vbr_high", QT_TRANSLATE_NOOP("SettingsMP3", "VBR High (V2)"), {Mode::VBR, MP3_DEFAULT_BITRATE, 2}}, 82 | {"vbr_extreme", QT_TRANSLATE_NOOP("SettingsMP3", "VBR Extreme (V0)"), {Mode::VBR, MP3_DEFAULT_BITRATE, 0}}, 83 | {"cbr_128kbps", QT_TRANSLATE_NOOP("SettingsMP3", "CBR 128 kbps"), {Mode::CBR, 128000, MP3_DEFAULT_QUALITY}}, 84 | {"cbr_192kbps", QT_TRANSLATE_NOOP("SettingsMP3", "CBR 192 kbps"), {Mode::CBR, 192000, MP3_DEFAULT_QUALITY}}, 85 | {"cbr_256kbps", QT_TRANSLATE_NOOP("SettingsMP3", "CBR 256 kbps"), {Mode::CBR, 256000, MP3_DEFAULT_QUALITY}}, 86 | {"cbr_320kbps", QT_TRANSLATE_NOOP("SettingsMP3", "CBR 320 kbps"), {Mode::CBR, 320000, MP3_DEFAULT_QUALITY}}, 87 | }, 88 | "vbr_medium" 89 | }; 90 | 91 | const Preset& get_preset() const { return preset.get_current_value(); } 92 | }; 93 | struct AAC { 94 | enum Mode { 95 | CBR, 96 | VBR 97 | }; 98 | struct FDK { 99 | struct Preset { 100 | Mode mode; 101 | int64_t bit_rate; 102 | int quality; 103 | }; 104 | bool afterburner = true; 105 | ComboMap preset { 106 | { 107 | {"vbr_low", QT_TRANSLATE_NOOP("SettingsAAC", "VBR Low"), {Mode::VBR, AAC_DEFAULT_BITRATE, 3}}, 108 | {"vbr_medium", QT_TRANSLATE_NOOP("SettingsAAC", "VBR Medium"), {Mode::VBR, AAC_DEFAULT_BITRATE, 4}}, 109 | {"vbr_high", QT_TRANSLATE_NOOP("SettingsAAC", "VBR High"), {Mode::VBR, AAC_DEFAULT_BITRATE, 5}}, 110 | {"cbr_96kbps", QT_TRANSLATE_NOOP("SettingsAAC", "CBR 96 kbps"), {Mode::VBR, 96000, AAC_DEFAULT_QUALITY}}, 111 | {"cbr_128kbps", QT_TRANSLATE_NOOP("SettingsAAC", "CBR 128 kbps"), {Mode::VBR, 128000, AAC_DEFAULT_QUALITY}}, 112 | {"cbr_192kbps", QT_TRANSLATE_NOOP("SettingsAAC", "CBR 192 kbps"), {Mode::VBR, 192000, AAC_DEFAULT_QUALITY}}, 113 | {"cbr_256kbps", QT_TRANSLATE_NOOP("SettingsAAC", "CBR 256 kbps"), {Mode::VBR, 256000, AAC_DEFAULT_QUALITY}}, 114 | }, 115 | "vbr_medium" 116 | }; 117 | const Preset& get_preset() const { return preset.get_current_value(); } 118 | }; 119 | struct LAVC { 120 | struct Preset { 121 | int64_t bit_rate; 122 | }; 123 | ComboMap preset { 124 | { 125 | {"cbr_96kbps", QT_TRANSLATE_NOOP("SettingsAAC", "CBR 96 kbps"), {96000}}, 126 | {"cbr_128kbps", QT_TRANSLATE_NOOP("SettingsAAC", "CBR 128 kbps"), {128000}}, 127 | {"cbr_192kbps", QT_TRANSLATE_NOOP("SettingsAAC", "CBR 192 kbps"), {192000}}, 128 | {"cbr_256kbps", QT_TRANSLATE_NOOP("SettingsAAC", "CBR 256 kbps"), {256000}}, 129 | }, 130 | "cbr_128kbps" 131 | }; 132 | const Preset& get_preset() const { return preset.get_current_value(); } 133 | }; 134 | FDK fdk; 135 | LAVC lavc; 136 | int profile = FF_PROFILE_AAC_LOW; 137 | }; 138 | struct OGG { 139 | int quality = VORBIS_DEFAULT_QUALITY; 140 | }; 141 | struct Opus { 142 | struct Preset { 143 | int64_t bit_rate; 144 | }; 145 | bool convert_r128 = false; 146 | int r128_adjustment = 0; 147 | std::string ext = ".opus"; 148 | ComboMap preset { 149 | { 150 | {"vbr_64kbps", QT_TRANSLATE_NOOP("SettingsOpus", "VBR 64 kbps"), {64000}}, 151 | {"vbr_96kbps", QT_TRANSLATE_NOOP("SettingsOpus", "VBR 96 kbps"), {96000}}, 152 | {"vbr_128kbps", QT_TRANSLATE_NOOP("SettingsOpus", "VBR 128 kbps"), {128000}}, 153 | {"vbr_160bps", QT_TRANSLATE_NOOP("SettingsOpus", "VBR 160 kbps"), {160000}}, 154 | {"vbr_192kbps", QT_TRANSLATE_NOOP("SettingsOpus", "VBR 192 kbps"), {192000}}, 155 | {"vbr_256kbps", QT_TRANSLATE_NOOP("SettingsOpus", "VBR 256 kbps"), {256000}}, 156 | }, 157 | "vbr_128kbps" 158 | }; 159 | const Preset& get_preset() const { return preset.get_current_value(); } 160 | }; 161 | struct Features { 162 | bool lame = false; 163 | bool fdk_aac = false; 164 | bool lavc_aac = false; 165 | bool libvorbis = false; 166 | bool libopus = false; 167 | bool soxr = false; 168 | }; 169 | enum ReplayGainMode { 170 | NONE, 171 | TRACK, 172 | ALBUM 173 | }; 174 | enum ResamplingEngine { 175 | SWR, 176 | SOXR 177 | }; 178 | 179 | ComboMap rg_mode { 180 | { 181 | {"none", QT_TRANSLATE_NOOP("SettingsTranscoding", "Don't encode ReplayGain"), ReplayGainMode::NONE}, 182 | {"track", QT_TRANSLATE_NOOP("SettingsTranscoding", "Encode track gain"), ReplayGainMode::TRACK}, 183 | {"album", QT_TRANSLATE_NOOP("SettingsTranscoding", "Encode album gain"), ReplayGainMode::ALBUM}, 184 | }, 185 | "none" 186 | }; 187 | 188 | ComboMap log_level { 189 | { 190 | {"error", QT_TRANSLATE_NOOP("SettingsGeneral", "Error"), spdlog::level::err}, 191 | {"info", QT_TRANSLATE_NOOP("SettingsGeneral", "Info"), spdlog::level::info}, 192 | {"debug", QT_TRANSLATE_NOOP("SettingsGeneral", "Debug"), spdlog::level::debug}, 193 | }, 194 | "error" 195 | }; 196 | 197 | ComboMap resampling_engine { 198 | { 199 | {"swr", "SW", ResamplingEngine::SWR}, 200 | }, 201 | "swr" 202 | }; 203 | 204 | std::vector> encoders { 205 | {Codec::MP3, "libmp3lame"}, 206 | {Codec::AAC, "libfdk_aac"}, 207 | {Codec::VORBIS, "libvorbis"}, 208 | {Codec::OPUS, "libopus"} 209 | }; 210 | 211 | std::array file_handling {{ 212 | { Codec::MP3, Action::COPY, Codec::MP3 }, 213 | { Codec::FLAC, Action::COPY, Codec::MP3 }, 214 | { Codec::VORBIS, Action::COPY, Codec::MP3 }, 215 | { Codec::OPUS, Action::COPY, Codec::MP3 }, 216 | { Codec::AAC, Action::COPY, Codec::MP3 }, 217 | { Codec::ALAC, Action::COPY, Codec::MP3 }, 218 | { Codec::WAV, Action::COPY, Codec::MP3 }, 219 | { Codec::WAVPACK, Action::COPY, Codec::MP3 }, 220 | { Codec::APE, Action::COPY, Codec::MP3 }, 221 | { Codec::MPC, Action::COPY,Codec::MP3 }, 222 | { Codec::WMA, Action::COPY, Codec::MP3 } 223 | }}; 224 | 225 | #include 226 | 227 | MP3 mp3; 228 | AAC aac; 229 | OGG ogg; 230 | Opus opus; 231 | Features has_feature; 232 | std::unordered_set codec_support; 233 | 234 | bool copy_metadata = true; 235 | bool extended_tags = false; 236 | bool copy_artwork = true; 237 | bool skip_existing = true; 238 | bool sync_newer = true; 239 | bool abort_on_error = false; 240 | bool clean_dest = false; 241 | bool copy_nonaudio = false; 242 | int cpu_threads = 1; 243 | bool cpu_max_threads = true; 244 | bool downsample_hi_res = true; 245 | bool downmix_multichannel = true; 246 | QString lang; 247 | 248 | void check_features(); 249 | std::pair get_file_handling(const std::filesystem::path &path) const; 250 | Codec get_input_codec(Codec out_codec) const; 251 | std::string get_encoder_name(Codec codec) const; 252 | bool set_encoder_name(Codec codec, const std::string &encoder); 253 | std::string get_output_ext(Codec codec, const std::filesystem::path &path) const; 254 | void load(const QSettings &settings); 255 | spdlog::level::level_enum get_log_level() const { return log_level.get_current_value(); } 256 | }; 257 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #ifdef _WIN32 11 | #include 12 | #else 13 | #include 14 | #endif 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | #include "main.hpp" 28 | #include "sync.hpp" 29 | #include "config.hpp" 30 | #include "util.hpp" 31 | #include "settings.hpp" 32 | #include "about.hpp" 33 | #include 34 | 35 | #include "ui_mainwindow.h" 36 | 37 | #define LOG_FILENAME EXECUTABLE_NAME ".log" 38 | 39 | Application::Application(QWidget *parent) : 40 | QMainWindow(parent), 41 | ui(std::make_unique()), 42 | icon(std::make_unique(":/icons/easyaudiosync256.png")), 43 | config(std::make_unique()), 44 | style(QApplication::style()), 45 | settings(EXECUTABLE_NAME, EXECUTABLE_NAME) 46 | { 47 | config->check_features(); 48 | config->load(settings); 49 | #ifdef _WIN32 50 | char buffer[MAX_PATH]; 51 | if (GetEnvironmentVariableA("USERPROFILE", buffer, sizeof(buffer))) 52 | log_path = join_paths(buffer, "." EXECUTABLE_NAME, LOG_FILENAME); 53 | #else 54 | #ifdef __APPLE__ 55 | log_path = join_paths(getenv("HOME"), "Library", EXECUTABLE_NAME, LOG_FILENAME); 56 | #else 57 | const char* const XDG_DATA = getenv("XDG_DATA_HOME"); 58 | if (XDG_DATA) 59 | log_path = join_paths(XDG_DATA, EXECUTABLE_NAME, LOG_FILENAME); 60 | else 61 | log_path = join_paths(getenv("HOME"), ".local", "share", EXECUTABLE_NAME, LOG_FILENAME); 62 | #endif 63 | #endif 64 | 65 | // Load translator 66 | if (config->lang.isEmpty()) { 67 | if (!translator.load(QLocale(), "", "", ":/translations")) 68 | std::ignore = translator.load("en", ":/translations"); 69 | QString lang(QFileInfo(translator.filePath()).baseName()); 70 | settings.setValue("language", lang); 71 | } 72 | else 73 | std::ignore = translator.load(config->lang, ":/translations"); 74 | QCoreApplication::installTranslator(&translator); 75 | ui->setupUi(this); 76 | ui->stop_button->setEnabled(false); 77 | ui->stop_button->setIcon(style->standardIcon(QStyle::SP_BrowserStop)); 78 | #ifdef _WIN32 79 | ui->stop_button->setIconSize({ 22, 22 }); 80 | ui->stop_button->setStyleSheet("padding: 4px;"); 81 | #else 82 | ui->stop_button->setStyleSheet("padding: 2px;"); 83 | #endif 84 | read_settings(); 85 | 86 | set_sync_button_state(); 87 | } 88 | Application::~Application() = default; 89 | 90 | void Application::read_settings() 91 | { 92 | QString source_dir = settings.value("source_dir").toString(); 93 | if (!source_dir.isEmpty()) { 94 | std::filesystem::path path(source_dir.toStdString()); 95 | if (std::filesystem::exists(path) && std::filesystem::is_directory(path)) { 96 | ui->source_dir_box->setText(source_dir); 97 | ui->source_dir_box->setCursorPosition(0); 98 | source = std::move(path); 99 | } 100 | } 101 | QString dest_dir = settings.value("dest_dir").toString(); 102 | if (!dest_dir.isEmpty()) { 103 | std::filesystem::path path(dest_dir.toStdString()); 104 | if (std::filesystem::exists(path) && std::filesystem::is_directory(path)) { 105 | ui->dest_dir_box->setText(dest_dir); 106 | ui->dest_dir_box->setCursorPosition(0); 107 | dest = std::move(path); 108 | } 109 | } 110 | #ifdef PERSIST_GEOMETRY 111 | restore_window_size(this, "MainWindow", settings); 112 | restore_window_pos(this, "MainWindow", settings); 113 | #endif 114 | } 115 | 116 | void Application::write_settings() 117 | { 118 | settings.setValue("settings_version", SETTINGS_VERSION); 119 | settings.setValue("source_dir", ui->source_dir_box->text()); 120 | settings.setValue("dest_dir", ui->dest_dir_box->text()); 121 | #ifdef PERSIST_GEOMETRY 122 | save_window_size(this, "MainWindow", settings); 123 | save_window_pos(this, "MainWindow", settings); 124 | #endif 125 | } 126 | 127 | void Application::on_browse_source_button_clicked() 128 | { 129 | open_directory(source, ui->source_dir_box, tr("Select Source Directory")); 130 | } 131 | 132 | void Application::on_browse_dest_button_clicked() 133 | { 134 | open_directory(dest, ui->dest_dir_box, tr("Select Destination Directory")); 135 | } 136 | 137 | void Application::open_directory(std::filesystem::path &path, QLineEdit *box, const QString &title) 138 | { 139 | QString dir = QFileDialog::getExistingDirectory(this, title, box->text()); 140 | if (!dir.isNull()) { 141 | #ifdef _WIN32 142 | dir = QDir::toNativeSeparators(dir); 143 | #endif 144 | box->setText(dir); 145 | box->setCursorPosition(0); 146 | path = dir.toStdString(); 147 | } 148 | set_sync_button_state(); 149 | } 150 | 151 | void Application::show_dialog(QMessageBox::Icon icon, const QString &&title, const QString &&message) 152 | { 153 | auto msg_box = new QMessageBox(icon, 154 | title, 155 | message, 156 | QMessageBox::Ok, 157 | this 158 | ); 159 | msg_box->setModal(true); 160 | msg_box->setAttribute(Qt::WA_DeleteOnClose, true); 161 | msg_box->show(); 162 | } 163 | 164 | void Application::on_sync_button_clicked() 165 | { 166 | if (!std::filesystem::exists(source)) { 167 | show_dialog(QMessageBox::Critical, tr("Error"), tr("The source directory does not exist.")); 168 | return; 169 | } 170 | if (!std::filesystem::exists(dest)) { 171 | show_dialog(QMessageBox::Critical, tr("Error"), tr("The destination directory does not exist.")); 172 | return; 173 | } 174 | if (source == dest) { 175 | show_dialog(QMessageBox::Critical, tr("Error"), tr("The source and destination directories cannot be the same.")); 176 | return; 177 | } 178 | 179 | sync = new Sync(source, dest, log_path, *this, *config, stop_flag); 180 | sync->moveToThread(&thread); 181 | connect(sync, &Sync::finished, this, &Application::sync_finished); 182 | connect(this, &Application::sync_begin, sync, &Sync::begin); 183 | ui->sync_button->setEnabled(false); 184 | ui->stop_button->setEnabled(true); 185 | ui->action_settings->setEnabled(false); 186 | ui->browse_source_button->setEnabled(false); 187 | ui->browse_dest_button->setEnabled(false); 188 | stop_flag.clear(); 189 | quit_flag = false; 190 | ui->console->clear(); 191 | 192 | thread.start(); 193 | emit sync_begin(); 194 | begin = std::chrono::system_clock::now(); 195 | } 196 | 197 | void Application::on_stop_button_clicked() 198 | { 199 | message(tr("Stop requested")); 200 | ui->stop_button->setEnabled(false); 201 | stop_flag.test_and_set(); 202 | } 203 | 204 | void Application::set_sync_button_state() 205 | { 206 | ui->sync_button->setEnabled(!(ui->source_dir_box->text().isEmpty() || ui->dest_dir_box->text().isEmpty())); 207 | } 208 | 209 | void Application::on_action_settings_triggered() 210 | { 211 | auto dialog = new Settings(*config, settings, translator, this); 212 | dialog->setAttribute(Qt::WA_DeleteOnClose, true); 213 | dialog->setModal(true); 214 | dialog->show(); 215 | } 216 | 217 | void Application::on_action_about_triggered() 218 | { 219 | auto dialog = new AboutDialog(*config, *icon, this); 220 | dialog->setAttribute(Qt::WA_DeleteOnClose, true); 221 | dialog->setModal(true); 222 | dialog->show(); 223 | } 224 | 225 | void Application::on_action_quit_triggered() 226 | { 227 | if (sync) { 228 | auto msg_box = new QMessageBox(QMessageBox::Question, 229 | tr("Quit?"), 230 | tr("Are you sure you want to quit while a sync is in-progress?"), 231 | QMessageBox::Yes | QMessageBox::No, 232 | this 233 | ); 234 | connect(msg_box, &QMessageBox::finished, this, &Application::quit_confirm_finished); 235 | connect(this, &Application::close_dialogs, msg_box, &QMessageBox::close); 236 | msg_box->setModal(true); 237 | msg_box->setAttribute(Qt::WA_DeleteOnClose, true); 238 | msg_box->show(); 239 | } 240 | else 241 | quit(); 242 | } 243 | 244 | void Application::quit_confirm_finished(int result) 245 | { 246 | if (result == QMessageBox::Yes) { 247 | if (!stop_flag.test()) 248 | on_stop_button_clicked(); 249 | quit_flag = true; 250 | ui->action_quit->setEnabled(false); 251 | } 252 | } 253 | 254 | void Application::quit() 255 | { 256 | write_settings(); 257 | QApplication::exit(); 258 | } 259 | 260 | void Application::set_progress_bar_max(int max) 261 | { 262 | ui->progress_bar->setMaximum(max); 263 | } 264 | 265 | void Application::set_progress_bar(int val) 266 | { 267 | ui->progress_bar->setValue(val); 268 | } 269 | 270 | void Application::sync_finished(int res) 271 | { 272 | thread.quit(); 273 | thread.wait(); 274 | if (quit_flag) 275 | quit(); 276 | std::chrono::duration duration = std::chrono::floor(std::chrono::system_clock::now() - begin); 277 | ui->sync_button->setEnabled(true); 278 | ui->stop_button->setEnabled(false); 279 | ui->speed_label->clear(); 280 | ui->eta_label->clear(); 281 | ui->action_settings->setEnabled(true); 282 | ui->browse_source_button->setEnabled(true); 283 | ui->browse_dest_button->setEnabled(true); 284 | emit close_dialogs(); 285 | 286 | auto result = static_cast(res); 287 | switch(result) { 288 | case Sync::Result::NOTHING: 289 | message(tr("Nothing to do")); 290 | show_dialog(QMessageBox::Information, PROJECT_NAME, tr("The destination directory is already up-to-date.")); 291 | break; 292 | 293 | case Sync::Result::SUCCESS: 294 | message(tr("Sync completed successfully in %1").arg(QString::fromStdString(fmt::format("{:%H:%M:%S}", duration)))); 295 | show_dialog(QMessageBox::Information, PROJECT_NAME, tr("The sync completed successfully.")); 296 | break; 297 | 298 | case Sync::Result::ERRORS: 299 | message(tr("Sync completed with one or more errors in %1").arg(QString::fromStdString(fmt::format("{:%H:%M:%S}", duration)))); 300 | message(tr("See the log file '%1' for more information").arg(QString::fromStdString(log_path.string()))); 301 | show_dialog(QMessageBox::Critical, tr("Error"), tr("The sync completed with one or more errors.")); 302 | break; 303 | 304 | case Sync::Result::STOPPED: 305 | message(tr("Sync stopped")); 306 | break; 307 | 308 | case Sync::Result::ABORTED: 309 | message(tr("Sync aborted due to error(s)")); 310 | break; 311 | } 312 | 313 | delete sync; 314 | sync = nullptr; 315 | } 316 | 317 | void Application::update_speed_eta(const QString &speed, const QString &eta) 318 | { 319 | ui->speed_label->setText(speed); 320 | ui->eta_label->setText(eta); 321 | } 322 | 323 | void Application::closeEvent(QCloseEvent *event) 324 | { 325 | if (sync) { 326 | if (!quit_flag) 327 | on_action_quit_triggered(); 328 | event->ignore(); 329 | } 330 | else { 331 | write_settings(); 332 | event->accept(); 333 | } 334 | } 335 | 336 | void Application::changeEvent(QEvent *event) 337 | { 338 | if (event->type() == QEvent::LanguageChange) { 339 | ui->retranslateUi(this); 340 | event->accept(); 341 | } 342 | } 343 | 344 | void Application::message(const QString &msg) 345 | { 346 | ui->console->appendPlainText(msg); 347 | ui->console->verticalScrollBar()->setValue(ui->console->verticalScrollBar()->maximum()); 348 | } 349 | 350 | int main(int argc, char *argv[]) 351 | { 352 | #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) 353 | QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); 354 | #endif 355 | #ifndef FFDEBUG 356 | av_log_set_callback(nullptr); 357 | #endif 358 | 359 | QApplication app(argc, argv); 360 | app.setWindowIcon(QIcon(":/icons/" EXECUTABLE_NAME "256.png")); 361 | Application window; 362 | window.setWindowTitle(PROJECT_NAME); 363 | window.show(); 364 | return app.exec(); 365 | } 366 | -------------------------------------------------------------------------------- /src/main.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | class Sync; 20 | struct Config; 21 | class QLineEdit; 22 | class QPixmap; 23 | 24 | namespace Ui { 25 | class MainWindow; 26 | } 27 | 28 | class Application : public QMainWindow 29 | { 30 | Q_OBJECT 31 | 32 | private slots: 33 | void on_browse_source_button_clicked(); 34 | void on_browse_dest_button_clicked(); 35 | void on_sync_button_clicked(); 36 | void on_stop_button_clicked(); 37 | void on_action_settings_triggered(); 38 | void on_action_about_triggered(); 39 | void on_action_quit_triggered(); 40 | 41 | public slots: 42 | void sync_finished(int res); 43 | void quit_confirm_finished(int result); 44 | void set_progress_bar_max(int max); 45 | void set_progress_bar(int val); 46 | void message(const QString &msg); 47 | void update_speed_eta(const QString &speed, const QString &eta); 48 | 49 | signals: 50 | void sync_begin(); 51 | void stop_export(); 52 | void close_dialogs(); 53 | 54 | private: 55 | std::unique_ptr ui; 56 | std::unique_ptr icon; 57 | std::unique_ptr config; 58 | QStyle *style; 59 | QSettings settings; 60 | QTranslator translator; 61 | QThread thread; 62 | Sync *sync = nullptr; 63 | std::filesystem::path log_path; 64 | std::filesystem::path source; 65 | std::filesystem::path dest; 66 | int progress_count = 0; 67 | std::atomic_flag stop_flag; 68 | bool quit_flag = false; 69 | std::chrono::time_point begin; 70 | 71 | void open_directory(std::filesystem::path &path, QLineEdit *box, const QString &title); 72 | void set_sync_button_state(); 73 | void read_settings(); 74 | void write_settings(); 75 | void quit(); 76 | void show_dialog(QMessageBox::Icon icon, const QString &&title, const QString &&message); 77 | 78 | public: 79 | explicit Application(QWidget *parent = nullptr); 80 | ~Application(); 81 | void closeEvent(QCloseEvent *event); 82 | void changeEvent(QEvent *event); 83 | }; 84 | -------------------------------------------------------------------------------- /src/metadata.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "util.hpp" 20 | struct Config; 21 | 22 | using ExtendedTags = std::vector>; 23 | 24 | class Metadata { 25 | public: 26 | struct Tag { 27 | struct ID3v2 { 28 | enum Version { 29 | V3, 30 | v4 31 | }; 32 | TagLib::String frame_ids[2]; 33 | std::vector descs; 34 | }; 35 | 36 | ID3v2 id3v2; 37 | TagLib::String vorbis; 38 | std::vector ape; 39 | std::vector mp4; 40 | }; 41 | struct AlbumArt { 42 | TagLib::ByteVector data; 43 | TagLib::String mime; 44 | 45 | AlbumArt(const TagLib::ByteVector &data, const TagLib::String mime) : data(data), mime(mime) {} 46 | }; 47 | 48 | Metadata(const Config &config); 49 | bool read(const std::filesystem::path &path); 50 | bool write(const std::filesystem::path &path, Codec codec); 51 | std::string get(const std::string &key) const; 52 | inline bool contains(const std::string &key) const { return tag_map.contains(key); } 53 | void convert_r128(); 54 | void strip_replaygain(); 55 | void print(spdlog::logger &logger); 56 | 57 | private: 58 | std::unordered_map tag_map; 59 | std::unique_ptr extended_tags; 60 | std::unique_ptr album_art; 61 | const Config &config; 62 | 63 | std::unique_ptr> track; 64 | std::unique_ptr> disc; 65 | std::unique_ptr date; 66 | 67 | bool read_tags(const TagLib::Ogg::XiphComment *tag, const TagLib::List &pictures); 68 | bool read_tags(const TagLib::ID3v2::Tag *tag); 69 | template 70 | void read_frames(const std::string &key, const TagLib::ID3v2::FrameList &frames); 71 | bool read_tags(TagLib::MP4::Tag *tag); 72 | bool read_tags(TagLib::APE::Tag *tag); 73 | void parse_intpair(const TagLib::Ogg::XiphComment *tag, const TagLib::String &first_key, const TagLib::String &second_key, std::unique_ptr> &ptr); 74 | void parse_intpair(const TagLib::ID3v2::Tag *tag, const TagLib::ByteVector &frame_id, std::unique_ptr> &ptr); 75 | void parse_intpair(const TagLib::MP4::Tag *tag, const TagLib::String &key, std::unique_ptr> &ptr); 76 | void parse_intpair(const TagLib::APE::Tag *tag, const TagLib::String &key, std::unique_ptr> &ptr); 77 | void parse_intpair_sep(const TagLib::ByteVector &value, std::unique_ptr> &ptr); 78 | void parse_date(const TagLib::Ogg::XiphComment *tag, const TagLib::String &key, std::unique_ptr &ptr); 79 | void parse_date(const TagLib::ID3v2::Tag *tag, std::unique_ptr &ptr); 80 | void parse_date(const TagLib::MP4::Tag *tag, const TagLib::String &key, std::unique_ptr &ptr); 81 | void parse_date(const TagLib::APE::Tag *tag, const TagLib::String &key, std::unique_ptr &ptr); 82 | 83 | void parse_iso8601(const TagLib::ByteVector &value, std::unique_ptr &ptr); 84 | 85 | bool write_tags(TagLib::Ogg::XiphComment *tag); 86 | bool write_tags(TagLib::ID3v2::Tag *tag); 87 | bool write_tags(TagLib::MP4::Tag *tag); 88 | void write_intpair(TagLib::Ogg::XiphComment *tag, const TagLib::String &first_key, const TagLib::String &second_key, const std::unique_ptr> &ptr); 89 | void write_intpair(TagLib::ID3v2::Tag *tag, const TagLib::String &key, const std::unique_ptr> &ptr); 90 | void write_intpair(TagLib::MP4::Tag *tag, const TagLib::String &key, const std::unique_ptr> &ptr); 91 | void write_date(TagLib::ID3v2::Tag *tag, std::unique_ptr &ptr); 92 | }; 93 | -------------------------------------------------------------------------------- /src/settings.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "util.hpp" 19 | 20 | #define SETTINGS_VERSION 1 21 | 22 | namespace Ui { 23 | class Settings; 24 | class SettingsGeneral; 25 | class SettingsFileHandling; 26 | class SettingsTranscoding; 27 | class SettingsMP3; 28 | class SettingsAAC; 29 | class SettingsOggVorbis; 30 | class SettingsOpus; 31 | class CleanDestWarningDialog; 32 | } 33 | class SettingsWidgetBase; 34 | class ComboMapBase; 35 | class Config; 36 | 37 | class Settings : public QDialog 38 | { 39 | Q_OBJECT 40 | 41 | public: 42 | explicit Settings(Config &config, QSettings &settings, QTranslator &translator, QWidget *parent = nullptr); 43 | ~Settings() override; 44 | 45 | public slots: 46 | void done(int r) override { 47 | if (r == QDialog::Accepted) 48 | save(); 49 | close(); 50 | } 51 | 52 | private slots: 53 | void on_list_currentRowChanged(int currentRow); 54 | 55 | private: 56 | std::unique_ptr ui; 57 | Config &config; 58 | QSettings &settings; 59 | QTranslator &translator; 60 | 61 | // title, translatable, listitem, widget 62 | std::vector> pages; 63 | 64 | void closeEvent(QCloseEvent *event) override { 65 | close(); 66 | event->accept(); 67 | } 68 | void changeEvent(QEvent *event) override; 69 | void retranslate(); 70 | void save(); 71 | void close(); 72 | }; 73 | 74 | class SettingsWidgetBase : public QWidget 75 | { 76 | public: 77 | SettingsWidgetBase(QWidget *parent) : QWidget(parent) {} 78 | virtual ~SettingsWidgetBase() = default; 79 | virtual void retranslate() = 0; 80 | virtual void load(const Config &config) = 0; 81 | virtual void save(Config &config, QSettings &settings) = 0; 82 | 83 | protected: 84 | std::vector> combo_boxes; 85 | 86 | void populate_comboboxes(const char *tr_context); 87 | void retranslate_comboboxes(const char *tr_context); 88 | inline void save_checkbox(const char *key, bool &setting, QCheckBox *box, QSettings &settings); 89 | inline void save_spinbox(const char *key, int &setting, QSpinBox *box, QSettings &settings); 90 | }; 91 | 92 | class SettingsGeneral : public SettingsWidgetBase 93 | { 94 | Q_OBJECT 95 | 96 | public: 97 | SettingsGeneral(Config &config, QTranslator &translator, QWidget *parent); 98 | ~SettingsGeneral() override; 99 | void retranslate() override; 100 | void load(const Config &config) override; 101 | void save(Config &config, QSettings &settings) override; 102 | 103 | private slots: 104 | void on_skip_existing_box_stateChanged(int state); 105 | void on_clean_dest_box_clicked(bool checked); 106 | void on_language_box_activated(int index); 107 | void clean_dest_warning_finished(int result); 108 | 109 | private: 110 | std::unique_ptr ui; 111 | Config &config; 112 | QTranslator &translator; 113 | }; 114 | 115 | class SettingsFileHandling : public SettingsWidgetBase 116 | { 117 | Q_OBJECT 118 | 119 | public: 120 | SettingsFileHandling(const Config &config, QWidget *parent); 121 | ~SettingsFileHandling() override; 122 | void retranslate() override; 123 | void load(const Config &config) override; 124 | void save(Config &config, QSettings &settings) override; 125 | 126 | 127 | private slots: 128 | void on_flac_copy_button_toggled(bool checked); 129 | void on_alac_copy_button_toggled(bool checked); 130 | void on_wv_copy_button_toggled(bool checked); 131 | void on_ape_copy_button_toggled(bool checked); 132 | void on_wav_copy_button_toggled(bool checked); 133 | void on_mp3_copy_button_toggled(bool checked); 134 | void on_aac_copy_button_toggled(bool checked); 135 | void on_ogg_copy_button_toggled(bool checked); 136 | void on_opus_copy_button_toggled(bool checked); 137 | void on_mpc_copy_button_toggled(bool checked); 138 | 139 | private: 140 | struct GUIFileHandling { 141 | Codec codec; 142 | QString key; 143 | QRadioButton *copy_button; 144 | QRadioButton *transcode_button; 145 | QComboBox *box; 146 | }; 147 | std::unique_ptr ui; 148 | std::vector> transcode_outputs; 149 | std::vector file_handling; 150 | 151 | void set_transcode_box_state(QComboBox *box, bool checked) { box->setEnabled(checked); } 152 | }; 153 | 154 | class SettingsTranscoding : public SettingsWidgetBase 155 | { 156 | Q_OBJECT 157 | 158 | private slots: 159 | void on_cpu_threads_max_button_toggled(bool checked); 160 | void on_copy_metadata_box_stateChanged(int state); 161 | 162 | private: 163 | std::unique_ptr ui; 164 | 165 | public: 166 | SettingsTranscoding(const Config &config, QWidget *parent); 167 | ~SettingsTranscoding() override; 168 | void retranslate() override; 169 | void load(const Config &config) override; 170 | void save(Config &config, QSettings &settings) override; 171 | }; 172 | 173 | class SettingsMP3 : public SettingsWidgetBase 174 | { 175 | Q_OBJECT 176 | 177 | private: 178 | std::unique_ptr ui; 179 | 180 | public: 181 | SettingsMP3(const Config &config, QWidget *parent); 182 | ~SettingsMP3() override; 183 | void retranslate() override; 184 | void load(const Config &config) override; 185 | void save(Config &config, QSettings &settings) override; 186 | }; 187 | 188 | class SettingsAAC : public SettingsWidgetBase 189 | { 190 | Q_OBJECT 191 | 192 | private: 193 | std::unique_ptr ui; 194 | std::vector> encoders; 195 | void set_fdk_aac_state(bool enabled); 196 | void set_lavc_aac_state(bool enabled); 197 | 198 | public: 199 | SettingsAAC(const Config &config, QWidget *parent); 200 | ~SettingsAAC() override; 201 | void retranslate() override; 202 | void load(const Config &config) override; 203 | void save(Config &config, QSettings &settings) override; 204 | 205 | private slots: 206 | void on_aac_encoder_box_currentIndexChanged(int index); 207 | }; 208 | 209 | class SettingsOggVorbis : public SettingsWidgetBase 210 | { 211 | Q_OBJECT 212 | 213 | private: 214 | std::unique_ptr ui; 215 | 216 | public: 217 | SettingsOggVorbis(const Config &config, QWidget *parent); 218 | ~SettingsOggVorbis() override; 219 | void retranslate() override; 220 | void load(const Config &config) override; 221 | void save(Config &config, QSettings &settings) override; 222 | }; 223 | 224 | class SettingsOpus : public SettingsWidgetBase 225 | { 226 | Q_OBJECT 227 | 228 | private: 229 | std::unique_ptr ui; 230 | 231 | private slots: 232 | void on_opus_convert_r128_box_stateChanged(int state); 233 | 234 | public: 235 | SettingsOpus(const Config &config, QWidget *parent); 236 | ~SettingsOpus() override; 237 | void retranslate() override; 238 | void load(const Config &config) override; 239 | void save(Config &config, QSettings &settings) override; 240 | }; 241 | 242 | class CleanDestWarningDialog : public QDialog 243 | { 244 | Q_OBJECT 245 | 246 | private slots: 247 | void on_accept_box_clicked(bool checked); 248 | 249 | private: 250 | std::unique_ptr ui; 251 | 252 | public: 253 | explicit CleanDestWarningDialog(QWidget *parent = nullptr); 254 | ~CleanDestWarningDialog(); 255 | void closeEvent(QCloseEvent *event) override; 256 | }; 257 | -------------------------------------------------------------------------------- /src/sync.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include "basic_lazy_file_sink.hpp" 13 | #include 14 | #include 15 | #include 16 | 17 | #include "sync.hpp" 18 | #include "main.hpp" 19 | #include "transcode.hpp" 20 | #include "config.hpp" 21 | #include 22 | 23 | 24 | Sync::Sync(const std::filesystem::path &source, const std::filesystem::path &dest, const std::filesystem::path &log_path, 25 | Application &app, const Config &config, std::atomic_flag &stop_flag) 26 | : source(source), dest(dest), app(app), config(config), stop_flag(stop_flag) 27 | { 28 | connect(this, &Sync::message, &app, &Application::message); 29 | connect(this, &Sync::set_progress_bar, &app, &Application::set_progress_bar); 30 | connect(this, &Sync::set_progress_bar_max, &app, &Application::set_progress_bar_max); 31 | connect(this, &Sync::update_speed_eta, &app, &Application::update_speed_eta); 32 | if (!log_path.empty()) { 33 | file_sink = std::make_shared(log_path.string(), true); 34 | file_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v"); 35 | logger = std::make_unique("", file_sink); 36 | spdlog::level::level_enum level = config.get_log_level(); 37 | logger->set_level(level); 38 | logger->flush_on(level == spdlog::level::debug ? spdlog::level::debug : spdlog::level::err); 39 | } 40 | } 41 | 42 | void Sync::scan_source() 43 | { 44 | emit message(tr("Scanning source directory...")); 45 | for (const auto &entry : std::filesystem::recursive_directory_iterator(source)) { 46 | if (!entry.is_regular_file()) 47 | continue; 48 | FileType type = filetype_from_path(entry.path()); 49 | if (type == FileType::NONE && !config.copy_nonaudio) 50 | continue; 51 | 52 | const std::filesystem::path &input = entry.path(); 53 | const auto& [action, out_codec] = config.get_file_handling(input); 54 | std::filesystem::path output = dest / std::filesystem::relative(input, source); 55 | if (action == Action::TRANSCODE) 56 | output.replace_extension(config.get_output_ext(out_codec, input)); 57 | if (!config.skip_existing 58 | || !std::filesystem::exists(output) 59 | || (config.sync_newer && std::filesystem::last_write_time(input) > std::filesystem::last_write_time(output))) { 60 | if (action == Action::TRANSCODE) 61 | jobs.emplace(std::make_unique(input, output, out_codec)); 62 | else 63 | files.emplace(input, std::move(output)); 64 | } 65 | } 66 | } 67 | 68 | bool Sync::transcode() 69 | { 70 | std::vector> threads; 71 | std::mutex mutex; 72 | std::mutex ffmutex; 73 | std::unique_lock lock(mutex); 74 | std::condition_variable cond; 75 | avg = std::make_unique(nb_jobs); 76 | emit set_progress_bar_max((int) nb_jobs); 77 | emit set_progress_bar((progress = 0)); 78 | emit message(tr("Transcoding %Ln file(s)...", nullptr, (int) nb_jobs)); 79 | last_update = std::chrono::system_clock::now(); 80 | 81 | if (config.cpu_max_threads) { 82 | nb_threads = std::thread::hardware_concurrency(); 83 | if (!nb_threads) 84 | nb_threads = 1; 85 | } 86 | else 87 | nb_threads = config.cpu_threads; 88 | if (nb_threads > nb_jobs) 89 | nb_threads = nb_jobs; 90 | logger->debug("Transcoding with {} threads", nb_threads); 91 | 92 | // Spawn threads 93 | for (size_t i = 0; i < nb_threads; i++) { 94 | threads.emplace_back(std::make_unique(i, jobs.front(), *this, mutex, cond, ffmutex, *logger, config)); 95 | cond.wait_for(lock, std::chrono::milliseconds(100)); // Wait for the thread to initialize before starting the next 96 | jobs.pop(); 97 | } 98 | 99 | // Feed jobs to the worker threads 100 | while (jobs.size() && !(stop |= stop_flag.test())) { 101 | cond.wait_for(lock, max_sleep_time()); 102 | for (auto &thread : threads) { 103 | if (thread->place_job(jobs.front())) { 104 | jobs.pop(); 105 | break; 106 | } 107 | } 108 | update_transcode_progress(); 109 | } 110 | if (!stop) 111 | cond.wait_for(lock, std::chrono::milliseconds(100)); 112 | 113 | // Wait for threads to finish their jobs 114 | while (1) { 115 | for (auto thread = threads.begin(); thread != threads.end();) 116 | thread = (*thread)->wait() ? threads.erase(thread) : thread + 1; 117 | if (!threads.size()) 118 | break; 119 | cond.wait_for(lock, max_sleep_time()); 120 | update_transcode_progress(); 121 | } 122 | return true; 123 | } 124 | 125 | std::chrono::duration Sync::max_sleep_time() 126 | { 127 | auto diff = std::chrono::round(std::chrono::system_clock::now() - last_update); 128 | auto duration = progress_interval - diff; 129 | return duration > max_sleep || duration < duration.zero() ? max_sleep : duration; 130 | } 131 | 132 | // Called by the main thread periodically throughout the transcoding 133 | void Sync::update_transcode_progress() 134 | { 135 | const auto now = std::chrono::system_clock::now(); 136 | if (std::chrono::duration_cast(now - last_update) < progress_interval) 137 | return; 138 | last_update = now; 139 | if (avg->empty()) 140 | return; 141 | 142 | if (received_update) 143 | eta.update((int) (nb_jobs - jobs_finished), avg->time(), (int) nb_threads); 144 | else 145 | eta -= progress_interval; 146 | received_update = false; 147 | 148 | QString speed_str(QString::fromStdString(fmt::format("{:.1f}x", avg->speed() * static_cast(nb_threads)))); 149 | QString eta_str(eta.get_updates() < nb_threads 150 | ? tr("TBD") 151 | : QString::fromStdString(fmt::format("{:%H:%M:%S}",std::chrono::round(eta.eta())))); 152 | emit update_speed_eta(speed_str, eta_str); 153 | } 154 | 155 | // Called by a worker thread after finishing a transcode while owning the main mutex 156 | void Sync::transcode_finished(double speed, double time) 157 | { 158 | if (avg) 159 | avg->update(speed, time); 160 | jobs_finished++; 161 | received_update = true; 162 | 163 | emit set_progress_bar(++progress); 164 | } 165 | 166 | // Called by a worker thread after a transcode has failed while owning the main mutex 167 | void Sync::report_transcode_error(const std::filesystem::path &path) 168 | { 169 | emit message(tr("Failed to transcode file '%1'").arg(QString::fromStdString(path.string()))); 170 | ret = false; 171 | if (config.abort_on_error) 172 | stop = true; 173 | jobs_finished++; 174 | emit set_progress_bar(++progress); 175 | } 176 | 177 | void Sync::copy_finished(double speed, double time) 178 | { 179 | emit set_progress_bar(++progress); 180 | received_update = true; 181 | avg->update(speed, time); 182 | files_remaining--; 183 | } 184 | 185 | void Sync::report_copy_error(const std::filesystem::path &path) 186 | { 187 | emit message(tr("Failed to copy file '%1'").arg(QString::fromStdString(path.string()))); 188 | ret = false; 189 | if (config.abort_on_error) 190 | stop = true; 191 | emit set_progress_bar(++progress); 192 | files_remaining--; 193 | } 194 | 195 | void Sync::Copier::copy() 196 | { 197 | bool ret = true; 198 | size_t bytes; 199 | std::filesystem::path error_path; 200 | char buffer[BUFSIZ]; 201 | { 202 | std::scoped_lock lock(mutex); // Wait for main thread before starting the copying 203 | } 204 | 205 | while (!files.empty() && (ret || !config.abort_on_error) && !(stop |= stop_flag.test())) { 206 | auto& [in_file, out_file] = files.front(); 207 | const auto begin = std::chrono::system_clock::now(); 208 | bool file_ret = true; 209 | 210 | // Make sure directory exists 211 | std::filesystem::path directory(out_file.parent_path()); 212 | if (!std::filesystem::exists(directory)) { 213 | try { 214 | logger.info("Creating directory '{}'", directory.string()); 215 | std::filesystem::create_directories(directory); 216 | } 217 | catch(...) { 218 | logger.error("Could not create directory '{}'", directory.string()); 219 | file_ret = false; 220 | } 221 | } 222 | if (file_ret) { 223 | std::unique_ptr in(fopen(in_file.string().c_str(), "rb"), fclose); 224 | std::unique_ptr out(fopen(out_file.string().c_str(), "wb"), fclose); 225 | file_ret = in && out; 226 | if (file_ret) { 227 | logger.info("Copying file '{}' to '{}'", in_file.string(), out_file.string()); 228 | while(file_ret && (bytes = fread(buffer, 1, sizeof(buffer), in.get()))) 229 | file_ret = fwrite(buffer, 1, bytes, out.get()) == bytes; 230 | } 231 | } 232 | const auto duration = std::chrono::ceil(std::chrono::system_clock::now() - begin); 233 | double time = static_cast(duration.count()); 234 | double size_mb = 0.0; 235 | try { 236 | size_mb = static_cast(std::filesystem::file_size(in_file)) / 1048576.0; 237 | } 238 | catch(...) {} 239 | double speed = size_mb / (time / 1000000.0); 240 | std::scoped_lock lock(mutex); 241 | if (file_ret) 242 | sync.copy_finished(speed, time); 243 | else { 244 | logger.error("Failed to copy file '{}'", in_file.string()); 245 | sync.report_copy_error(in_file); 246 | } 247 | files.pop(); 248 | if (files.empty()) 249 | finished = true; 250 | ret &= file_ret; 251 | cv.notify_all(); 252 | } 253 | } 254 | 255 | bool Sync::copy_files() 256 | { 257 | bool finished = false; 258 | emit set_progress_bar((progress = 0)); 259 | emit set_progress_bar_max((int) nb_files); 260 | emit message(tr("Copying %Ln file(s)...", nullptr, (int) nb_files)); 261 | avg = std::make_unique(nb_files); 262 | files_remaining = nb_files; 263 | std::mutex mutex; 264 | std::unique_lock lock(mutex); 265 | std::condition_variable cv; 266 | Copier copier(files, *this, finished, mutex, cv, config, *logger, stop_flag); 267 | received_update = false; 268 | last_update = std::chrono::system_clock::now(); 269 | while (!finished && !(stop |= stop_flag.test())) { 270 | cv.wait_for(lock, max_sleep_time()); 271 | update_copy_progress(files_remaining); 272 | } 273 | if (lock.owns_lock()) // Make sure we aren't holding the lock if we quit early 274 | lock.unlock(); 275 | emit set_progress_bar(++progress); 276 | copier.finish(); 277 | return ret; 278 | } 279 | 280 | void Sync::update_copy_progress(size_t files_remaining) 281 | { 282 | const auto now = std::chrono::system_clock::now(); 283 | if (std::chrono::duration_cast(now - last_update) < progress_interval) 284 | return; 285 | last_update = now; 286 | if (avg->empty()) 287 | return; 288 | 289 | if (received_update) 290 | eta.update((int) files_remaining, avg->time() / 1000.0, 1); 291 | else 292 | eta -= progress_interval; 293 | received_update = false; 294 | 295 | QString speed_str(QString::fromStdString(fmt::format("{:.1f} MB/s", avg->speed()))); 296 | QString eta_str(QString::fromStdString(fmt::format("{:%H:%M:%S}", std::chrono::round(eta.eta())))); 297 | emit update_speed_eta(speed_str, eta_str); 298 | } 299 | 300 | // Generate a list of files to delete 301 | void Sync::scan_dest() 302 | { 303 | emit message (tr("Scanning destination directory...")); 304 | 305 | // Generate vectors of all possible input file extensions for a given output extension 306 | // based on user settings 307 | std::vector>> exts; 308 | for (const auto &fh : config.file_handling) { 309 | std::vector *ext_vec = nullptr; 310 | Codec codec = fh.action == Action::COPY ? fh.in_codec : fh.out_codec; 311 | auto it = std::find_if(exts.begin(), exts.end(), [&](const auto &i) { return i.first == codec; }); 312 | if (it == exts.end()) { 313 | exts.emplace_back(std::make_pair>(std::move(codec), {})); 314 | ext_vec = &exts.back().second; 315 | } 316 | else 317 | ext_vec = &(it->second); 318 | 319 | const auto &input_exts = exts_for_codec(fh.in_codec); 320 | for (const auto &ext : input_exts) 321 | ext_vec->push_back(ext); 322 | } 323 | 324 | // Scan destination directory 325 | for (const auto &entry : std::filesystem::recursive_directory_iterator(dest)) { 326 | if (entry.is_directory()) { 327 | directory_list.add(entry.path(), dest); 328 | continue; 329 | } 330 | if (!entry.is_regular_file()) 331 | continue; 332 | const auto &path = entry.path(); 333 | FileType type = filetype_from_path(path); 334 | std::vector codecs; 335 | if (type == FileType::NONE) { 336 | if (!config.copy_nonaudio) 337 | continue; 338 | } 339 | else { 340 | codecs = codecs_from_path(path); 341 | if (codecs.empty()) 342 | continue; 343 | } 344 | bool exists = false; 345 | std::filesystem::path source_path = source / std::filesystem::relative(path, dest); 346 | 347 | // Non-audio, same file exension 348 | if (type == FileType::NONE) 349 | exists = std::filesystem::exists(source_path); 350 | 351 | // Audio, check every possible input file extension 352 | else { 353 | for (Codec codec : codecs) { 354 | auto it = std::find_if(exts.begin(), exts.end(), [&](const auto &i){ return i.first == codec; }); 355 | if (it != exts.end()) { 356 | for (const auto &ext : it->second) { 357 | source_path.replace_extension(ext); 358 | if ((exists = std::filesystem::exists(source_path))) 359 | goto end; 360 | } 361 | } 362 | } 363 | } 364 | end: 365 | if (!exists) 366 | deleted_files.push_back(path); 367 | } 368 | } 369 | 370 | bool Sync::delete_files() 371 | { 372 | bool ret = true; 373 | emit message(tr("Deleting %Ln file(s) in destination directory...", nullptr, (int) deleted_files.size())); 374 | for (auto it = deleted_files.begin(); it != deleted_files.end() && (ret || !config.abort_on_error); ++it) { 375 | logger->info("Deleting destination file '{}'", it->string()); 376 | try { 377 | std::filesystem::remove(*it); 378 | } 379 | catch(...) { 380 | ret = false; 381 | logger->error("Could not delete destination file '{}'", it->string()); 382 | emit message(tr("Could not delete destination file '%1'").arg(it->string().c_str())); 383 | } 384 | } 385 | 386 | if (!directory_list.empty() && !(ret &= directory_list.delete_all(config, *logger))) 387 | emit message(tr("Could not delete one or more empty subdirectories in destination")); 388 | 389 | return this->ret; 390 | } 391 | 392 | Sync::Result Sync::sync() 393 | { 394 | scan_source(); 395 | 396 | nb_jobs = jobs.size(); 397 | nb_files = files.size(); 398 | if (nb_jobs && !(stop |= stop_flag.test())) 399 | ret &= transcode(); 400 | 401 | if (nb_files && (ret || !config.abort_on_error) && !(stop |= stop_flag.test())) 402 | ret &= copy_files(); 403 | 404 | if (config.clean_dest && (ret || !config.abort_on_error) && !(stop |= stop_flag.test())) { 405 | scan_dest(); 406 | nb_delete = deleted_files.size(); 407 | } 408 | if (nb_delete && !(stop |= stop_flag.test())) 409 | ret &= delete_files(); 410 | 411 | if (!nb_jobs && !nb_files && !nb_delete) 412 | return Result::NOTHING; 413 | 414 | if (stop) { 415 | if (!ret && config.abort_on_error) 416 | return Result::ABORTED; 417 | else 418 | return Result::STOPPED; 419 | } 420 | else 421 | return ret ? Result::SUCCESS : Result::ERRORS; 422 | } 423 | 424 | 425 | void Sync::DirectoryList::add(const std::filesystem::path &path, const std::filesystem::path &dest) 426 | { 427 | static constexpr char separator = std::filesystem::path::preferred_separator; 428 | std::filesystem::path relative = std::filesystem::relative(path, dest); 429 | const std::string &s = relative.string(); 430 | int n = static_cast(std::count(s.begin(), s.end(), separator)); 431 | 432 | auto it = dirs.find(n); 433 | if (it == dirs.end()) 434 | dirs.insert({n, {path}}); 435 | else 436 | it->second.push_back(path); 437 | } 438 | 439 | bool Sync::DirectoryList::delete_all(const Config &config, spdlog::logger &logger) 440 | { 441 | bool ret = true; 442 | for (auto it = dirs.rbegin(); it != dirs.rend() && (ret || !config.abort_on_error); ++it) { 443 | const auto &vec = it->second; 444 | for (auto dir = vec.begin(); dir != vec.end() && (ret || !config.abort_on_error); ++dir) { 445 | if (std::filesystem::is_empty(*dir)) { 446 | logger.info("Deleting empty destination directory '{}'", dir->string()); 447 | try { 448 | std::filesystem::remove(*dir); 449 | } 450 | catch(...) { 451 | logger.error("Could not delete destination directory '{}'", dir->string()); 452 | ret = false; 453 | } 454 | } 455 | } 456 | } 457 | 458 | return ret; 459 | } 460 | -------------------------------------------------------------------------------- /src/sync.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include "basic_lazy_file_sink.hpp" 13 | #include 14 | 15 | #include "transcode.hpp" 16 | 17 | #include // this has to come last to workaround Qt MOC bug 18 | 19 | class Application; 20 | struct Config; 21 | 22 | class Sync : public QObject 23 | { 24 | Q_OBJECT 25 | 26 | public: 27 | enum Result { 28 | NOTHING, 29 | SUCCESS, 30 | ERRORS, 31 | STOPPED, 32 | ABORTED 33 | }; 34 | 35 | void transcode_finished(double speed, double time); 36 | void report_transcode_error(const std::filesystem::path &path); 37 | void copy_finished(double speed, double time); 38 | void report_copy_error(const std::filesystem::path &path); 39 | Sync(const std::filesystem::path &source, const std::filesystem::path &dest, 40 | const std::filesystem::path &log_path, Application &app, const Config &config, 41 | std::atomic_flag &stop_flag); 42 | 43 | public slots: 44 | void begin() { emit finished(static_cast(sync())); } 45 | 46 | signals: 47 | void message(const QString &message); 48 | void set_progress_bar_max(int max); 49 | void set_progress_bar(int val); 50 | void update_speed_eta(const QString &speed, const QString &eta); 51 | void finished(int result); 52 | 53 | private: 54 | class DirectoryList 55 | { 56 | public: 57 | void add(const std::filesystem::path &path, const std::filesystem::path &dest); 58 | bool delete_all(const Config &config, spdlog::logger &logger); 59 | inline bool empty() { return dirs.empty(); } 60 | 61 | private: 62 | std::map> dirs; 63 | }; 64 | class Copier { 65 | public: 66 | Copier(std::queue> &files, Sync &sync, 67 | bool &finished, std::mutex &mutex, std::condition_variable &cv, const Config &config, 68 | spdlog::logger &logger, std::atomic_flag &stop_flag) 69 | : files(files), sync(sync), finished(finished), mutex(mutex), cv(cv), config(config), 70 | logger(logger), stop_flag(stop_flag) 71 | { 72 | thread = std::make_unique(&Copier::copy, this); 73 | } 74 | void finish() { thread->join(); } 75 | 76 | private: 77 | std::queue> &files; 78 | Sync &sync; 79 | bool &finished; 80 | std::mutex &mutex; 81 | std::condition_variable &cv; 82 | const Config &config; 83 | spdlog::logger &logger; 84 | std::atomic_flag &stop_flag; 85 | std::unique_ptr thread; 86 | bool stop = false; 87 | void copy(); 88 | }; 89 | 90 | std::filesystem::path source; 91 | std::filesystem::path dest; 92 | Application &app; 93 | const Config &config; 94 | std::atomic_flag &stop_flag; 95 | bool stop = false; 96 | std::shared_ptr file_sink; 97 | std::unique_ptr logger; 98 | std::queue> jobs; 99 | std::queue> files; 100 | std::vector deleted_files; 101 | std::vector> speeds; 102 | ETA eta; 103 | std::unique_ptr avg; 104 | bool received_update = false; 105 | std::chrono::time_point last_update; 106 | const std::chrono::duration progress_interval = std::chrono::milliseconds(1000); 107 | const std::chrono::duration max_sleep = std::chrono::milliseconds(200); 108 | DirectoryList directory_list; 109 | bool ret = true; 110 | int progress = 0; 111 | size_t nb_directories = 0; 112 | size_t nb_jobs = 0; 113 | size_t nb_files = 0; 114 | size_t files_remaining = 0; 115 | size_t nb_delete = 0; 116 | size_t nb_threads; 117 | size_t jobs_finished = 0; 118 | 119 | Result sync(); 120 | std::chrono::duration max_sleep_time(); 121 | void update_transcode_progress(); 122 | void update_copy_progress(size_t files_remaining); 123 | void scan_source(); 124 | bool transcode(); 125 | bool copy_files(); 126 | void scan_dest(); 127 | bool delete_files(); 128 | }; 129 | 130 | -------------------------------------------------------------------------------- /src/transcode.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | extern "C" { 12 | #include "libavformat/avformat.h" 13 | #include "libswresample/swresample.h" 14 | #include "libavcodec/avcodec.h" 15 | #include "libavfilter/avfilter.h" 16 | #include "libavutil/avutil.h" 17 | #include "libavutil/audio_fifo.h" 18 | } 19 | #include 20 | #include 21 | 22 | #include "metadata.hpp" 23 | #include "util.hpp" 24 | 25 | class Sync; 26 | struct Config; 27 | 28 | class Transcoder { 29 | public: 30 | struct Job { 31 | std::filesystem::path input; 32 | std::filesystem::path output; 33 | Codec out_codec; 34 | 35 | Job(const std::filesystem::path &input, std::filesystem::path &output, Codec out_codec) 36 | : input(input), output(std::move(output)), out_codec(out_codec) {} 37 | Job(Job &&o) : input(std::move(o.input)), output(std::move(o.output)), out_codec(o.out_codec) {} 38 | }; 39 | class WorkerThread { 40 | public: 41 | WorkerThread(int id, std::unique_ptr &initial_job, Sync &sync, std::mutex &mutex, 42 | std::condition_variable &cond, std::mutex &ffmutex, spdlog::logger &logger, 43 | const Config &config); 44 | bool place_job(std::unique_ptr &job); 45 | bool wait(); 46 | 47 | private: 48 | int id; 49 | std::unique_ptr job; 50 | Sync &sync; 51 | std::mutex &main_mutex; 52 | std::condition_variable &main_cond; 53 | std::mutex &ffmutex; 54 | spdlog::logger &logger; 55 | const Config &config; 56 | std::unique_ptr thread; 57 | std::mutex mutex; 58 | std::condition_variable cond; 59 | bool quit = false; 60 | bool job_available = true; 61 | 62 | void work(); 63 | }; 64 | 65 | Transcoder(Job &job, std::mutex &mutex, const Config &config, spdlog::logger &logger) 66 | : in_file(job.input, this), out_file(job.output, this), out_codec(job.out_codec), 67 | mutex(mutex), config(config), metadata(config), logger(logger) {} 68 | ~Transcoder(); 69 | bool transcode(); 70 | const std::filesystem::path& in_path() { return in_file.path; } 71 | inline double get_duration() const { return duration; } 72 | template 73 | void ffmpeg_error(int error, fmt::format_string fmts, Args&&... args); 74 | 75 | private: 76 | struct File { 77 | enum Type { 78 | INPUT, 79 | OUTPUT 80 | }; 81 | const std::filesystem::path path; 82 | AVFormatContext *fmt_ctx = nullptr; 83 | AVCodecContext *codec_ctx = nullptr; 84 | const AVCodec *codec = nullptr; 85 | AVStream *stream = nullptr; 86 | Transcoder *transcoder; 87 | 88 | File(std::filesystem::path &path, Transcoder *transcoder) : path(std::move(path)), transcoder(transcoder) {} 89 | ~File(); 90 | void debug_info(Type type, spdlog::logger &logger); 91 | }; 92 | struct InputFile : File { 93 | int stream_id = -1; 94 | 95 | bool open(); 96 | double duration() const; 97 | InputFile(std::filesystem::path &path, Transcoder *transcoder) : File(path, transcoder) {} 98 | }; 99 | struct OutputFile : File { 100 | AVIOContext *io_ctx = nullptr; 101 | 102 | bool open(Codec codec, const InputFile &in_file, const Config &config); 103 | bool write_header(Codec out_codec, const Config &config); 104 | bool write_trailer(); 105 | OutputFile(std::filesystem::path &path, Transcoder *transcoder) : File(path, transcoder) {} 106 | ~OutputFile(); 107 | }; 108 | class VolumeFilter { 109 | public: 110 | bool adjust(AVFrame *frame); 111 | inline double get_volume() const { return volume; } 112 | static VolumeFilter* factory(const AVCodecContext *codec_ctx, const Metadata &metadata, const Config &config); 113 | VolumeFilter(double volume, AVFilterGraph *graph, AVFilterContext *src_ctx, AVFilterContext *sink_ctx) 114 | : volume(volume), graph(graph), src_ctx(src_ctx), sink_ctx(sink_ctx) {} 115 | ~VolumeFilter(); 116 | 117 | private: 118 | double volume = 0.0; 119 | AVFilterGraph *graph; 120 | AVFilterContext *src_ctx; 121 | AVFilterContext *sink_ctx; 122 | 123 | static double parse_replaygain(const Metadata &metadata, const Config &config); 124 | }; 125 | 126 | InputFile in_file; 127 | OutputFile out_file; 128 | Codec out_codec; 129 | std::mutex &mutex; 130 | const Config &config; 131 | Metadata metadata; 132 | spdlog::logger &logger; 133 | int id = 0; 134 | int64_t pts = 0; 135 | double duration = 0.0; 136 | SwrContext *swr_ctx = nullptr; 137 | AVAudioFifo *fifo = nullptr; 138 | std::unique_ptr volume_filter; 139 | bool init_resampler(); 140 | bool init_fifo(); 141 | bool decode_to_fifo(bool &finished); 142 | bool encode_from_fifo(); 143 | bool encode_frame(AVFrame *frame, bool &has_data); 144 | void cleanup(); 145 | }; 146 | -------------------------------------------------------------------------------- /src/ui/about.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AboutDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 439 10 | 330 11 | 12 | 13 | 14 | About 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 0 24 | 0 25 | 26 | 27 | 28 | 29 | 100 30 | 100 31 | 32 | 33 | 34 | 35 | 100 36 | 100 37 | 38 | 39 | 40 | 41 | 42 | 43 | true 44 | 45 | 46 | 8 47 | 48 | 49 | 50 | 51 | 52 | 53 | 0 54 | 55 | 56 | QLayout::SetDefaultConstraint 57 | 58 | 59 | 60 | 61 | Qt::Vertical 62 | 63 | 64 | QSizePolicy::Fixed 65 | 66 | 67 | 68 | 20 69 | 20 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 0 79 | 0 80 | 81 | 82 | 83 | <html><head/><body><p><span style=" font-size:18pt; font-weight:600;">Easy Audio Sync</span></p></body></html> 84 | 85 | 86 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 0 95 | 0 96 | 97 | 98 | 99 | <html><head/><body><p><span style=" font-size:12pt;">Version</span></p></body></html> 100 | 101 | 102 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 103 | 104 | 105 | 106 | 107 | 108 | 109 | Qt::Vertical 110 | 111 | 112 | QSizePolicy::Fixed 113 | 114 | 115 | 116 | 20 117 | 25 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 0 131 | 0 132 | 133 | 134 | 135 | 0 136 | 137 | 138 | false 139 | 140 | 141 | 142 | About 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 152 | 153 | 154 | true 155 | 156 | 157 | true 158 | 159 | 160 | Qt::LinksAccessibleByMouse 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | Library Versions 169 | 170 | 171 | 172 | 173 | 174 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 175 | 176 | 177 | 178 | 179 | Qt: 180 | 181 | 182 | Qt::LinksAccessibleByMouse 183 | 184 | 185 | 186 | 187 | 188 | 189 | libavformat: 190 | 191 | 192 | Qt::LinksAccessibleByMouse 193 | 194 | 195 | 196 | 197 | 198 | 199 | libavcodec: 200 | 201 | 202 | Qt::LinksAccessibleByMouse 203 | 204 | 205 | 206 | 207 | 208 | 209 | libavfilter: 210 | 211 | 212 | Qt::LinksAccessibleByMouse 213 | 214 | 215 | 216 | 217 | 218 | 219 | libswresample: 220 | 221 | 222 | Qt::LinksAccessibleByMouse 223 | 224 | 225 | 226 | 227 | 228 | 229 | libavutil: 230 | 231 | 232 | Qt::LinksAccessibleByMouse 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | Qt::LinksAccessibleByMouse 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | Qt::LinksAccessibleByMouse 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | Qt::LinksAccessibleByMouse 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | Qt::LinksAccessibleByMouse 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | Qt::LinksAccessibleByMouse 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | Qt::LinksAccessibleByMouse 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | Feature Support 303 | 304 | 305 | 306 | 307 | 308 | QFormLayout::FieldsStayAtSizeHint 309 | 310 | 311 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 312 | 313 | 314 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 315 | 316 | 317 | 318 | 319 | LAME: 320 | 321 | 322 | Qt::LinksAccessibleByMouse 323 | 324 | 325 | 326 | 327 | 328 | 329 | Fraunhofer FDK AAC: 330 | 331 | 332 | Qt::LinksAccessibleByMouse 333 | 334 | 335 | 336 | 337 | 338 | 339 | libavcodec AAC: 340 | 341 | 342 | Qt::LinksAccessibleByMouse 343 | 344 | 345 | 346 | 347 | 348 | 349 | libvorbis: 350 | 351 | 352 | Qt::LinksAccessibleByMouse 353 | 354 | 355 | 356 | 357 | 358 | 359 | libopus: 360 | 361 | 362 | Qt::LinksAccessibleByMouse 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | Qt::LinksAccessibleByMouse 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | Qt::LinksAccessibleByMouse 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | Qt::LinksAccessibleByMouse 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | Qt::LinksAccessibleByMouse 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | Qt::LinksAccessibleByMouse 413 | 414 | 415 | 416 | 417 | 418 | 419 | SoX: 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | Build Info 437 | 438 | 439 | 440 | 441 | 442 | QFormLayout::ExpandingFieldsGrow 443 | 444 | 445 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 446 | 447 | 448 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 449 | 450 | 451 | 0 452 | 453 | 454 | 455 | 456 | 457 | 0 458 | 0 459 | 460 | 461 | 462 | Build Date: 463 | 464 | 465 | Qt::LinksAccessibleByMouse 466 | 467 | 468 | 469 | 470 | 471 | 472 | Compiler: 473 | 474 | 475 | Qt::LinksAccessibleByMouse 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 0 484 | 0 485 | 486 | 487 | 488 | 489 | 490 | 491 | Qt::LinksAccessibleByMouse 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 0 500 | 0 501 | 502 | 503 | 504 | Unknown 505 | 506 | 507 | Qt::LinksAccessibleByMouse 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | -------------------------------------------------------------------------------- /src/ui/clean_dest_warning.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | CleanDestWarningDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 347 11 | 12 | 13 | 14 | Warning 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | I understand and wish to continue 28 | 29 | 30 | 31 | 32 | 33 | 34 | Qt::Horizontal 35 | 36 | 37 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | button_box 47 | accepted() 48 | CleanDestWarningDialog 49 | accept() 50 | 51 | 52 | 248 53 | 254 54 | 55 | 56 | 157 57 | 274 58 | 59 | 60 | 61 | 62 | button_box 63 | rejected() 64 | CleanDestWarningDialog 65 | reject() 66 | 67 | 68 | 316 69 | 260 70 | 71 | 72 | 286 73 | 274 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/ui/mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 518 10 | 581 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 12 20 | 21 | 22 | 8 23 | 24 | 25 | 12 26 | 27 | 28 | 8 29 | 30 | 31 | 32 | 33 | QFrame::NoFrame 34 | 35 | 36 | QFrame::Plain 37 | 38 | 39 | 40 | 0 41 | 42 | 43 | 0 44 | 45 | 46 | 3 47 | 48 | 49 | 50 | 51 | 52 | 53 | Source 54 | 55 | 56 | 57 | 58 | 59 | 60 | Destination 61 | 62 | 63 | 64 | 65 | 66 | 67 | true 68 | 69 | 70 | 71 | 72 | 73 | 74 | true 75 | 76 | 77 | 78 | 79 | 80 | 81 | Browse 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | Browse 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 0 107 | 0 108 | 109 | 110 | 111 | 112 | 0 113 | 0 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 28 122 | 28 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 0 132 | 0 133 | 134 | 135 | 136 | 0 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | Sync 146 | 147 | 148 | 149 | 150 | 151 | 152 | true 153 | 154 | 155 | Qt::TextSelectableByMouse 156 | 157 | 158 | false 159 | 160 | 161 | 162 | 163 | 164 | 165 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 166 | 167 | 168 | Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing 169 | 170 | 171 | 172 | 173 | 174 | 0 175 | 0 176 | 177 | 178 | 179 | Time Remaining: 180 | 181 | 182 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 0 191 | 0 192 | 193 | 194 | 195 | Speed: 196 | 197 | 198 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 0 207 | 0 208 | 209 | 210 | 211 | 212 | 213 | 214 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 0 223 | 0 224 | 225 | 226 | 227 | 228 | 229 | 230 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 0 242 | 0 243 | 518 244 | 30 245 | 246 | 247 | 248 | 249 | File 250 | 251 | 252 | 253 | 254 | 255 | 256 | Help 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | Settings 266 | 267 | 268 | 269 | 270 | Quit 271 | 272 | 273 | 274 | 275 | About 276 | 277 | 278 | 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /src/ui/settings.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Settings 4 | 5 | 6 | 7 | 0 8 | 0 9 | 610 10 | 443 11 | 12 | 13 | 14 | Settings 15 | 16 | 17 | 18 | 19 | 20 | QFrame::NoFrame 21 | 22 | 23 | Qt::Horizontal 24 | 25 | 26 | true 27 | 28 | 29 | 1 30 | 31 | 32 | false 33 | 34 | 35 | 36 | 37 | 0 38 | 0 39 | 40 | 41 | 42 | 43 | 44 | 45 | 0 46 | 0 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Qt::Horizontal 56 | 57 | 58 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | buttonBox 68 | accepted() 69 | Settings 70 | accept() 71 | 72 | 73 | 248 74 | 254 75 | 76 | 77 | 157 78 | 274 79 | 80 | 81 | 82 | 83 | buttonBox 84 | rejected() 85 | Settings 86 | reject() 87 | 88 | 89 | 316 90 | 260 91 | 92 | 93 | 286 94 | 274 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/ui/settings_aac.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsAAC 4 | 5 | 6 | 7 | 0 8 | 0 9 | 485 10 | 478 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 28 | 29 | AAC Settings 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Encoder: 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 0 46 | 0 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | QFrame::Box 57 | 58 | 59 | QFrame::Raised 60 | 61 | 62 | 63 | 64 | 65 | Fraunhofer FDK AAC Settings 66 | 67 | 68 | true 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | Encoder Preset: 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 0 85 | 0 86 | 87 | 88 | 89 | -1 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Enable Afterburner 99 | 100 | 101 | true 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | QFrame::Box 115 | 116 | 117 | QFrame::Raised 118 | 119 | 120 | 121 | 122 | 123 | Libavcodec AAC Settings 124 | 125 | 126 | true 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | Encoder Preset: 135 | 136 | 137 | 138 | 139 | 140 | 141 | -1 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | Qt::Vertical 160 | 161 | 162 | 163 | 20 164 | 40 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /src/ui/settings_general.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsGeneral 4 | 5 | 6 | 7 | 0 8 | 0 9 | 408 10 | 402 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | General 33 | 34 | 35 | false 36 | 37 | 38 | false 39 | 40 | 41 | 42 | 43 | 44 | Skip files that exist in destination 45 | 46 | 47 | true 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Qt::Horizontal 57 | 58 | 59 | QSizePolicy::Fixed 60 | 61 | 62 | 63 | 20 64 | 20 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | Except files with older timestamps 73 | 74 | 75 | true 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | Copy non-audio files 85 | 86 | 87 | 88 | 89 | 90 | 91 | Clean destination directory 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 0 103 | 0 104 | 105 | 106 | 107 | Locale 108 | 109 | 110 | false 111 | 112 | 113 | false 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | Language: 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | Debugging 137 | 138 | 139 | false 140 | 141 | 142 | false 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 0 152 | 0 153 | 154 | 155 | 156 | Minimum Log Level: 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 0 165 | 0 166 | 167 | 168 | 169 | Qt::LeftToRight 170 | 171 | 172 | QComboBox::AdjustToContentsOnFirstShow 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | Abort on errors 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | Qt::Vertical 192 | 193 | 194 | 195 | 20 196 | 40 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /src/ui/settings_mp3.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsMP3 4 | 5 | 6 | 7 | 0 8 | 0 9 | 478 10 | 456 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 28 | 29 | MP3 Settings 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Encoder Preset: 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 0 46 | 0 47 | 48 | 49 | 50 | -1 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ID3 Version: 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 0 72 | 0 73 | 74 | 75 | 76 | 2.3 77 | 78 | 79 | true 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 0 88 | 0 89 | 90 | 91 | 92 | 2.4 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | Qt::Vertical 107 | 108 | 109 | 110 | 20 111 | 341 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/ui/settings_ogg_vorbis.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsOggVorbis 4 | 5 | 6 | 7 | 0 8 | 0 9 | 465 10 | 527 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 28 | 29 | Ogg Vorbis Settings 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Encoding Quality: 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 0 46 | 0 47 | 48 | 49 | 50 | -1 51 | 52 | 53 | 10 54 | 55 | 56 | 4 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Qt::Vertical 69 | 70 | 71 | 72 | 20 73 | 40 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/ui/settings_opus.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsOpus 4 | 5 | 6 | 7 | 0 8 | 0 9 | 491 10 | 525 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 28 | 29 | 30 | 0 31 | 0 32 | 33 | 34 | 35 | Opus Settings 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Encoder Preset: 44 | 45 | 46 | 47 | 48 | 49 | 50 | -1 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | File Extension: 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 0 70 | 0 71 | 72 | 73 | 74 | .opus 75 | 76 | 77 | true 78 | 79 | 80 | buttonGroup 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 0 89 | 0 90 | 91 | 92 | 93 | .ogg 94 | 95 | 96 | buttonGroup 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | Convert ReplayGain tags to R128 format 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Qt::Horizontal 115 | 116 | 117 | QSizePolicy::Fixed 118 | 119 | 120 | 121 | 30 122 | 20 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 0 132 | 0 133 | 134 | 135 | 136 | Adjust gain by: 137 | 138 | 139 | 140 | 141 | 142 | 143 | false 144 | 145 | 146 | 147 | 0 148 | 0 149 | 150 | 151 | 152 | -10 153 | 154 | 155 | 10 156 | 157 | 158 | 159 | 160 | 161 | 162 | dB 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | Qt::Vertical 175 | 176 | 177 | 178 | 20 179 | 40 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /src/ui/settings_transcoding.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsTranscoding 4 | 5 | 6 | 7 | 0 8 | 0 9 | 459 10 | 452 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 28 | 29 | Transcoding 30 | 31 | 32 | 33 | 34 | 35 | Copy metadata 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Qt::Horizontal 48 | 49 | 50 | QSizePolicy::Fixed 51 | 52 | 53 | 54 | 20 55 | 20 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Include extended tags 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | Copy cover art 73 | 74 | 75 | true 76 | 77 | 78 | 79 | 80 | 81 | 82 | Downsample high resolution audio 83 | 84 | 85 | true 86 | 87 | 88 | 89 | 90 | 91 | 92 | Downmix multi-channel audio 93 | 94 | 95 | true 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Resampling Engine: 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 0 113 | 0 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | Number of CPU threads: 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 0 134 | 0 135 | 136 | 137 | 138 | Maximum 139 | 140 | 141 | true 142 | 143 | 144 | buttonGroup 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 0 153 | 0 154 | 155 | 156 | 157 | Specific: 158 | 159 | 160 | buttonGroup 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 0 169 | 0 170 | 171 | 172 | 173 | 1 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | ReplayGain: 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | Qt::Vertical 203 | 204 | 205 | 206 | 20 207 | 215 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /src/util.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | extern "C" { 14 | #include 15 | #include 16 | } 17 | #include 18 | 19 | #include "util.hpp" 20 | 21 | FileType filetype_from_path(const std::filesystem::path &path) 22 | { 23 | static const std::unordered_map map { 24 | {".mp3", FileType::MP3}, 25 | {".flac", FileType::FLAC}, 26 | {".ogg", FileType::OGG}, 27 | {".opus", FileType::OPUS}, 28 | {".m4a", FileType::MP4}, 29 | {".m4b", FileType::MP4}, 30 | {".wav", FileType::WAV}, 31 | {".wv", FileType::WAVPACK}, 32 | {".ape", FileType::APE}, 33 | {".mpc", FileType::MPC}, 34 | {".wma", FileType::WMA}, 35 | }; 36 | std::string ext(path.extension().string()); 37 | std::transform(ext.begin(), ext.end(), ext.begin(), tolower); 38 | auto it = map.find(ext); 39 | return it == map.end() ? FileType::NONE : it->second; 40 | } 41 | 42 | // Get the exact codec, opening the file and checking if necessary 43 | Codec codec_from_path(const std::filesystem::path &path) 44 | { 45 | static const std::unordered_map map { 46 | {".mp3", Codec::MP3}, 47 | {".flac", Codec::FLAC}, 48 | {".opus", Codec::OPUS}, 49 | {".wv", Codec::WAVPACK}, 50 | {".ape", Codec::APE}, 51 | {".mpc", Codec::MPC}, 52 | {".wav", Codec::WAV}, 53 | {".wma", Codec::WMA} 54 | }; 55 | if (!path.has_extension()) 56 | return Codec::NONE; 57 | std::string ext(path.extension().string()); 58 | std::transform(ext.begin(), ext.end(), ext.begin(), tolower); 59 | auto it = map.find(ext); 60 | if (it != map.end()) 61 | return it->second; 62 | else { 63 | AVCodecID codec_id = ffcodec_from_file(path); 64 | if (codec_id == AV_CODEC_ID_ALAC) 65 | return Codec::ALAC; 66 | else if (codec_id == AV_CODEC_ID_AAC) 67 | return Codec::AAC; 68 | else if (codec_id == AV_CODEC_ID_VORBIS) 69 | return Codec::VORBIS; 70 | else 71 | return Codec::NONE; 72 | } 73 | } 74 | 75 | // This returns a vector of possible Codecs, so the file is not opened (faster) 76 | std::vector codecs_from_path(const std::filesystem::path &path) 77 | { 78 | static const std::unordered_map> map { 79 | {".mp3", { Codec::MP3 }}, 80 | {".flac", { Codec::FLAC }}, 81 | {".ogg", { Codec::VORBIS, Codec::OPUS }}, 82 | {".opus", { Codec::OPUS}}, 83 | {".m4a", { Codec::ALAC, Codec::AAC }}, 84 | {".m4b", { Codec::ALAC, Codec::AAC }}, 85 | {".wav", { Codec::WAV }}, 86 | {".wv", { Codec::WAVPACK }}, 87 | {".ape", { Codec::APE }}, 88 | {".mpc", { Codec::MPC }}, 89 | {".wma", { Codec::WMA }} 90 | }; 91 | std::string ext(path.extension().string()); 92 | std::transform(ext.begin(), ext.end(), ext.begin(), tolower); 93 | auto it = map.find(ext); 94 | assert(it != map.end()); 95 | return it == map.end() ? std::vector() : it->second; 96 | } 97 | 98 | AVCodecID ffcodec_from_path(const std::filesystem::path &path) 99 | { 100 | static const std::unordered_map map { 101 | {".mp3", AV_CODEC_ID_MP3}, 102 | {".flac", AV_CODEC_ID_FLAC}, 103 | {".opus", AV_CODEC_ID_OPUS}, 104 | {".wv", AV_CODEC_ID_WAVPACK}, 105 | {".ape", AV_CODEC_ID_APE} 106 | }; 107 | if (!path.has_extension()) 108 | return AV_CODEC_ID_NONE; 109 | std::string ext(path.extension().string()); 110 | std::transform(ext.begin(), ext.end(), ext.begin(), tolower); 111 | auto it = map.find(ext); 112 | assert(it != map.end()); 113 | return it != map.end() ? it->second : AV_CODEC_ID_NONE; 114 | } 115 | 116 | AVCodecID ffcodec_from_file(const std::filesystem::path &path) 117 | { 118 | AVFormatContext *fmt_ctx = nullptr; 119 | AVCodecContext *codec_ctx = nullptr; 120 | const AVCodec *codec = nullptr; 121 | AVCodecID codec_id = AV_CODEC_ID_NONE; 122 | int stream_id = -1; 123 | 124 | if (!avformat_open_input(&fmt_ctx, path.string().c_str(), nullptr, nullptr) 125 | && (avformat_find_stream_info(fmt_ctx, nullptr) >= 0) 126 | && ((stream_id = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0)) >= 0) 127 | && ((codec_ctx = avcodec_alloc_context3(codec)) != nullptr) 128 | && (avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[stream_id]->codecpar) >= 0)) 129 | codec_id = codec_ctx->codec_id; 130 | 131 | if(fmt_ctx) 132 | avformat_close_input(&fmt_ctx); 133 | if(codec_ctx) 134 | avcodec_free_context(&codec_ctx); 135 | return codec_id; 136 | } 137 | 138 | const std::vector& exts_for_codec(Codec codec) 139 | { 140 | static const std::vector>> exts { 141 | { Codec::MP3, {".mp3", ".MP3"} }, 142 | { Codec::FLAC, {".flac", ".FLAC"} }, 143 | { Codec::VORBIS, {".ogg", ".OGG"} }, 144 | { Codec::OPUS, {".opus", ".OPUS"} }, 145 | { Codec::AAC, {".m4a", ".M4A", ".m4b", ".M4B"} }, 146 | { Codec::ALAC, {".m4a", ".M4A", ".m4b", ".M4B"} }, 147 | { Codec::WMA, {".wma", ".WMA"} }, 148 | { Codec::WAV, {".wav", ".WAV"} }, 149 | { Codec::WAVPACK, {".wv", ".WV"} }, 150 | { Codec::APE, {".ape", ".APE"} }, 151 | { Codec::MP3, {".ape", ".APE"} }, 152 | { Codec::MPC, {".mpc", ".MPC"}}, 153 | }; 154 | return std::find_if(exts.begin(), exts.end(), [&](const auto &i){return i.first == codec;})->second; 155 | } 156 | 157 | int determine_sample_rate(Codec codec, const AVCodec *avcodec, int in_rate) 158 | { 159 | if (codec == Codec::MP3) 160 | return std::min(48000, in_rate); 161 | if (!avcodec->supported_samplerates) 162 | return in_rate; 163 | 164 | const int *rate = avcodec->supported_samplerates; 165 | while (*(rate + 1)) 166 | rate++; 167 | while (rate > avcodec->supported_samplerates && (*rate - in_rate < 0)) 168 | rate--; 169 | return *rate; 170 | } 171 | 172 | int64_t determine_bitrate(int64_t in_rate, int nb_channels) 173 | { 174 | return nb_channels == 2 ? in_rate : (in_rate * nb_channels) / 2; 175 | } 176 | 177 | void ExpAvg::update(double speed, double time) 178 | { 179 | bool is_empty = empty(); 180 | avg_speed = is_empty ? speed : avg_speed + (speed - avg_speed) / window; 181 | avg_time = is_empty ? time : avg_time + (time - avg_time) / window; 182 | } 183 | 184 | void ETA::update(int jobs_left, double avg_time, int nb_threads) 185 | { 186 | time_remaining = std::chrono::milliseconds(static_cast(static_cast((jobs_left) * avg_time / static_cast(nb_threads)))); 187 | nb_updates++; 188 | } 189 | 190 | void ETA::operator-=(std::chrono::duration duration) 191 | { 192 | time_remaining -= duration; 193 | if (time_remaining < time_remaining.zero()) 194 | time_remaining = time_remaining.zero(); 195 | } 196 | 197 | void save_window_size(QWidget *window, const QString &name, QSettings &settings) 198 | { 199 | if (window->isMaximized()) 200 | return; 201 | settings.beginGroup("WindowSize"); 202 | settings.setValue(name, window->size()); 203 | settings.endGroup(); 204 | } 205 | 206 | bool restore_window_size(QWidget *window, const QString &name, QSettings &settings) 207 | { 208 | bool ret; 209 | settings.beginGroup("WindowSize"); 210 | QSize size = settings.value(name).toSize(); 211 | if ((ret = !size.isEmpty())) 212 | window->resize(size); 213 | settings.endGroup(); 214 | return ret; 215 | } 216 | 217 | void save_window_pos(QWidget *window, const QString &name, QSettings &settings) 218 | { 219 | if (window->isMaximized()) 220 | return; 221 | settings.beginGroup("WindowPos"); 222 | settings.setValue(name, window->pos()); 223 | settings.endGroup(); 224 | } 225 | 226 | bool restore_window_pos(QWidget *window, const QString &name, QSettings &settings) 227 | { 228 | bool ret; 229 | settings.beginGroup("WindowPos"); 230 | QPoint point = settings.value(name).toPoint(); 231 | if (!(ret = point.isNull())) 232 | window->move(point); 233 | settings.endGroup(); 234 | return ret; 235 | } 236 | -------------------------------------------------------------------------------- /src/util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | extern "C" { 13 | #include "libavcodec/avcodec.h" 14 | } 15 | 16 | #define AVG_WINDOW 5.0 17 | 18 | enum class FileType { 19 | NONE = -1, 20 | MP3, 21 | FLAC, 22 | OGG, 23 | OPUS, 24 | MP4, 25 | WMA, 26 | WAV, 27 | WAVPACK, 28 | APE, 29 | MPC 30 | }; 31 | 32 | enum class Codec { 33 | NONE = -1, 34 | MP3, 35 | FLAC, 36 | VORBIS, 37 | OPUS, 38 | AAC, 39 | ALAC, 40 | WMA, 41 | WAV, 42 | WAVPACK, 43 | APE, 44 | MPC 45 | }; 46 | 47 | enum class Action { 48 | COPY, 49 | TRANSCODE 50 | }; 51 | struct FileHandling { 52 | Codec in_codec; 53 | Action action; 54 | Codec out_codec; 55 | }; 56 | 57 | class ExpAvg { 58 | public: 59 | void update(double speed, double time); 60 | bool empty() const { return avg_speed == 0.0; } 61 | double speed() const { return avg_speed; } 62 | double time() const { return avg_time; } 63 | 64 | ExpAvg(int size) : size(size) {} 65 | 66 | private: 67 | double avg_speed = 0.0; 68 | double avg_time = 0.0; 69 | double window = AVG_WINDOW; 70 | int size; 71 | }; 72 | 73 | class ETA { 74 | public: 75 | void update(int jobs_left, double avg_time, int nb_threads); 76 | void operator-=(std::chrono::duration duration); 77 | const std::chrono::milliseconds& eta() const { return time_remaining; } 78 | size_t get_updates() const { return nb_updates; } 79 | 80 | private: 81 | std::chrono::milliseconds time_remaining; 82 | size_t nb_updates = 0; 83 | }; 84 | 85 | int determine_sample_rate(Codec codec, const AVCodec *avcodec, int in_rate); 86 | int64_t determine_bitrate(int64_t in_rate, int nb_channels); 87 | FileType filetype_from_path(const std::filesystem::path &path); 88 | Codec codec_from_path(const std::filesystem::path &path); 89 | std::vector codecs_from_path(const std::filesystem::path &path); 90 | AVCodecID ffcodec_from_file(const std::filesystem::path &path); 91 | std::string get_output_ext(Codec codec, const std::filesystem::path &path); 92 | const std::vector& exts_for_codec(Codec codec); 93 | void save_window_size(QWidget *window, const QString &name, QSettings &settings); 94 | void save_window_pos(QWidget *window, const QString &name, QSettings &settings); 95 | bool restore_window_size(QWidget *window, const QString &name, QSettings &settings); 96 | bool restore_window_pos(QWidget *window, const QString &name, QSettings &settings); 97 | 98 | inline bool join_path([[maybe_unused]] std::filesystem::path &p) 99 | { 100 | return true; 101 | } 102 | 103 | template 104 | inline bool join_path(std::filesystem::path &path, const std::filesystem::path &first, const Args&... args) 105 | { 106 | path /= first; 107 | return join_path(path, args...); 108 | } 109 | 110 | template 111 | inline std::filesystem::path join_paths(const std::filesystem::path &first, const Args&... args) 112 | { 113 | std::filesystem::path path(first); 114 | return join_path(path, args...) ? path : std::filesystem::path(); 115 | } 116 | -------------------------------------------------------------------------------- /translations/README.md: -------------------------------------------------------------------------------- 1 | # Translation 2 | Easy Audio Sync uses the Qt translation system for localization. To translate this application you will need to install Qt Linguist. On Linux, this is typically packaged as `qt-tools` or similar. For Windows users, there are third party providers of standalone Linguist builds, so you don't need to install the entire Qt development suite, which is quite large. See [here](https://github.com/thurask/Qt-Linguist), for example. 3 | 4 | ## Instructions 5 | Determine your language's [ISO-639](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) code. Region extensions are supported. For example, Portuguese is `pt` and Brazillian Portuguese is `pt_BR`. 6 | 7 | ### Qt Linguist 8 | The translation source files have `.ts` file extension. Use the `source.ts` file as a base. Open it in Qt Linguist and save it as a new file with your language's ISO-639 code. Click Edit->Translation File Settings, and set the target language. 9 | 10 | Translate all of the application's strings and mark them with the green "done" checkmark. Strings may have format specifiers such as `%1`, `%2`, etc. This means that the application will substitute values at runtime in the order given. It's important to keep these format specifiers in the translated string, but you may need to change the order based on your language's grammar. 11 | 12 | ### Languages File 13 | After you have translated the strings in Qt Linguist, open the `languages.txt` file in a text editor. Add a line which contains your language's ISO-639 code and the name of the language (in the language itself), delimited by a semicolon without any space between. The languages should be in alphabetical order based on the ISO-639 code. The `languages.txt` file triggers the compilation of the source files and populates the Language combo box in the application settings dialog. 14 | 15 | For example, a `languages.txt` file containing English, Spanish and German should look like this: 16 | ``` 17 | de;Deutsch 18 | en;English 19 | es;Español 20 | ``` 21 | -------------------------------------------------------------------------------- /translations/languages.hpp.in: -------------------------------------------------------------------------------- 1 | std::array, @NB_LANGS@> langs = {{ 2 | @LANG_PAIRS@}}; -------------------------------------------------------------------------------- /translations/languages.txt: -------------------------------------------------------------------------------- 1 | en;English 2 | -------------------------------------------------------------------------------- /translations/translations.qrc.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | @TRANSLATION_FILES@ 4 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "qtbase", 5 | "platform": "!linux", 6 | "default-features": false, 7 | "features": ["gui", "widgets", "png"] 8 | }, 9 | { 10 | "name": "qttools", 11 | "platform": "!linux", 12 | "default-features": false, 13 | "features": ["linguist"] 14 | }, 15 | { 16 | "name": "ffmpeg", 17 | "default-features": false, 18 | "features": ["avcodec", "avformat", "avfilter", "swresample", "mp3lame", "fdk-aac", "vorbis", "opus", "soxr"] 19 | }, 20 | "taglib", 21 | "spdlog" 22 | ] 23 | } 24 | --------------------------------------------------------------------------------