diff --git a/.codespellignore b/.codespellignore index 577a3452b8d8..fc97495e885c 100644 --- a/.codespellignore +++ b/.codespellignore @@ -12,3 +12,4 @@ bufferIn indexIn allLocations allRight +leadIn diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 57bdc60f2f9e..c1e6a0480cb2 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -1,7 +1,7 @@ name: 🐛 Bug Report description: | Describe your problem here. -labels: [bug] +type: "bug" body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 18489e986b0c..2ce3e4bd9aed 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,7 +1,7 @@ name: 🚀 Feature Request description: | What feature would you like to see added to Mixxx? -labels: [feature] +type: "feature" body: - type: markdown attributes: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c14ed8eeffe2..8ec6c9f7a867 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,9 +4,9 @@ updates: directory: "/" schedule: interval: "weekly" - target-branch: "2.6" + target-branch: "2.5" - package-ecosystem: "pre-commit" directory: "/" schedule: interval: "weekly" - target-branch: "2.6" + target-branch: "2.5" diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index dfb8d8a368d9..91520e7570c2 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -126,7 +126,7 @@ jobs: with: # This should always match the minimum required version in # our CMakeLists.txt - cmake-version: "3.21.x" + cmake-version: "3.22.x" - name: "[Windows] Set up cmake" uses: jwlawson/actions-setup-cmake@v2.2 @@ -134,10 +134,9 @@ jobs: # On Windows ARM64 this action installs the x64 version which has unwanted side effects if: runner.os == 'Windows' && runner.arch != 'ARM64' with: - # This is a workaround for a SSL false positive in cmake 3.26.4 - # When downloading the manual. 3.21 is required for installing the - # ANGLE Dlls via IMPORTED_RUNTIME_ARTIFACTS - cmake-version: "3.21.x" + # This is a workaround for a SSL false positive in cmake 3.26.4 when downloading the manual. + # 3.22 is required for Qt 6.10 https://doc.qt.io/qt-6.10/cmake-supported-cmake-versions.html + cmake-version: "3.22.x" - name: "[Windows] Set up MSVC Developer Command Prompt" if: runner.os == 'Windows' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 784da6c4ab2c..e129859b7487 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,6 +31,10 @@ on: required: false MACOS_NOTARIZATION_APP_SPECIFIC_PASSWORD: required: false + ANDROID_SIGNING_KEYSTORE_BASE64: + required: false + ANDROID_SIGNING_PASSWORD: + required: false NETLIFY_BUILD_HOOK: required: false RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY: @@ -113,8 +117,8 @@ jobs: artifacts_name: macOS ARM DMG artifacts_path: build/*.dmg artifacts_slug: macos-macosarm - - name: Windows 2022 x64 - os: windows-2022 + - name: Windows Server 2025 VS2026 x64 + os: windows-2025-vs2026 # Attention: If you change the cmake_args for the Windows CI build, # also adjust the for the local Windows build setup in # ./tools/windows_buildenv.bat @@ -140,8 +144,8 @@ jobs: artifacts_path: build/*.msi artifacts_slug: windows-win64 arch: x64 - - name: Windows 11 ARM64 - os: windows-11-arm + - name: Windows 11 VS2026 ARM64 + os: windows-11-vs2026-arm # Attention: If you change the cmake_args for the Windows CI build, # also adjust the for the local Windows build setup in # ./tools/windows_buildenv.bat @@ -168,11 +172,36 @@ jobs: artifacts_path: build/*.msi artifacts_slug: windows-winarm arch: arm64 + - name: Android 15 arm64 + os: ubuntu-24.04 + # DBUILD_TESTING=OFF: error: OpenMP support and version of OpenMP (31, 40 or 45) differs + cmake_args: >- + -DBULK=ON + -DQT6=ON + -DQML=ON + -DHID=ON + -DVCPKG_TARGET_TRIPLET=arm64-android + -DVCPKG_DEFAULT_HOST_TRIPLET=x64-linux-release + -DCMAKE_SYSTEM_NAME=Android + -DBUILD_TESTING=OFF + -DBUILD_BENCH=OFF + buildenv_basepath: /home/runner/buildenv + buildenv_script: tools/android_buildenv.sh + artifacts_name: Android 15 APK + artifacts_path: build/android-build/build/outputs/apk/release/*.apk + artifacts_slug: android-15 + compiler_cache: ccache + compiler_cache_path: /home/runner/.cache/ccache + crosscompile: true + arch: arm64 env: # macOS codesigning MACOS_CODESIGN_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CODESIGN_CERTIFICATE_P12_BASE64 }} MACOS_CODESIGN_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CODESIGN_CERTIFICATE_PASSWORD }} + # Android signing + ANDROID_SIGNING_KEYSTORE_BASE64: ${{ secrets.ANDROID_SIGNING_KEYSTORE_BASE64 }} + ANDROID_SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }} runs-on: ${{ matrix.os }} name: ${{ matrix.name }} @@ -207,7 +236,7 @@ jobs: with: # This should always match the minimum required version in # our CMakeLists.txt - cmake-version: "3.21.x" + cmake-version: "3.22.x" - name: "[Windows] Set up cmake" uses: jwlawson/actions-setup-cmake@v2.2 @@ -265,6 +294,33 @@ jobs: echo "CMAKE_ARGS_EXTRA=${CMAKE_ARGS_EXTRA} -DAPPLE_CODESIGN_IDENTITY=${APPLE_CODESIGN_IDENTITY}" >> "${GITHUB_ENV}" echo "APPLE_CODESIGN_IDENTITY=${APPLE_CODESIGN_IDENTITY}" >> $GITHUB_ENV + - name: "[android] Setup signing key" + if: startsWith(matrix.artifacts_slug, 'android') + run: | + if [ -z "${ANDROID_SIGNING_KEYSTORE_BASE64}" ]; then + # If no signing key is available (e.g running on a fork), generate a temporary key + keytool \ + -genkey \ + -keystore mixxx.keystore \ + -alias mixxx \ + -keyalg RSA \ + -keysize 2048 \ + -validity 365 \ + -keypass mixxxandroid \ + -storepass mixxxandroid \ + -dname "CN=${{ github.actor }}" + echo "QT_ANDROID_KEYSTORE_ALIAS=mixxx" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_KEY_PASS=mixxxandroid" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_STORE_PASS=mixxxandroid" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_PATH=${{ github.workspace }}/mixxx.keystore" >> $GITHUB_ENV + else + echo "${{ env.ANDROID_SIGNING_KEYSTORE_BASE64 }}" | base64 -d > ${{ github.workspace }}/mixxx.keystore + echo "QT_ANDROID_KEYSTORE_ALIAS=mixxx" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_KEY_PASS=${{ env.ANDROID_SIGNING_PASSWORD }}" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_STORE_PASS=${{ env.ANDROID_SIGNING_PASSWORD }}" >> $GITHUB_ENV + echo "QT_ANDROID_KEYSTORE_PATH=${{ github.workspace }}/mixxx.keystore" >> $GITHUB_ENV + fi + - name: "[macOS/Linux] Set up build environment" if: matrix.buildenv_script != null && runner.os != 'Windows' run: ${{ matrix.buildenv_script }} setup @@ -298,6 +354,29 @@ jobs: ${{ matrix.compiler_cache }} --max-size=2G if: runner.os != 'windows' + # Remove unused pre-installed software as the runner runs out of space otherwise + # Currently freeing up about 17.7G, ~20% + - name: "[android] Free up disk space" + if: startsWith(matrix.artifacts_slug, 'android') + run: | + sudo apt-get autoremove -y && sudo apt-get clean + sudo rm -rf /home/packer # Free up 677M + sudo rm -rf /opt/az # Free up 649M + sudo rm -rf /opt/google # Free up 378M + sudo rm -rf /opt/hostedtoolcache/CodeQL # Free up 1.6G + sudo rm -rf /opt/hostedtoolcache/go # Free up 808M + sudo rm -rf /opt/hostedtoolcache/node # Free up 532M + sudo rm -rf /opt/hostedtoolcache/PyPy # Free up 520M + sudo rm -rf /opt/hostedtoolcache/Python # Free up 1.5G + sudo rm -rf /opt/microsoft # Free up 781M + sudo rm -rf /opt/pipx # Free up 499M + sudo rm -rf /usr/lib/google-cloud-sdk # Free up 957M + sudo rm -rf /usr/local/julia1.11.7 # Free up 996M + sudo rm -rf /usr/local/share/powershell # Free up 1.3G + sudo rm -rf /usr/share/dotnet # Free up 3.4G + sudo rm -rf /usr/share/swift # Free up 3.2G + sudo rm -rf /usr/local/share/vcpkg # Size unknown, but obvious duplicate + - name: "Create build directory" run: mkdir build @@ -425,7 +504,7 @@ jobs: path: ${{ github.workspace }}/build/_CPack_Packages/win64/WIX/wix.log - name: "[Ubuntu] Import PPA GPG key" - if: startsWith(matrix.os, 'ubuntu') && env.RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY != null + if: startsWith(matrix.os, 'ubuntu') && matrix.crosscompile != true && env.RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY != null run: gpg --import <(echo "${{ secrets.RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY }}") env: RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY: ${{ secrets.RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY }} @@ -497,6 +576,15 @@ jobs: --dest-url 'https://downloads.mixxx.org' ${{ matrix.artifacts_path }} + # TODO create a F-droid repo? + # - name: fdroid nightly + # run: | + # sudo add-apt-repository ppa:fdroid/fdroidserver + # sudo apt-get update + # sudo apt-get install apksigner fdroidserver --no-install-recommends + # export DEBUG_KEYSTORE=$ + # fdroid nightly --archive-older 10 + # Warning: do not move this step before restoring caches or it will break caching due to # https://github.com/actions/cache/issues/531 - name: "[Windows] Install rsync and openssh" @@ -550,9 +638,9 @@ jobs: ssh-keyscan "${SSH_HOST}" >> "${HOME}/.ssh/known_hosts" echo "SSH_AUTH_SOCK=${SSH_AUTH_SOCK}" >> "${GITHUB_ENV}" - - name: "[macOS/Windows] Upload build to downloads.mixxx.org" + - name: "[Android/macOS/Windows] Upload build to downloads.mixxx.org" # skip deploying Ubuntu builds to downloads.mixxx.org because these are deployed to the PPA - if: runner.os != 'Linux' && inputs.publish && env.SSH_AUTH_SOCK != null + if: startsWith(matrix.artifacts_slug, 'ubuntu') != true && inputs.publish && env.SSH_AUTH_SOCK != null shell: bash --login -eo pipefail "{0}" run: rsync --verbose --recursive --checksum --times --delay-updates "deploy/" "${SSH_USER}@${SSH_HOST}:${DESTDIR}/" env: diff --git a/.github/workflows/download_cleanup.yml b/.github/workflows/download_cleanup.yml index 5e79705e9e01..03f56f9ad407 100644 --- a/.github/workflows/download_cleanup.yml +++ b/.github/workflows/download_cleanup.yml @@ -22,76 +22,36 @@ jobs: echo "SSH_AUTH_SOCK=${SSH_AUTH_SOCK}" >> "${GITHUB_ENV}" - name: Delete obsolete files - if: env.SSH_AUTH_SOCK != null + # if: env.SSH_AUTH_SOCK != null + if: false run: | mkdir empty_folder echo 2.5.1 >> include_file.txt echo 2.5.1/manifest.json >> include_file.txt - echo 2.5.1/mixxx-2.6-alpha-174-g2c2dda9781-win64* >> include_file.txt - echo AzureCodeSigning >> include_file.txt - echo AzureCodeSigning/manifest.json >> include_file.txt - echo AzureCodeSigning/mixxx-2.6-alpha-* >> include_file.txt - echo CAStreamBasicDescription >> include_file.txt - echo CAStreamBasicDescription/manifest.json >> include_file.txt - echo CAStreamBasicDescription/mixxx-2.4.1-46-* >> include_file.txt - echo PR_13709 >> include_file.txt - echo PR_13709/manifest.json >> include_file.txt - echo PR_13709/mixxx-2.4.1-81-* >> include_file.txt - echo azure_signing_update >> include_file.txt - echo azure_signing_update/manifest.json >> include_file.txt - echo azure_signing_update/mixxx-2.4.1-61-* >> include_file.txt - echo chore >> include_file.txt - echo chore/upgrade-macos13-xcode15.2 >> include_file.txt - echo chore/upgrade-macos13-xcode15.2/manifest.json >> include_file.txt - echo chore/upgrade-macos13-xcode15.2/mixxx-2.6-alpha-76-* >> include_file.txt - echo daschuer-patch-1 >> include_file.txt - echo daschuer-patch-1/manifest.json >> include_file.txt - echo daschuer-patch-1/mixxx-2.6-alpha-284-* >> include_file.txt - echo dependabot >> include_file.txt - echo dependabot/github_actions >> include_file.txt - echo dependabot/github_actions/actions >> include_file.txt - echo dependabot/github_actions/actions/stale-6 >> include_file.txt - echo dependabot/github_actions/actions/stale-6/manifest.json >> include_file.txt - echo dependabot/github_actions/actions/stale-6/mixxx-2.4-alpha-1318-* >> include_file.txt - echo dependabot/github_actions/actions/upload-artifact-3.1.2 >> include_file.txt - echo dependabot/github_actions/actions/upload-artifact-3.1.2/manifest.json >> include_file.txt - echo dependabot/github_actions/actions/upload-artifact-3.1.2/mixxx-2.3.3-117-* >> include_file.txt - echo fix-14326 >> include_file.txt - echo fix-14326/manifest.json >> include_file.txt - echo fix-14326/mixxx-2.5.0-68-* >> include_file.txt - echo inpulse >> include_file.txt - echo inpulse/manifest.json >> include_file.txt - echo inpulse/mixxx-2.* >> include_file.txt - echo pr >> include_file.txt - echo pr/13709 >> include_file.txt - echo pr/13709/manifest.json >> include_file.txt - echo pr/13709/mixxx-2.4.1-81-* >> include_file.txt - echo resolve-from-urls-2.5 >> include_file.txt - echo resolve-from-urls-2.5/manifest.json >> include_file.txt - echo resolve-from-urls-2.5/mixxx-2.5-beta-100-* >> include_file.txt - echo revert-13208-gh13206 >> include_file.txt - echo revert-13208-gh13206/mixxx-2.4.1-5-gc71a48b76e-* >> include_file.txt - echo revert-13271-revert-13208-gh13206 >> include_file.txt - echo revert-13271-revert-13208-gh13206/mixxx-2.4.1-6-* >> include_file.txt - echo rg-use-opengl-node-and-add-shaders >> include_file.txt - echo rg-use-opengl-node-and-add-shaders/manifest.json >> include_file.txt - echo rg-use-opengl-node-and-add-shaders/mixxx-2.6-alpha-* >> include_file.txt - echo traktor-s3-updates >> include_file.txt - echo traktor-s3-updates/manifest.json >> include_file.txt - echo traktor-s3-updates/mixxx-2.6-alpha-* >> include_file.txt - echo ts_source_copy_check >> include_file.txt - echo ts_source_copy_check/manifest.json >> include_file.txt - echo ts_source_copy_check/mixxx-2.4.1-42-* >> include_file.txt - echo tsan-fix-13893 >> include_file.txt - echo tsan-fix-13893/manifest.json >> include_file.txt - echo tsan-fix-13893/mixxx-2.5-beta-83-* >> include_file.txt - echo tsan-fix-13895 >> include_file.txt - echo tsan-fix-13895/manifest.json >> include_file.txt - echo tsan-fix-13895/mixxx-2.5-beta-83-* >> include_file.txt - echo waveformwidgetinfo >> include_file.txt - echo waveformwidgetinfo/mixxx-2.5-alpha-3* >> include_file.txt rsync --verbose --archive --times --recursive --delete --include-from=include_file.txt --exclude=* "empty_folder/" "${SSH_USER}@${SSH_HOST}:${DESTDIR}/snapshots/" env: DESTDIR: public_html/downloads SSH_HOST: downloads-hostgator.mixxx.org SSH_USER: mixxx + + - name: Remove folders + if: env.SSH_AUTH_SOCK != null + run: | + ssh "${SSH_USER}@${SSH_HOST}" " + set -euo pipefail + rm -rf \"${DESTDIR}/releases/2.5.4-3-g8d7a9c728a\" + rm -rf \"${DESTDIR}/releases/2.5.5\" + rm -rf \"${DESTDIR}/releases/2.7-alpha-215-g1666d06877\" + rm -rf \"${DESTDIR}/snapshots/borrowable_ptr\" + rm -rf \"${DESTDIR}/snapshots/ci\" + rm -f \"${DESTDIR}/snapshots/2.5/mixxx-2.5.5-macosarm.dmg\" + rm -f \"${DESTDIR}/snapshots/2.5/mixxx-2.5.5-macosarm.dmg.sha256sum\" + rm -f \"${DESTDIR}/snapshots/2.5/mixxx-2.5.5-macosintel.dmg\" + rm -f \"${DESTDIR}/snapshots/2.5/mixxx-2.5.5-macosintel.dmg.sha256sum\" + rm -f \"${DESTDIR}/snapshots/2.5/mixxx-2.5.5-win64.msi\" + rm -f \"${DESTDIR}/snapshots/2.5/mixxx-2.5.5-win64.msi.sha256sum\" + " + env: + DESTDIR: public_html/downloads + SSH_HOST: downloads-hostgator.mixxx.org + SSH_USER: mixxx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31866026bd22..6526def48ea7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,6 +41,8 @@ jobs: MACOS_CODESIGN_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CODESIGN_CERTIFICATE_P12_BASE64 }} MACOS_CODESIGN_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CODESIGN_CERTIFICATE_PASSWORD }} MACOS_NOTARIZATION_APP_SPECIFIC_PASSWORD: ${{ secrets.MACOS_NOTARIZATION_APP_SPECIFIC_PASSWORD }} + ANDROID_SIGNING_KEYSTORE_BASE64: ${{ secrets.ANDROID_SIGNING_KEYSTORE_BASE64 }} + ANDROID_SIGNING_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }} NETLIFY_BUILD_HOOK: ${{ secrets.NETLIFY_BUILD_HOOK }} RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY: ${{ secrets.RRYAN_AT_MIXXX_DOT_ORG_GPG_PRIVATE_KEY }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2cdaa380e83c..dfad2b89f4b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -125,10 +125,6 @@ repos: hooks: - id: prettier files: \.(d\.ts|ya?ml)$ - - repo: https://github.com/qarmin/qml_formatter.git - rev: 37c2513b1b8275a475a160ed2f5b044910335d5f # No release tag yet including #6 fix - hooks: - - id: qml_formatter - repo: https://github.com/BlankSpruce/gersemi rev: 0.17.1 hooks: @@ -160,6 +156,17 @@ repos: language: python types: [text] files: ^CHANGELOG.md$ + - id: qmlformat + name: qmlformat + entry: qmlformat + pass_filenames: true + require_serial: true + language: system + types: [text] + files: ^.*\.qml$ + args: [-n, -i] + stages: + - manual - id: qmllint name: qmllint entry: qmllint @@ -168,6 +175,8 @@ repos: language: system types: [text] files: ^.*\.qml$ + stages: + - manual - id: metainfo name: metainfo description: Update AppStream metainfo releases from CHANGELOG.md. diff --git a/.qmlformat.ini b/.qmlformat.ini new file mode 100644 index 000000000000..c103c2005ad1 --- /dev/null +++ b/.qmlformat.ini @@ -0,0 +1,5 @@ +[General] +IndentWidth=4 +NewlineType=native +NormalizeOrder=true +UseTabs=false diff --git a/.tx/config b/.tx/config index 2fec46c1f484..7b5c8801a000 100644 --- a/.tx/config +++ b/.tx/config @@ -1,7 +1,7 @@ [main] host = https://www.transifex.com -[o:mixxx-dj-software:p:mixxxdj:r:mixxx2-6] +[o:mixxx-dj-software:p:mixxxdj:r:mixxx2-7] file_filter = res/translations/mixxx_.ts source_file = res/translations/mixxx.ts source_lang = en diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..59b5f31c693a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,58 @@ +# AGENTS.md — Mixxx Project Instructions + +See [README.md](README.md) for a project overview, and +[CONTRIBUTING.md](CONTRIBUTING.md) for build instructions, code style, +pre-commit setup, Git workflow, and pull request guidelines. + +## AI Agent Policy + +> **Important:** The Mixxx project only accepts contributions driven by human developers. +> Automated AI agents acting autonomously on a user's behalf are not welcome and may result +> in pull requests being closed without comment. +> But using AI tools that **assist** the human contributor with code generation, refactoring, +> documentation and reviewing the work before submission is welcome. + +### No Autonomous Pull Requests + +AI agents **must not** open, update, or re-open pull requests autonomously on behalf of a user. +All pull requests must be explicitly reviewed, approved, and submitted by the human developer +themselves. An agent may prepare and stage changes locally, but `git commit`, `git push`, and PR +creation must be deliberate human actions. Before this, a test of the code change by a human, +using a real DJ setup, is required. + +### No Automated Responses to PR Review Comments + +AI agents **must not** post replies to review comments, questions, or change requests left on +a pull request or its commits. All communication in the PR thread must come directly from the human contributor. + +### AI Code-Reviews + +AI agents **may** and **should** perform code reviews on explicitly request in scope of the PR. + +### No Autonomous Creation of Issues, Bug Reports or Feature Requests + +AI agents **must not** open, update, or re-open Issues, Bug Reports or Feature Requests autonomously +or on behalf of a user. +All Issues, Bug Reports, and Feature Requests must be explicitly reviewed, approved, and submitted by +the human user themselves. + +### AI-Generated Text Must Carry a Disclaimer + +Any text (PR description, commit message body, code comments, documentation) that was written +autonomously by an AI Agent **must** be framed with an disclaimer that the text is autonomously generated +by the AI Agent, at both the start and the end of that text block. + +## Key Architecture + +- **ControlObject/ControlProxy**: `[Group], key_name` inter-component communication. +- **Engine thread**: Real-time audio — no allocations, no locks, may emit Qt signals but cannot receive them. +- **parented_ptr/make_parented**: Qt object-tree ownership. Object must get a parent before `parented_ptr` destructs. + +## Project Layout + +```text +src/ C++ source (engine/, controllers/, library/, mixer/, effects/, qml/, preferences/, util/, test/) +res/ Resources (controllers/ JS/XML, skins/, qml/) +cmake/ CMake modules +tools/ Python helper scripts +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 633759c7f8b5..47d2287343ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [2.7.0](https://github.com/mixxxdj/mixxx/milestone/47) (Unreleased) + ## [2.6.0](https://github.com/mixxxdj/mixxx/milestone/44) (Unreleased) ### STEM file support diff --git a/CMakeLists.txt b/CMakeLists.txt index 200d3b821ca6..0d81e2ff824d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,5 @@ -cmake_minimum_required(VERSION 3.21) +# Qt 6.10 requires minimum CMake 3.22: https://doc.qt.io/qt-6.10/cmake-supported-cmake-versions.html +cmake_minimum_required(VERSION 3.22) # lint_cmake: -readability/wonkycase message(STATUS "CMAKE_VERSION: ${CMAKE_VERSION}") @@ -36,6 +37,11 @@ if(POLICY CMP0099) cmake_policy(SET CMP0099 NEW) endif() +# Add support for cmake_dependent_option +if(POLICY CMP0127) + cmake_policy(SET CMP0127 NEW) +endif() + # An imported target missing its location property fails during generation. if(POLICY CMP0111) cmake_policy(SET CMP0111 NEW) @@ -48,7 +54,54 @@ if(POLICY CMP0135) cmake_policy(SET CMP0135 NEW) endif() -if(((APPLE AND NOT IOS) OR WIN32) AND NOT IS_DIRECTORY "${MIXXX_VCPKG_ROOT}") +if(CMAKE_SYSTEM_NAME STREQUAL Android) + if(NOT DEFINED ENV{JAVA_HOME}) + message(FATAL_ERROR "JAVA_HOME is not set. Did you source the setup file?") + endif() + if((NOT CMAKE_ANDROID_NDK) AND DEFINED ENV{ANDROID_NDK_HOME}) + set(CMAKE_ANDROID_NDK "$ENV{ANDROID_NDK_HOME}") + endif() + set(ANDROID ON) + if(DEFINED ENV{QT_ANDROID_KEYSTORE_PATH}) + set(QT_ANDROID_SIGN_APK ON) + endif() + + set(QT_ANDROID_APP_PATH "$") + set( + QT_ANDROID_APP_PACKAGE_SOURCE_ROOT + "${CMAKE_SOURCE_DIR}/packaging/android" + ) + + if((NOT ANDROID_SDK_ROOT) AND DEFINED ENV{ANDROID_SDK}) + set(ANDROID_SDK_ROOT "$ENV{ANDROID_SDK}") + endif() + set(ANDROID_ABI arm64-v8a) + set(ANDROID_NDK_HOST_SYSTEM_NAME linux-x86_64) + set(CMAKE_SYSTEM_VERSION 35) # API level + set(ANDROID_PLATFORM "android-${CMAKE_SYSTEM_VERSION}") + set(ANDROID_API_VERSION "android-${CMAKE_SYSTEM_VERSION}") + set(CMAKE_ANDROID_ARCH_ABI "${ANDROID_ABI}") # or x86_64, armeabi-v7a, etc. + set(CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION clang) + set(CMAKE_ANDROID_STL_TYPE c++_shared) + set( + CMAKE_SYSROOT + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/sysroot" + ) + include_directories( + BEFORE + SYSTEM + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/sysroot/usr/include/" + ) + set( + CMAKE_LIBRARY_PATH + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/sysroot/usr/lib/aarch64-linux-android/${CMAKE_SYSTEM_VERSION}/;${CMAKE_LIBRARY_PATH}" + ) +endif() + +if( + ((APPLE AND NOT IOS) OR WIN32 OR ANDROID) + AND NOT IS_DIRECTORY "${MIXXX_VCPKG_ROOT}" +) if(NOT DEFINED BUILDENV_BASEPATH) if(DEFINED ENV{BUILDENV_BASEPATH}) set(BUILDENV_BASEPATH "$ENV{BUILDENV_BASEPATH}") @@ -172,7 +225,7 @@ function(fatal_error_missing_env) "Did you download the Mixxx build environment using `source ${CMAKE_SOURCE_DIR}/tools/macos_release_buildenv.sh setup` or `source ${CMAKE_SOURCE_DIR}/tools/macos_buildenv.sh setup` (includes Debug)?" ) endif() - elseif(UNIX AND NOT APPLE) + elseif(UNIX AND NOT APPLE AND NOT ANDROID) # Linux, BSD, Solaris, Minix if(EXISTS "/etc/debian_version") # exists also on Ubuntu and Mint message( @@ -315,8 +368,8 @@ set( # Set a default build type if none was specified # See https://blog.kitware.com/cmake-and-the-default-build-type/ for details. set(default_build_type "RelWithDebInfo") -if(EXISTS "${CMAKE_SOURCE_DIR}/.git" AND NOT WIN32) - # On Windows, Debug builds are linked to unoptimized libs +if(EXISTS "${CMAKE_SOURCE_DIR}/.git" AND NOT WIN32 AND NOT ANDROID) + # On Windows and Android, Debug builds are linked to unoptimized libs # generating unusable slow Mixxx builds. set(default_build_type "Debug") endif() @@ -350,13 +403,13 @@ endif() include(CMakeDependentOption) option(QT6 "Build with Qt6" ON) - -# Because of multiple concurrent definition of symbols caused by the rendergraph -# compile definition we need to disable QML by default. This avoids the risk of -# undefined behaviour in a stable build. -# See: https://github.com/mixxxdj/mixxx/issues/14766 -# Once this is fixed we can revert the commit introducing this. -option(QML "Build with QML" OFF) +cmake_dependent_option( + QML + "Build with QML" + ON + "QT6" + OFF +) if(QT6) set(QOPENGL ON CACHE INTERNAL "Forced ON because QT6=ON") @@ -466,9 +519,9 @@ elseif(APPLE) endif() endif() -project(mixxx VERSION 2.6.0 LANGUAGES C CXX) +project(mixxx VERSION 2.7.0 LANGUAGES C CXX) # Work around missing version suffixes support https://gitlab.kitware.com/cmake/cmake/-/issues/16716 -set(MIXXX_VERSION_PRERELEASE "beta") # set to "alpha" "beta" or "" +set(MIXXX_VERSION_PRERELEASE "alpha") # set to "alpha" "beta" or "" set(CMAKE_PROJECT_HOMEPAGE_URL "https://www.mixxx.org") set( @@ -982,7 +1035,7 @@ else() else() message(STATUS "Could NOT find ccache (missing executable)") endif() - default_option(CCACHE_SUPPORT "Enable ccache support" "CCACHE_EXECUTABLE") + default_option(CCACHE_SUPPORT "Enable ccache support" "CCACHE_EXECUTABLE;NOT ANDROID") if(NOT DEFINED CMAKE_DISABLE_PRECOMPILE_HEADERS) set(CMAKE_DISABLE_PRECOMPILE_HEADERS ${CCACHE_SUPPORT}) @@ -1049,7 +1102,7 @@ if(NOT MSVC) set(MOLD_SYMLINK_FOUND TRUE) endif() default_option(MOLD_SUPPORT "Use 'mold' for linking" "MOLD_FUSE_FOUND OR MOLD_SYMLINK_FOUND") - if(MOLD_SUPPORT) + if(MOLD_SUPPORT AND NOT ANDROID) if(MOLD_FUSE_FOUND) message(STATUS "Selecting mold as linker") add_link_options("-fuse-ld=mold") @@ -1220,14 +1273,18 @@ add_library( src/effects/backends/builtin/echoeffect.cpp src/effects/backends/builtin/filtereffect.cpp src/effects/backends/builtin/flangereffect.cpp + src/effects/backends/builtin/gaineffect.cpp src/effects/backends/builtin/glitcheffect.cpp src/effects/backends/builtin/graphiceqeffect.cpp src/effects/backends/builtin/linkwitzriley8eqeffect.cpp src/effects/backends/builtin/loudnesscontoureffect.cpp src/effects/backends/builtin/metronomeeffect.cpp src/effects/backends/builtin/metronomeclick.cpp + src/effects/backends/builtin/keycomparisoneffect.cpp + src/effects/backends/builtin/pianosample.cpp src/effects/backends/builtin/moogladder4filtereffect.cpp src/effects/backends/builtin/compressoreffect.cpp + src/effects/backends/builtin/autogaincontroleffect.cpp src/effects/backends/builtin/parametriceqeffect.cpp src/effects/backends/builtin/phasereffect.cpp src/effects/backends/builtin/reverbeffect.cpp @@ -1528,6 +1585,8 @@ add_library( src/skin/legacy/tooltips.cpp src/skin/skincontrols.cpp src/skin/skinloader.cpp + src/soundio/portaudioenumerator.cpp + src/soundio/sounddeviceenumerator.cpp src/soundio/sounddevice.cpp src/soundio/sounddevicenetwork.cpp src/soundio/sounddeviceportaudio.cpp @@ -1684,6 +1743,7 @@ add_library( src/waveform/widgets/glvsynctestwidget.cpp src/widget/controlwidgetconnection.cpp src/widget/findonwebmenufactory.cpp + src/widget/findonweblast.cpp src/widget/findonwebmenuservices/findonwebmenudiscogs.cpp src/widget/findonwebmenuservices/findonwebmenulastfm.cpp src/widget/findonwebmenuservices/findonwebmenusoundcloud.cpp @@ -1694,6 +1754,7 @@ add_library( src/widget/wbasewidget.cpp src/widget/wbattery.cpp src/widget/wbeatspinbox.cpp + src/widget/wbpmeditor.cpp src/widget/wcollapsiblegroupbox.cpp src/widget/wcolorpicker.cpp src/widget/wcolorpickeraction.cpp @@ -1766,6 +1827,8 @@ add_library( src/widget/wwidget.cpp src/widget/wwidgetgroup.cpp src/widget/wwidgetstack.cpp + src/controllers/scripting/javascriptplayerproxy.cpp + src/controllers/scripting/javascriptplayerproxy.h ) set(MIXXX_COMMON_PRECOMPILED_HEADER src/util/assert.h) set( @@ -2160,13 +2223,17 @@ if(RELATIVE_MACRO_PATHS) endif() option(WARNINGS_FATAL "Fail if compiler generates a warning" OFF) -if(WARNINGS_FATAL) - if(MSVC) - target_compile_options(mixxx-lib PUBLIC /WX) - else() - target_compile_options(mixxx-lib PUBLIC -Werror) +macro(mixxx_target_warnings_fatal target scope) + if(WARNINGS_FATAL) + if(MSVC) + target_compile_options(${target} ${scope} /WX) + else() + target_compile_options(${target} ${scope} -Werror) + endif() endif() -endif() +endmacro() + +mixxx_target_warnings_fatal(mixxx-lib PUBLIC) target_compile_definitions( mixxx-lib @@ -2281,6 +2348,8 @@ if(WIN32) elseif(UNIX) if(APPLE) target_compile_definitions(mixxx-lib PUBLIC __APPLE__) + elseif(ANDROID) + target_compile_definitions(mixxx-lib PUBLIC __ANDROID__) else() target_compile_definitions(mixxx-lib PUBLIC __UNIX__) if(CMAKE_SYSTEM_NAME STREQUAL Linux) @@ -2302,7 +2371,92 @@ if(QT6) # below that takes care of the correct object order in the resulting binary # According to https://doc.qt.io/qt-6/qt-finalize-target.html it is importand for # builds with Qt < 3.21 - qt_add_executable(mixxx WIN32 src/main.cpp MANUAL_FINALIZATION) + if(ANDROID) + target_compile_definitions( + mixxx-lib + PUBLIC __STDC_CONSTANT_MACROS __STDC_LIMIT_MACROS __STDC_FORMAT_MACROS + ) + set_property(TARGET mixxx-lib PROPERTY ANDROID_ABIS "arm64-v8a") + target_compile_definitions( + mixxx-lib + PUBLIC ANDROID_PACKAGE_NAME="org.mixxx" + ) + qt_add_executable(mixxx src/main.cpp MANUAL_FINALIZATION) + set_property( + TARGET mixxx + PROPERTY + QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_SOURCE_DIR}/packaging/android" + ) + set_target_properties(mixxx PROPERTIES QT_ANDROID_ABIS "${ANDROID_ABI}") + set_target_properties(mixxx PROPERTIES QT_ANDROID_PACKAGE_NAME "org.mixxx") + set_target_properties( + mixxx + PROPERTIES QT_ANDROID_VERSION_NAME "${MIXXX_VERSION}" + ) + set_target_properties( + mixxx + PROPERTIES + QT_ANDROID_APPLICATION_ARGUMENTS "--qml --log-level debug --developer" + ) + qt_add_android_permission(mixxx + NAME android.permission.ACCESS_NETWORK_STATE + ) + qt_add_android_permission(mixxx + NAME android.permission.INTERNET + ) + qt_add_android_permission(mixxx + NAME android.permission.MODIFY_AUDIO_SETTINGS + ) + qt_add_android_permission(mixxx + NAME android.permission.RECORD_AUDIO + ) + qt_add_android_permission(mixxx + NAME android.permission.WRITE_EXTERNAL_STORAGE + ) + qt_add_android_permission(mixxx + NAME android.permission.MANAGE_EXTERNAL_STORAGE + ) + qt_add_android_permission(mixxx + NAME android.permission.WAKE_LOCK + ) + qt_add_android_permission(mixxx + NAME android.permission.USB_PERMISSION + ) + set( + CMAKE_LINKER + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/bin/ld" + ) + set( + CMAKE_C_COMPILER + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/bin/clang" + ) + set( + CMAKE_CXX_COMPILER + "${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/bin/clang++" + ) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -fPIC") + set_target_properties( + mixxx + PROPERTIES + QT_ANDROID_EXTRA_LIBS + ${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_NDK_HOST_SYSTEM_NAME}/lib/clang/18/lib/linux/aarch64/libomp.so + ) + add_custom_target( + copy-resource-android + COMMENT "Copy resources folder to Android build asset directory" + COMMAND + ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/packaging/android + ${CMAKE_CURRENT_BINARY_DIR}/android + COMMAND + ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/res + ${CMAKE_CURRENT_BINARY_DIR}/android-build/assets + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + ) + add_dependencies(mixxx copy-resource-android) + target_link_libraries(mixxx PUBLIC omp) + else() + qt_add_executable(mixxx WIN32 src/main.cpp MANUAL_FINALIZATION) + endif() else() find_package(Qt5 COMPONENTS Core) # For Qt Core cmake functions # This is the first package form the environment, if this fails give hints how to install the environment @@ -2391,37 +2545,41 @@ if(WIN32) include(InstallRequiredSystemLibraries) endif() -install( - TARGETS mixxx - RUNTIME DESTINATION "${MIXXX_INSTALL_BINDIR}" - BUNDLE DESTINATION . -) +if(NOT ANDROID) + install( + TARGETS mixxx + RUNTIME DESTINATION "${MIXXX_INSTALL_BINDIR}" + BUNDLE DESTINATION . + ) -# Skins -install( - DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/skins" - DESTINATION "${MIXXX_INSTALL_DATADIR}" -) + # Skins + install( + DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/skins" + DESTINATION "${MIXXX_INSTALL_DATADIR}" + ) -# Controller mappings -install( - DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/controllers" - DESTINATION "${MIXXX_INSTALL_DATADIR}" -) + # Controller mappings + install( + DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/controllers" + DESTINATION "${MIXXX_INSTALL_DATADIR}" + ) -# Effect presets -install( - DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/effects" - DESTINATION "${MIXXX_INSTALL_DATADIR}" -) + # Effect presets + install( + DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/effects" + DESTINATION "${MIXXX_INSTALL_DATADIR}" + ) -# Translation files -install( - DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/translations" - DESTINATION "${MIXXX_INSTALL_DATADIR}" - FILES_MATCHING - PATTERN "*.qm" -) + # Translation files + install( + DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/translations" + DESTINATION "${MIXXX_INSTALL_DATADIR}" + FILES_MATCHING + PATTERN "*.qm" + ) + # else() + # qt_finalize_target(mixxx-lib) +endif() # Font files # @@ -2592,6 +2750,7 @@ if(BUILD_TESTING) else() message(FATAL_ERROR "GTest: not found") endif() + add_compile_definitions(BUILD_TESTING) endif() find_package(benchmark) @@ -2608,11 +2767,11 @@ endif() # FFmpeg support # FFmpeg is multimedia library that can be found http://ffmpeg.org/ -find_package(FFMPEG COMPONENTS libavcodec libavformat libavutil libswresample) -default_option(FFMPEG "FFmpeg support (version 4.1.9 or later)" "FFMPEG_FOUND") +find_package(FFmpeg COMPONENTS AVCODEC AVFORMAT AVUTIL SWRESAMPLE) +default_option(FFMPEG "FFmpeg support (version 4.1.9 or later)" "FFmpeg_FOUND") if(FFMPEG) - if(NOT FFMPEG_FOUND) - message(FATAL_ERROR "FFMPEG was not found") + if(NOT FFmpeg_FOUND) + message(FATAL_ERROR "FFmpeg was not found") endif() # Check minimum required versions @@ -2620,39 +2779,56 @@ if(FFMPEG) # Windows: Version numbers are not available!? # macOS: Untested if( - FFMPEG_libavcodec_VERSION - AND FFMPEG_libavcodec_VERSION VERSION_LESS 58.35.100 + NOT FFmpeg_AVCODEC_VERSION + OR FFmpeg_AVCODEC_VERSION VERSION_LESS 58.35.100 ) message( FATAL_ERROR - "FFmpeg support requires at least version 58.35.100 of libavcodec (found: ${FFMPEG_libavcodec_VERSION})." + "FFmpeg support requires at least version 58.35.100 of libavcodec (found: ${FFmpeg_AVCODEC_VERSION})." + ) + else() + message( + STATUS + "Found FFmpeg AVCODEC: ${FFmpeg_AVCODEC_LIBRARIES} (found version \"${FFmpeg_AVCODEC_VERSION}\")" ) endif() if( - FFMPEG_libavformat_VERSION - AND FFMPEG_libavformat_VERSION VERSION_LESS 58.20.100 + NOT FFmpeg_AVFORMAT_VERSION + OR FFmpeg_AVFORMAT_VERSION VERSION_LESS 58.20.100 ) message( FATAL_ERROR - "FFmpeg support requires at least version 58.20.100 of libavformat (found: ${FFMPEG_libavformat_VERSION})." + "FFmpeg support requires at least version 58.20.100 of libavformat (found: ${FFmpeg_AVFORMAT_VERSION})." + ) + else() + message( + STATUS + "Found FFmpeg AVFORMAT: ${FFmpeg_AVFORMAT_LIBRARIES} (found version \"${FFmpeg_AVFORMAT_VERSION}\")" ) endif() - if( - FFMPEG_libavutil_VERSION - AND FFMPEG_libavutil_VERSION VERSION_LESS 56.22.100 - ) + if(NOT FFmpeg_AVUTIL_VERSION OR FFmpeg_AVUTIL_VERSION VERSION_LESS 56.22.100) message( FATAL_ERROR - "FFmpeg support requires at least version 56.22.100 of libavutil (found: ${FFMPEG_libavutil_VERSION})." + "FFmpeg support requires at least version 56.22.100 of libavutil (found: ${FFmpeg_AVUTIL_VERSION})." + ) + else() + message( + STATUS + "Found FFmpeg AVUTIL: ${FFmpeg_AVUTIL_LIBRARIES} (found version \"${FFmpeg_AVUTIL_VERSION}\")" ) endif() if( - FFMPEG_libswresample_VERSION - AND FFMPEG_libswresample_VERSION VERSION_LESS 3.3.100 + NOT FFmpeg_SWRESAMPLE_VERSION + OR FFmpeg_SWRESAMPLE_VERSION VERSION_LESS 3.3.100 ) message( FATAL_ERROR - "FFmpeg support requires at least version 3.3.100 of libswresample (found: ${FFMPEG_libswresample_VERSION})." + "FFmpeg support requires at least version 3.3.100 of libswresample (found: ${FFmpeg_SWRESAMPLE_VERSION})." + ) + else() + message( + STATUS + "Found FFmpeg SWRESAMPLE: ${FFmpeg_SWRESAMPLE_LIBRARIES} (found version \"${FFmpeg_SWRESAMPLE_VERSION}\")" ) endif() @@ -2666,15 +2842,16 @@ if(FFMPEG) __STDC_LIMIT_MACROS __STDC_FORMAT_MACROS ) - target_link_libraries(mixxx-lib PRIVATE "${FFMPEG_LIBRARIES}") - target_include_directories(mixxx-lib PUBLIC "${FFMPEG_INCLUDE_DIRS}") + + target_link_libraries(mixxx-lib PRIVATE FFmpeg::FFmpeg) + target_include_directories(mixxx-lib PUBLIC "${FFmpeg_INCLUDE_DIRS}") endif() # STEM file support -default_option(STEM "STEM file support" "FFMPEG_FOUND;FFMPEG") +default_option(STEM "STEM file support" "FFmpeg_FOUND;FFMPEG") if(STEM) if(NOT FFMPEG) - message(FATAL_ERROR "STEM requires that also FFMPEG is enabled") + message(FATAL_ERROR "STEM requires that also FFmpeg is enabled") endif() target_compile_definitions(mixxx-lib PUBLIC __STEM__) list(APPEND MIXXX_LIB_PRECOMPILED_HEADER src/track/steminfo.h) @@ -2762,6 +2939,7 @@ if(BUILD_TESTING) src/test/looping_control_test.cpp src/test/main.cpp src/test/mathutiltest.cpp + src/test/matrixapitest.cpp src/test/metadatatest.cpp #TODO: make this build again #src/test/metaknob_link_test.cpp @@ -2783,6 +2961,7 @@ if(BUILD_TESTING) src/test/rgbcolor_test.cpp src/test/rotary_test.cpp src/test/samplebuffertest.cpp + src/test/savitzkygolaytest.cpp src/test/schemamanager_test.cpp src/test/searchqueryparsertest.cpp src/test/seratobeatgridtest.cpp @@ -2841,19 +3020,34 @@ if(BUILD_TESTING) PUBLIC RESOURCE_FOLDER="${CMAKE_CURRENT_SOURCE_DIR}/res" ) + if(HID) + target_sources( + mixxx-test + PRIVATE src/test/controller_hid_reportdescriptor_test.cpp + ) + endif() + if(QML) target_sources( mixxx-test PRIVATE src/test/controller_mapping_file_handler_test.cpp src/test/controllerrenderingengine_test.cpp + src/test/qmlcontrolproxytest.cpp + src/test/qmlkeyutilstest.cpp + src/test/qmlskincontrolcreatortest.cpp + src/test/themeqml_test.cpp ) endif() + if(AU_EFFECTS) + target_sources(mixxx-test PRIVATE src/test/audiounitmanager_test.mm) + endif() + set_target_properties(mixxx-test PROPERTIES AUTOMOC ON) target_link_libraries( mixxx-test - PRIVATE mixxx-lib mixxx-gitinfostore GTest::gtest GTest::gmock + PRIVATE mixxx-lib mixxx-gitinfostore mixxx-xwax GTest::gtest GTest::gmock ) if(BUILD_BENCH) @@ -3055,38 +3249,53 @@ endif() # For LOCALECOMPARE we call directly sqlite functions to the database opened by # Qt. It only works without crashing when Mixxx links to the same sqlite -# library as Qt. -# This is difficult on macOS where system the SQLite can be installed and linked -# dynamically from various locations. There is no issue in case of static -# linking which would result in a duplicate symbol error. -if(NOT SQLite3_FOUND) - set(LOCALECOMPARE_DEFAULT OFF) -else() - is_static_library(SQLite3_IS_STATIC SQLite::SQLite3) - if(SQLite3_IS_STATIC OR NOT APPLE) - set(LOCALECOMPARE_DEFAULT ON) - else() - set(LOCALECOMPARE_DEFAULT OFF) - endif() -endif() +# library as Qt. On vcpkg builds, the custom FindSQLite3.cmake wrapper ensures +# that both Qt and Mixxx resolve to the same vcpkg-provided SQLite library. cmake_dependent_option( LOCALECOMPARE "Locale Aware Compare support for SQLite" ON - "LOCALECOMPARE_DEFAULT" + "SQLite3_FOUND" OFF ) if(LOCALECOMPARE) - if(NOT SQLite3_FOUND) - message( - FATAL_ERROR - "Locale Aware Compare for SQLite requires libsqlite and its development headers." + target_compile_definitions(mixxx-lib PUBLIC __SQLITE3__) + if( + NOT VCPKG_TARGET_TRIPLET MATCHES "-release$" + AND TARGET unofficial::sqlite3::sqlite3 + ) + get_target_property( + _sqlite3_location + unofficial::sqlite3::sqlite3 + IMPORTED_IMPLIB_RELEASE ) + if(NOT _sqlite3_location) + get_target_property( + _sqlite3_location + unofficial::sqlite3::sqlite3 + IMPORTED_IMPLIB + ) + endif() + if(_sqlite3_location) + message(STATUS "Forcing link to SQLite3 library: ${_sqlite3_location}") + target_link_libraries(mixxx-lib PRIVATE "${_sqlite3_location}") + else() + # Fallback: try to find sqlite3.lib manually + message( + WARNING + "Could not find SQLite3 import library, looking for sqlite3.lib" + ) + find_library(SQLITE3_LIB sqlite3) + if(SQLITE3_LIB) + target_link_libraries(mixxx-lib PRIVATE ${SQLITE3_LIB}) + endif() + endif() + else() + target_link_libraries(mixxx-lib PRIVATE SQLite3::SQLite3) endif() - target_compile_definitions(mixxx-lib PUBLIC __SQLITE3__) - target_link_libraries(mixxx-lib PRIVATE SQLite3::SQLite3) -elseif(SQLite3_IS_STATIC) - # in the static case we need to link SQLite3 uncoditionally +elseif(SQLite3_FOUND) + # When LOCALECOMPARE is OFF (manually set) but SQLite3 is found (static Qt builds), + # we still need to link SQLite3 unconditionally target_link_libraries(mixxx-lib PRIVATE SQLite3::SQLite3) endif() @@ -3107,9 +3316,7 @@ if(ENGINEPRIME) message(STATUS "STATIC link existing system installation of libdjinterop") target_link_libraries(mixxx-lib PUBLIC DjInterop::DjInterop) else() - # On MacOS, Mixxx does not use system SQLite, so we will use libdjinterop's - # embedded SQLite in such a case. - if(APPLE AND NOT SQLite3_IS_STATIC) + if(NOT SQLite3_FOUND) message( STATUS "Building libdjinterop sources (with embedded SQLite) fetched from GitHub" @@ -3179,10 +3386,12 @@ if(ENGINEPRIME) INSTALL_DIR ${DJINTEROP_INSTALL_DIR} LIST_SEPARATOR "|" CMAKE_ARGS - -DBUILD_SHARED_LIBS=OFF -DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=ON + -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON + -DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=ON -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_PREFIX_PATH=${PIPE_DELIMITED_CMAKE_PREFIX_PATH} + -DCMAKE_FIND_ROOT_PATH=${PIPE_DELIMITED_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_LIBDIR:PATH=lib -DCMAKE_MODULE_PATH:PATH=${CMAKE_MODULE_PATH} -$,D,U>CMAKE_TOOLCHAIN_FILE:PATH=${CMAKE_TOOLCHAIN_FILE} @@ -3487,8 +3696,8 @@ else() set(CMAKE_FIND_FRAMEWORK FIRST) endif() set(OpenGL_GL_PREFERENCE "GLVND") - find_package(OpenGL REQUIRED) if(EMSCRIPTEN) + find_package(OpenGL REQUIRED) # Emscripten's FindOpenGL.cmake does not create OpenGL::GL target_link_libraries(mixxx-lib PRIVATE ${OPENGL_gl_LIBRARY}) target_compile_definitions(mixxx-lib PUBLIC QT_OPENGL_ES_2) @@ -3499,8 +3708,17 @@ else() mixxx-lib PUBLIC -sMIN_WEBGL_VERSION=2 -sMAX_WEBGL_VERSION=2 -sFULL_ES2=1 ) + elseif(ANDROID) + find_library(GLESv2_LIBRARY GLESv2) + target_link_libraries(mixxx-lib PRIVATE "${GLESv2_LIBRARY}") + target_compile_definitions(mixxx-lib PUBLIC QT_OPENGL_ES_2) else() - target_link_libraries(mixxx-lib PRIVATE OpenGL::GL) + find_package(WrapOpenGL REQUIRED) + if(OPENGL_opengl_LIBRARY) + target_link_libraries(mixxx-lib PRIVATE OpenGL::OpenGL) + else() + target_link_libraries(mixxx-lib PRIVATE OpenGL::GL) + endif() endif() if(UNIX AND QGLES2) target_compile_definitions(mixxx-lib PUBLIC QT_OPENGL_ES_2) @@ -3526,6 +3744,11 @@ target_link_libraries( find_package(PortAudio REQUIRED) target_link_libraries(mixxx-lib PUBLIC PortAudio::PortAudio) +if(ANDROID) + target_link_libraries(mixxx-lib PUBLIC OpenSLES android) + target_compile_definitions(mixxx-lib PUBLIC PA_USE_OBOE) +endif() + # PortAudio Ring Buffer add_library( PortAudioRingBuffer @@ -3536,8 +3759,22 @@ add_library( target_include_directories(mixxx-lib SYSTEM PUBLIC lib/portaudio) target_link_libraries(mixxx-lib PRIVATE PortAudioRingBuffer) +# PipeWire +find_package(PipeWire) +default_option(PIPEWIRE "Enable the PipeWire backend" "PipeWire_FOUND") +if(PIPEWIRE) + target_link_libraries(mixxx-lib PUBLIC PipeWire::PipeWire) + target_compile_definitions(mixxx-lib PUBLIC __PIPEWIRE__) + target_sources( + mixxx-lib + PRIVATE + src/soundio/pipewireenumerator.cpp + src/soundio/sounddevicepipewire.cpp + ) +endif() + # PortMidi -option(PORTMIDI "Enable the PortMidi backend for MIDI controllers" ON) +default_option(PORTMIDI "Enable the PortMidi backend for MIDI controllers" "NOT ANDROID") if(PORTMIDI) target_compile_definitions(mixxx-lib PUBLIC __PORTMIDI__) find_package(PortMidi REQUIRED) @@ -3594,10 +3831,12 @@ else() find_package(QT 5.12 NAMES Qt5 COMPONENTS Core REQUIRED) endif() if(QML) - list(APPEND QT_EXTRA_COMPONENTS "Quick") list(APPEND QT_EXTRA_COMPONENTS "LabsQmlModels") + list(APPEND QT_EXTRA_COMPONENTS "Multimedia") + list(APPEND QT_EXTRA_COMPONENTS "Quick") list(APPEND QT_EXTRA_COMPONENTS "QuickControls2") list(APPEND QT_EXTRA_COMPONENTS "QuickControls2Impl") + list(APPEND QT_EXTRA_COMPONENTS "QuickDialogs2") list(APPEND QT_EXTRA_COMPONENTS "QuickLayouts") list(APPEND QT_EXTRA_COMPONENTS "QuickShapesPrivate") list(APPEND QT_EXTRA_COMPONENTS "QuickTemplates2") @@ -3629,21 +3868,41 @@ if(QT_EXTRA_COMPONENTS) endforeach() endif() +if(QT_KNOWN_POLICY_QTP0002 AND ANDROID) + qt6_policy(SET QTP0002 NEW) +endif() + if(QML) + target_sources(mixxx-lib PRIVATE src/skin/qml/qmlskin.cpp) + if(QT_KNOWN_POLICY_QTP0004) # See: https://doc.qt.io/qt-6/qt-cmake-policy-qtp0004.html - # OLD (Qt < 6.8) requires to import qml modules with a folder e.g.: + # OLD requires to import qml modules with a folder e.g.: # fragmentShader: "qrc:/shaders/rgbsignal_qml.frag.qsb" - # For using NEW, we need to back-port: + # We keep OLD for Qt < 6.8 because using NEW would require a backport: # https://github.com/qt/qtdeclarative/commit/6314d305ee0d9064ca848980ef2dab1793c191b8 - # until Qt 6.8 lands in all supported distros - qt6_policy(SET QTP0004 OLD) + # For Qt >= 6.8, we use NEW to match current Qt behavior and suppress the deprecation warning. + if(QT_VERSION VERSION_GREATER_EQUAL 6.8) + qt6_policy(SET QTP0004 NEW) + else() + qt6_policy(SET QTP0004 OLD) + endif() endif() add_subdirectory(res/shaders) set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml) qt_add_library(mixxx-qml-lib STATIC) + mixxx_target_warnings_fatal(mixxx-qml-lib PUBLIC) + + if(WIN32) + target_compile_definitions(mixxx-qml-lib PUBLIC __WINDOWS__) + endif() + + if(ENGINEPRIME) + target_compile_definitions(mixxx-qml-lib PUBLIC __ENGINEPRIME__) + endif() + foreach(component ${QT_COMPONENTS}) target_link_libraries( mixxx-qml-lib @@ -3659,6 +3918,14 @@ if(QML) endforeach() endif() set_target_properties(mixxx-qml-lib PROPERTIES AUTOMOC ON) + set_source_files_properties( + res/qml/Mixxx/MathUtils.mjs + PROPERTIES QT_RESOURCE_ALIAS MathUtils.mjs + ) + set_source_files_properties( + res/qml/Mixxx/PlayerDropArea.qml + PROPERTIES QT_RESOURCE_ALIAS PlayerDropArea.qml + ) qt_add_qml_module(mixxx-qml-lib URI Mixxx VERSION 1.0 @@ -3699,11 +3966,13 @@ if(QML) RESOURCE_PREFIX /mixxx.org/imports OPTIONAL_IMPORTS Mixxx QML_FILES - res/qml/Mixxx/Controls/Knob.qml res/qml/Mixxx/Controls/Slider.qml + res/qml/Mixxx/Controls/Knob.qml + res/qml/Mixxx/Controls/Fader.qml res/qml/Mixxx/Controls/Spinny.qml res/qml/Mixxx/Controls/WaveformOverviewHotcueMarker.qml res/qml/Mixxx/Controls/WaveformOverviewMarker.qml + res/qml/Mixxx/Controls/WaveformOverviewMarkerLayer.qml res/qml/Mixxx/Controls/WaveformOverview.qml res/qml/Mixxx/Controls/WaveformDisplay.qml ) @@ -3715,24 +3984,39 @@ if(QML) src/qml/asyncimageprovider.cpp src/qml/qmlapplication.cpp src/qml/qmlautoreload.cpp + src/qml/qmlbatteryproxy.cpp src/qml/qmlbeatsmodel.cpp - src/qml/qmlcuesmodel.cpp - src/qml/qmlcontrolproxy.cpp + src/qml/qmlchainpresetmodel.cpp src/qml/qmlconfigproxy.cpp + src/qml/qmlconfigproxybase.cpp + src/qml/qmlcontrolproxy.cpp + src/qml/qmlcuesmodel.cpp src/qml/qmldlgpreferencesproxy.cpp src/qml/qmleffectmanifestparametersmodel.cpp - src/qml/qmleffectsmanagerproxy.cpp src/qml/qmleffectslotproxy.cpp + src/qml/qmleffectsmanagerproxy.cpp + src/qml/qmlkeyutils.cpp + src/qml/qmllegacylibraryitem.cpp src/qml/qmllibraryproxy.cpp + src/qml/qmllibrarysource.cpp + src/qml/qmllibrarysourcetree.cpp src/qml/qmllibrarytracklistmodel.cpp + src/qml/qmlmixxxcontrollerscreen.cpp src/qml/qmlplayermanagerproxy.cpp src/qml/qmlplayerproxy.cpp + src/qml/qmlrecordingproxy.cpp + src/qml/qmlsidebarmodelproxy.cpp + src/qml/qmllibrarytracklistcolumn.cpp + src/qml/qmltrackproxy.cpp src/qml/qmlvisibleeffectsmodel.cpp - src/qml/qmlchainpresetmodel.cpp - src/qml/qmlwaveformoverview.cpp - src/qml/qmlmixxxcontrollerscreen.cpp src/qml/qmlwaveformdisplay.cpp + src/qml/qmlwaveformoverview.cpp src/qml/qmlwaveformrenderer.cpp + src/qml/qmlsettingparameter.cpp + src/qml/qmlskincontrolcreator.cpp + src/qml/qmltrackproxy.cpp + src/qml/qmlsoundmanagerproxy.cpp + src/qml/qmlpreferencesproxy.cpp src/waveform/renderers/allshader/digitsrenderer.cpp src/waveform/renderers/allshader/waveformrenderbeat.cpp src/waveform/renderers/allshader/waveformrenderer.cpp @@ -4037,6 +4321,40 @@ else() applocal ) + # FIXME: Install using IMPORTED_RUNTIME_ARTIFACTS seems to create plugins in the wrong place, leading to packaging issue and runtime problems. + install( + DIRECTORY "${Qt6_DIR}/../../Qt${QT_VERSION_MAJOR}/plugins/multimedia" + DESTINATION "${MIXXX_INSTALL_DATADIR}/Qt${QT_VERSION_MAJOR}/plugins" + COMPONENT applocal + ) + + install( + IMPORTED_RUNTIME_ARTIFACTS + Qt${QT_VERSION_MAJOR}::QuickDialogs2 + DESTINATION + "${MIXXX_INSTALL_DATADIR}" + COMPONENT + applocal + ) + + install( + IMPORTED_RUNTIME_ARTIFACTS + Qt${QT_VERSION_MAJOR}::QuickDialogs2QuickImpl + DESTINATION + "${MIXXX_INSTALL_DATADIR}" + COMPONENT + applocal + ) + + install( + IMPORTED_RUNTIME_ARTIFACTS + Qt${QT_VERSION_MAJOR}::QuickDialogs2Utils + DESTINATION + "${MIXXX_INSTALL_DATADIR}" + COMPONENT + applocal + ) + #install qml6-module-qt5compat-graphicaleffects install( DIRECTORY @@ -4045,6 +4363,21 @@ else() COMPONENT applocal ) + # install qml6-module-qtmultimedia + install( + DIRECTORY + "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}$<$:/debug>/Qt6/qml/QtMultimedia" + DESTINATION "${MIXXX_INSTALL_DATADIR}/Qt6/qml" + COMPONENT applocal + ) + # Qt6MultimediaQuick doesn't seem to be included in any defined target but it is required. + install( + FILES + "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}$<$:/debug>/bin/Qt6MultimediaQuick$<$:d>${CMAKE_SHARED_LIBRARY_SUFFIX}" + DESTINATION "${MIXXX_INSTALL_DATADIR}" + COMPONENT applocal + ) + # install qml6-module-qtqml-workerscript install( DIRECTORY @@ -4061,6 +4394,14 @@ else() COMPONENT applocal ) + # install qml6-module-qtquick-dialogs + install( + DIRECTORY + "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}$<$:/debug>/Qt6/qml/QtQuick/Dialogs" + DESTINATION "${MIXXX_INSTALL_DATADIR}/Qt6/qml/QtQuick" + COMPONENT applocal + ) + # install qml6-module-qtquick-layouts install( DIRECTORY @@ -4101,6 +4442,14 @@ else() COMPONENT applocal ) + # qml6-module-qtquick-effects + install( + DIRECTORY + "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}$<$:/debug>/Qt6/qml/QtQuick/Effects" + DESTINATION "${MIXXX_INSTALL_DATADIR}/Qt6/qml/Effects" + COMPONENT applocal + ) + # install qml6-module-qt-labs-qmlmodels install( DIRECTORY @@ -4108,6 +4457,22 @@ else() DESTINATION "${MIXXX_INSTALL_DATADIR}/Qt6/qml/Qt/labs" COMPONENT applocal ) + + # install qml6-module-qtquick-dialogs + install( + DIRECTORY + "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}$<$:/debug>/Qt6/qml/QtQuick/Dialogs" + DESTINATION "${MIXXX_INSTALL_DATADIR}/Qt6/qml/QtQuick" + COMPONENT applocal + ) + + # install qml6-module-qt-labs-folderlistmodel + install( + DIRECTORY + "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}$<$:/debug>/Qt6/qml/Qt/labs/folderlistmodel" + DESTINATION "${MIXXX_INSTALL_DATADIR}/Qt6/qml/Qt/labs" + COMPONENT applocal + ) endif() if(WIN32) @@ -4192,7 +4557,7 @@ if(APPLE) # Used for battery measurements and controlling the screensaver on macOS. target_link_libraries(mixxx-lib PRIVATE "-weak_framework IOKit") endif() -elseif(UNIX AND NOT APPLE AND NOT EMSCRIPTEN) +elseif(UNIX AND NOT APPLE AND NOT EMSCRIPTEN AND NOT ANDROID) if(QT6) find_package(X11) else() @@ -4203,6 +4568,7 @@ elseif(UNIX AND NOT APPLE AND NOT EMSCRIPTEN) if(${X11_FOUND}) target_include_directories(mixxx-lib SYSTEM PUBLIC "${X11_INCLUDE_DIR}") target_link_libraries(mixxx-lib PRIVATE "${X11_LIBRARIES}") + target_compile_definitions(mixxx-lib PUBLIC __X11__) endif() find_package(Qt${QT_VERSION_MAJOR} COMPONENTS DBus REQUIRED) target_link_libraries(mixxx-lib PUBLIC Qt${QT_VERSION_MAJOR}::DBus) @@ -4417,6 +4783,10 @@ if(RUBBERBAND) find_package(rubberband REQUIRED) target_link_libraries(mixxx-lib PUBLIC rubberband::rubberband) target_compile_definitions(mixxx-lib PUBLIC __RUBBERBAND__) + if(QML) + target_link_libraries(mixxx-qml-lib PRIVATE rubberband::rubberband) + target_compile_definitions(mixxx-qml-lib PUBLIC __RUBBERBAND__) + endif() target_sources( mixxx-lib PRIVATE @@ -4467,7 +4837,7 @@ cmake_dependent_option( BATTERY "Battery meter support" ON - "WIN32 OR UNIX" + "WIN32 OR (UNIX AND NOT ANDROID)" OFF ) if(BATTERY) @@ -4606,31 +4976,37 @@ endif() # FDK-AAC is loaded dynamically at runtime by EncoderFdkAac using QLibrary, # so copy it into the Windows and macOS packages, but do not link to it. -if(APPLE AND MACOS_BUNDLE) - find_library(FDK_AAC_LIBRARY fdk-aac) - if(FDK_AAC_LIBRARY) - message(STATUS "Found fdk-aac: ${FDK_AAC_LIBRARY}") - file( - COPY ${FDK_AAC_LIBRARY} - DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/lib/fdk-aac-install" - FOLLOW_SYMLINK_CHAIN - ) - install( - DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/lib/fdk-aac-install/" - DESTINATION "${MIXXX_INSTALL_PREFIX}/Contents/Frameworks" - ) - else() - message(STATUS "Could NOT find libfdk-aac.dylib") - endif() -elseif(WIN32) - # On Windows find_library finds the .lib file, but the installer needs the .dll file. - find_file(FDK_AAC_DLL fdk-aac.dll PATH_SUFFIXES ${CMAKE_INSTALL_BINDIR}) - if(FDK_AAC_DLL) - message(STATUS "Found fdk-aac DLL: ${FDK_AAC_DLL}") - install(FILES ${FDK_AAC_DLL} DESTINATION ${MIXXX_INSTALL_BINDIR}) +find_package(FdkAac QUIET) +if(FdkAac_FOUND) + if(WIN32) + if(FdkAac_DLL) + message( + STATUS + "Found fdk-aac DLL: ${FdkAac_DLL} (found version \"${FdkAac_VERSION}\")" + ) + install(FILES "${FdkAac_DLL}" DESTINATION "${MIXXX_INSTALL_BINDIR}") + else() + message(STATUS "Could NOT find fdk-aac.dll (FdkAac_DLL not set)") + endif() else() - message(STATUS "Could NOT find fdk-aac.dll") + message( + STATUS + "Found fdk-aac: ${FdkAac_LIBRARY} (found version \"${FdkAac_VERSION}\")" + ) + if(APPLE AND MACOS_BUNDLE) + file( + COPY "${FdkAac_LIBRARY}" + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/lib/fdk-aac-install" + FOLLOW_SYMLINK_CHAIN + ) + install( + DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/lib/fdk-aac-install/" + DESTINATION "${MIXXX_INSTALL_PREFIX}/Contents/Frameworks" + ) + endif() endif() +else() + message(STATUS "Could NOT find fdk-aac (FdkAac_FOUND is FALSE)") endif() # Google PerfTools @@ -4917,11 +5293,25 @@ if(HID) src/controllers/hid/hidiooutputreport.cpp src/controllers/hid/hiddevice.cpp src/controllers/hid/hidenumerator.cpp + src/controllers/hid/hidreportdescriptor.cpp src/controllers/hid/hidusagetables.cpp src/controllers/hid/legacyhidcontrollermapping.cpp src/controllers/hid/legacyhidcontrollermappingfilehandler.cpp ) + + if(ANDROID) + target_sources(mixxx-lib PRIVATE src/controllers/android.cpp) + else() + # Android doesn't support QWidget and is not able to compile this manager due to compiler lacking support for structure binding (error: capturing a structured binding is not yet supported) + target_sources( + mixxx-lib + PRIVATE src/controllers/controllerhidreporttabsmanager.cpp + ) + endif() target_compile_definitions(mixxx-lib PUBLIC __HID__) + if(QML) + target_compile_definitions(mixxx-qml-lib PUBLIC __HID__) + endif() endif() # USB Bulk controller support @@ -4949,6 +5339,9 @@ if(BULK) endif() target_compile_definitions(mixxx-lib PUBLIC __BULK__) target_link_libraries(mixxx-lib PRIVATE LibUSB::LibUSB) + if(QML) + target_compile_definitions(mixxx-qml-lib PUBLIC __BULK__) + endif() endif() # Vinyl Control @@ -4979,7 +5372,15 @@ if(VINYLCONTROL) add_library(mixxx-xwax STATIC EXCLUDE_FROM_ALL) target_sources( mixxx-xwax - PRIVATE lib/xwax/timecoder.c lib/xwax/lut.c lib/xwax/pitch_kalman.c + PRIVATE + lib/xwax/timecoder.c + lib/xwax/timecoder_mk2.c + lib/xwax/lut.c + lib/xwax/lut_mk2.c + lib/xwax/filters.c + lib/xwax/ringbuffer.c + lib/xwax/pitch_kalman.c + lib/xwax/fmatrix.c ) target_include_directories(mixxx-xwax SYSTEM PUBLIC lib/xwax) target_link_libraries(mixxx-lib PRIVATE mixxx-xwax) @@ -4991,6 +5392,7 @@ target_compile_definitions( rendergraph_gl PUBLIC $<$:MIXXX_DEBUG_ASSERTIONS_ENABLED> ) +mixxx_target_warnings_fatal(rendergraph_gl PRIVATE) target_link_libraries(mixxx-lib PUBLIC rendergraph_gl) target_compile_definitions(mixxx-lib PUBLIC rendergraph=rendergraph_gl) target_compile_definitions(mixxx-lib PRIVATE allshader=allshader_gl) @@ -5002,6 +5404,7 @@ if(QML) rendergraph_sg PUBLIC $<$:MIXXX_DEBUG_ASSERTIONS_ENABLED> ) + mixxx_target_warnings_fatal(rendergraph_sg PRIVATE) target_link_libraries(mixxx-qml-lib PRIVATE rendergraph_sg) target_compile_definitions(mixxx-qml-lib PRIVATE rendergraph=rendergraph_sg) target_compile_definitions(mixxx-qml-lib PRIVATE __SCENEGRAPH__) @@ -5101,18 +5504,25 @@ if(QT6) list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qt6-svg-plugins") endif() if(QML) + list( + APPEND + CPACK_DEBIAN_PACKAGE_DEPENDS + "qml6-module-qt-labs-folderlistmodel" + ) + list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qt-labs-qmlmodels") list( APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qt5compat-graphicaleffects" ) + list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qtmultimedia") + list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qtqml-workerscript") list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qtquick-controls") + list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qtquick-dialogs") list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qtquick-layouts") + list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qtquick-shapes") list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qtquick-templates") list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qtquick-window") - list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qt-labs-qmlmodels") - list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qtquick-shapes") - list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "qml6-module-qtqml-workerscript") endif() else() list(APPEND CPACK_DEBIAN_PACKAGE_DEPENDS "libqt5sql5-sqlite") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 053db88fda20..3b3adff97483 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,97 +1,291 @@ -# Mixxx Contribution Guidelines +# Contributing to Mixxx -Thank you for contributing to [Mixxx](https://mixxx.org/)! Your work helps DJs all over the world! We are global, all volunteer team that works by consensus and we are excited to have you join us. This document specifies technical aspects of our workflow. For social aspects please refer to [CODE_OF_CONDUCT.md](https://github.com/mixxxdj/mixxx/blob/main/CODE_OF_CONDUCT.md) in this repository. We encourage you to introduce yourself on our [Zulip chat](https://mixxx.zulipchat.com/) before starting to contribute code to Mixxx. +Thank you for contributing to [Mixxx](https://mixxx.org/)! Mixxx is a free, open-source DJ +application built with C++, Qt, and JavaScript (for controller mappings). We are global, all +volunteer team that works by consensus and we are excited to have you join us. Your work helps DJs +all over the world! This document specifies technical aspects of our workflow. For social aspects +please refer to [CODE_OF_CONDUCT.md](https://github.com/mixxxdj/mixxx/blob/main/CODE_OF_CONDUCT.md) +in this repository. We encourage you to introduce yourself on our [Zulip +chat](https://mixxx.zulipchat.com/) before starting to contribute code to Mixxx. -Table of Contents +**Contents:** 1. [Orientation](#orientation) - 1. [Git Repositories](#git-repositories) -2. [Git Workflow](#git-workflow) - 1. [All Contributors](#all-contributors) - 2. [Core Team](#core-team) + - [Important Guidelines and Policies](#important-guidelines-and-policies) + - [Git Repositories](#git-repositories) +2. [Getting Started](#getting-started) +3. [Pre-commit Setup](#pre-commit) +4. [Code Style](#code-style) + - [Code Formatting](#code-formatting) + - [C++ Conventions](#c-conventions) + - [Advanced Conventions](#advanced-conventions) + - [JavaScript (Controller Mappings)](#javascript-controller-mappings) + - [QML](#qml) +5. [Git Workflow](#git-workflow) +6. [Pull Requests](#pull-requests) +7. [Code Review](#code-review) +8. [Merging](#merging) +9. [Core Team](#core-team) +10. [Further Reading](#further-reading) ## Orientation -We have lots more helpful information for users and developers on the [Mixxx wiki](https://github.com/mixxxdj/mixxx/wiki) and elsewhere, including [build instructions](https://github.com/mixxxdj/mixxx/wiki#compile-mixxx-from-source-code). +We have lots more helpful information for users and developers on the [Mixxx wiki](https://github.com/mixxxdj/mixxx/wiki) and elsewhere, including [detailed build instructions](https://github.com/mixxxdj/mixxx/wiki#compile-mixxx-from-source-code). This document should be enough to get you off the ground. ### Important Guidelines and Policies -* [Git Workflow](#git-workflow) -* [Coding Guidelines](https://github.com/mixxxdj/mixxx/wiki/Coding-Guidelines) & [Setting up `pre-commit`](#pre-commit) -* [Minimum Requirements Policy](https://github.com/mixxxdj/mixxx/wiki/Coding-Guidelines) -* [Internationalization Workflow](https://github.com/mixxxdj/mixxx/wiki/Internationalization) -* [Release Checklist](https://github.com/mixxxdj/mixxx/wiki/Release-Checklist-2.5.0) +- [Git Workflow](#git-workflow) +- [Coding Guidelines](https://github.com/mixxxdj/mixxx/wiki/Coding-Guidelines) & [Setting up `pre-commit`](#pre-commit) +- [Minimum Requirements Policy](https://github.com/mixxxdj/mixxx/wiki/Minimum-requirements-policy) +- [Internationalization Workflow](https://github.com/mixxxdj/mixxx/wiki/Internationalization) +- [Release Checklist](https://github.com/mixxxdj/mixxx/wiki/Release-Checklist-2.5.0) ### Git Repositories This repository contains the Mixxx source code, skins, controller mappings, and some helpful scripts. We have a few other Git repositories too: -* [mixxxdj/website](https://github.com/mixxxdj/website): content for the main [mixxx.org](https://mixxx.org/) website which is generated with the [Pelican static site generator](https://getpelican.com/) -* [mixxxdj/manual](https://github.com/mixxxdj/manual): content for the Mixxx manual, which uses [Sphinx](https://www.sphinx-doc.org/) -* [mixxxdj/vcpkg](https://github.com/mixxxdj/vcpkg): dependencies packaged for macOS and Windows +- [mixxxdj/website](https://github.com/mixxxdj/website): content for the main [mixxx.org](https://mixxx.org/) website which is generated with the [Pelican static site generator](https://getpelican.com/) +- [mixxxdj/manual](https://github.com/mixxxdj/manual): content for the Mixxx manual, which uses [Sphinx](https://www.sphinx-doc.org/) +- [mixxxdj/vcpkg](https://github.com/mixxxdj/vcpkg): dependencies packaged for macOS, Windows, Android and Linux (statically linked builds) All of these are automatically built and deployed by [GitHub Actions](https://github.com/features/actions) whenever a change is committed. You are welcome to open a pull request in any of these repositories. -## Git Workflow +## Getting Started + +1. **Fork** the repository on GitHub by clicking the **Fork** button on + . + +2. **Clone** your fork locally: + + ```shell + git clone https://github.com//mixxx.git + cd mixxx + ``` + +3. **Set up a remote origin** so you can pull updates to your branch from Mixxx's official repo: + + ```shell + git remote add upstream https://github.com/mixxxdj/mixxx.git + `` + +4. **Create a branch** for your work: + + ```shell + git checkout -b my-feature + ``` + + Bug fixes go to the current stable branch (e.g. `2.5`). New features go + to `main`. + +5. **Install dependencies** for your platform: + + Mixxx requires many libraries to build. We have convenience scripts to make installing these dependencies easier: + + | Platform | Command | Requirements | + | -- | ------- | ------------ | + | Windows | `tools\windows_buildenv.bat` | ~2.5 GB download, ~9 GB disk space | + | macOS | `source tools/macos_buildenv.sh setup` | ~1.5 GB download, ~3 GB disk space | + | Debian/Ubuntu | `tools/debian_buildenv.sh setup` | ~200 MB download, ~1 GB disk space | + | Fedora | `tools/rpm_buildenv.sh setup` | ~200 MB download, ~1 GB disk space | + | Arch | `tools/archlinux_buildenv.sh setup` | ~200 MB download, ~1 GB disk space | + | Flatpak | `tools/flatpak_buildenv.sh setup` | ~2.6 GB download, ~5 GB disk space | + | Android | `tools/android_buildenv.sh setup` (see the [wiki article]( Building-for-Android)) | ~3.4 GB download, 13GB disk space | + | Other Linux distros | See the [wiki article](https://github.com/mixxxdj/mixxx/wiki/Compiling%20on%20Linux) | | + + Other platforms: see the wiki's + [build instructions](https://github.com/mixxxdj/mixxx/wiki#compile-mixxx-from-source-code). + +6. **Build**: + + ```shell + mkdir build && cd build + cmake .. + cmake --build . --parallel $(nproc) + + There should now be a `mixxx` executable in the current directory that you can run. Alternatively, can generate a package using `cpack`. + + For building and installing Mixxx as a Flatpak, check the documentation in [packaging/flatpak/README.md](packaging/flatpak/ README.md). + + Detailed build instructions for each target OS can be found [on the wiki](https://github.com/mixxxdj/mixxx/wiki#compile-mixxx-from-source-code) + +7. **Run tests**: + + `ctest` or `mixxx-test` (Useful if you wish to run the test with a debugger) + +## Pre-commit Setup {#pre-commit} + +Install [pre-commit](https://pre-commit.com/#install) to automatically ensure that your commits comply with our code style for both C++ and JavaScript. This saves time reviewing so we don't have to point out nitpicky style issues. Once you have pre-commit installed on your computer, set it up in your local Git repository: + + ```shell + pip install pre-commit # or use your system package manager + cd /path/to/mixxx + pre-commit install + pre-commit install -t pre-push + ``` + +To skip a specific hook when needed: -### `pre-commit` + ```shell + SKIP=clang-format,end-of-file-fixer git commit + ``` -* Install [pre-commit](https://pre-commit.com/#install) to automatically ensure that your commits comply with our code style for both C++ and JavaScript. This saves time reviewing so we don't have to point out nitpicky style issues. Once you have pre-commit installed on your computer, set it up in your local Git repository: +This is preferable to `git commit --no-verify` because it still runs the +other hooks. You can use `SKIP` to separate formatting changes from logic +changes into two sequential commits (see [Code Formatting](#code-formatting) +below). - cd /path/to/your/git/repo - pre-commit install - pre-commit install -t pre-push +## Code Style - If you have a problems with a particular hook, you can use the `SKIP` environment variable to disable hooks: +### Code Formatting - SKIP=clang-format,end-of-file-fixer git commit +Formatting is defined by the [`.clang-format`](.clang-format) file in the +project root (Google base, 4-space indent, 8-space continuation). **Only +format new or modified code** — do not mass-reformat unrelated code. Keep +formatting commits separate from logic commits. - This can also be used to separate logic changes and autoformatting into two subsequent commits. Using the SKIP environment variable is preferable to using `git commit --no-verify` (which also disables the checks) because it won't prevent catching other, unrelated issues. +### C++ Conventions -### All Contributors +- 4 spaces, never tabs. 100-column hard limit, 80-column soft target. +- **Naming**: Classes `CamelCase`, methods `camelBack()`, members `m_prefix`, + pointers `pPrefix`, constants `kPascalCase`, enums `enum class CamelCase`, + CO/setting keys `snake_case`. +- Google-style braces (opening brace on the same line). Always use braces on control + flow bodies, even single-line ones. +- `#pragma once` instead of include guards. +- **Include order** (separated by blank lines, alphabetical within each + group): matching header → system → Qt → library deps → Mixxx local → + forward declarations. +- No naked `new`/`delete` — use `std::make_unique`, `std::make_shared`, or + `make_parented`. +- No `goto`, no `Q_UNUSED` (use unnamed parameters), no C-style enums. +- `///` doc comments in headers. `// TODO(username)` or + `// TODO(issue URL)`. -* Each feature/bug fix should be done on its own Git branch so they can be reviewed and merged independently. Refer to [Using Git](https://github.com/mixxxdj/mixxx/wiki/using-git) for how to do this. Please ask for help on [Zulip](https://mixxx.zulipchat.com/) if you have questions about using Git after reading that page. -* Commits should be as small as they can while still building. The smaller the commit, the easier it is to review. It also makes it easier to revert if it is later identified as the source of a bug. If you have lots of changes that you need to commit, a [GUI Git client](https://git-scm.com/downloads/guis) can be helpful for picking out specific changes for multiple small commits. -* Every commit should build. This is important so [git bisect](https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git#_binary_search) works. -* Commit messages should succinctly describe what is changed in that commit and why. Lines should wrap at 72 characters so they show fully in GitHub and other Git tools. For example, this is a good commit message: +### Advanced Conventions + +First-time contributors probably don't need to worry about these: + +- Wrap new code in `namespace mixxx {}`. Use anonymous namespaces for + file-local helpers in `.cpp` files. +- `QStringLiteral("...")` for string literals; `tr("...")` for translatable + strings. +- `override` on all virtual overrides; omit the redundant `virtual` keyword. +- Non-const reference out-parameters: use pointers, not references (legacy + convention). +- Lambdas: use carefully — they receive extra scrutiny in review for lifetime + and control-flow issues. +- `VERIFY_OR_DEBUG_ASSERT(cond) { recovery; }` for defensive checks. + +For the full C++ style guide, see the +[Coding Guidelines](https://github.com/mixxxdj/mixxx/wiki/Coding-Guidelines) +wiki page. + +### JavaScript (Controller Mappings) + +- Scripts live in `res/controllers/`. ESLint is enforced via pre-commit + ([`eslint.config.cjs`](eslint.config.cjs)). +- Use the Components JS library and JSDoc comments. + +### QML + +The Mixxx UI is currently being rewritten in QML. This new code is in a high state of flux and will change rapidly. + +- Source in `res/qml/` and `src/qml/`. +- `qmlformat` and `qmllint` are available via pre-commit. + +## Git Workflow + +- **One branch per feature or bug fix**. Each feature/bug fix should be done on its own Git branch so they can be reviewed and merged independently. Refer to [Using Git](https://github.com/mixxxdj/mixxx/wiki/using-git) for how to do this. Please ask for help on [Zulip](https://mixxx.zulipchat.com/) if you have questions about using Git after reading that page. +- **Every commit must build.** The smaller the commit, the easier it is to review. This is also important so [git bisect](https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git#_binary_search) works. If you have lots of changes that you need to commit, a [GUI Git client](https://git-scm.com/downloads/guis) can be helpful for picking out specific changes for multiple small commits. +- **Small PRs.** The smaller the pull request, the easier it is to review and revert. +- Write commit messages in the **imperative mood**. Commit messages should succinctly describe what is changed in that commit and why. Lines should wrap at 72 characters so they show fully in GitHub and other Git tools. For example, this is a good commit message: + + Good: DlgPrefEffects: add QListWidget to set order of chains This order will soon be used by new ControlObjects to load them from controllers. - This is not a good commit message: + Not so good: address comments from PR review - Neither is this: + Refer to [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) for more details. - fix a bug with quantize while the deck is playing and sync leader is enabled and an effect unit is on the deck while the user is turning an EQ knob +- **Separate refactoring / fixup commits** from logic commits. Create commits that clean up or rearrange code as a no-op refactor first, then commit logic changes. +- Use `git commit --fixup=` for changes that should be squashed before + merge. Only squash when asked by a reviewer. +- Generally, prefer merging over rebasing. Do not rebase unless you have discussed that with whoever is reviewing the pull request. When you rebase a branch with an open pull request, it is no longer possible to distinguish your latest changes from already reviewed parts, resulting in unnecessary extra work for the reviewer. Comments made directly to a single commit will be lost. Rebased commits are likely not tested and there is a risk that building fails in a later `git bisect` run. If you want to correct minor mistakes with a rebase within a few minutes of pushing commits, that is okay as long as no one has started reviewing those commits yet. A `git commit --amend` is possible at any time as long the commit has the limited scope of one topic. +- If you plan to rebase your branch before merge to eliminate forth and back commits or such, you can commit them with the --fixup option or manually add "fixup!" as the first word in the commit message. This prevents mergeing to upstream before the rebase. +- If you are helping with someone else's pull request that is not yet merged, open a pull request targeted at their fork. Leave a comment on the upstream pull request (which targets mixxxdj/mixxx) with a link to your pull request so other Mixxx contributors are aware of your changes. +- Low risk bug fixes should be targeted at the stable branch (e.g., `2.3`). However, bug fixes for the stable branches must have a direct impact on users. If you spot a minor bug reading the code or only want to clean up the code, target that at the `main` or beta branch. +- Controller mappings should be targeted at the stable branch unless they use features that are new in the beta or `main` branch. +- If you are making changes to the GUI with a pull request, please post before and after screenshots of the changes. +- Please help review other people's pull requests. When others review your pull requests, please return the favor. The continued progress of Mixxx depends on all of us working together. Even if you are not familiar with the area of the code being changed in a pull request, you can be helpful by building the branch, verifying that it works as described, and commenting with feedback about the user experience design. +- If you demonstrate good coding skills, help review pull requests, contribute major features, and show a commitment to Mixxx over time, we may invite you to the core team. - Refer to [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) for more details. +## Pull Requests + +- Keep PRs focused — no unrelated formatting, config, or refactoring mixed + in. +- Target the correct branch: bug fixes → stable (e.g. `2.5`), features → + `main`. Controller mappings go to the stable branch unless they use new + features from `main`. +- If your PR changes the GUI, post before-and-after screenshots. +- CI must pass (builds + tests) before a PR can be merged. +- No `.DS_Store`, IDE files, or other untracked artifacts. +- If you are helping with someone else's PR that is not yet merged, open a PR + targeted at their fork and leave a comment on the upstream PR linking to + yours. + +## Code Review + +- **Review others' PRs.** Even if you're unfamiliar with the area, you can + build the branch, test it, and give UX feedback. When others review your + pull requests, please return the favor. +- Reviewers commonly flag: + 1. Pre-commit / CI not passing. + 2. Missing `std::chrono::duration` for time values. + 3. Manual `static_cast` instead of `Q_ENUM` + + `QVariant::fromValue`. + 4. Insufficiently descriptive commit messages — document *why*, not just + *what*. + 5. SVG assets not matching existing conventions (full-size, borderless). + +## Merging + +Merging will be performed by a core member after the pull request has LGTM. +A reviewer who is a core member is allowed to also merge the PR. +Be sure to add a message indicating if your PR is not ready to merge. + +## Core Team + +Core team members have write access to the +[upstream mixxxdj repositories](https://github.com/mixxxdj/). See the +[Contribution Guidelines](https://github.com/mixxxdj/mixxx/wiki/Contribution-Guidelines) +wiki page for core-team policies on merging, force-pushing, and branch +management. + +- Enable [two-factor authentication (2FA)](https://help.github.com/en/github/authenticating-to-github/securing-your-account-with-two-factor-authentication-2fa) for your GitHub account. +- *Never* force push to an upstream repository (mixxxdj). If you encounter an error from Git saying you would need to force push, stop what you are doing and discuss the situation on Zulip. +- All non-trivial contributions should be made with a pull request, just like any other contributor who does not have write access. Do not merge your own pull requests. +- You may merge someone else's pull request as the only reviewer if no other contributors have expressed concerns about the changes or said they want to review the code. Please do not merge pull requests immediately; allow at least a day or two for others to comment. Remember we are all volunteers and cannot respond to everything immediately. +- If there is disagreement about changes in a pull request, do not merge it until a consensus has been reached. +- Check CI to ensure builds work and tests pass before merging. If CI timed out, either manually restart it or build the branch and run the tests locally before merging. +- When you merge a pull request to a stable branch, merge the stable branch to the beta branch afterwards. If you merge a pull request to a beta branch, merge the beta branch to `main` afterwards. When backporting, cherry-pick or rebase rather than merge. +- Merge PRs using a merge, to keep the original commits valid. Keep the default commit message "Merge pull request ..." with the reference to the pull request. In case where the PR contains broken (non-building) commits, back-and-forth commits or commits without a meaningful commit message that are not worth keeping, ask the author to squash the commits before merge. Alternatively you may ask the contributor to check "Allow edits and access to secrets by maintainers". Then you can squash locally or use the `/softfix` comment to squash remotely. See [Softfix](https://github.com/daschuer/softfix/?tab=readme-ov-file#softfix-a-pull-request) +- Default to open; only post in the private Zulip stream for discussions that have a reason to be private. Most of the time, post to a public Zulip stream so anyone can participate in the discussion. +- When Mixxx participates in Google Summer of Code, you may volunteer as a mentor if you like. +- Benchmark runs will occur on release branches only by default. It also possible to request a benchmark run in the PR by using the comment `/benchmark`. + +## Further Reading -* Generally, prefer merging over rebasing. Do not rebase unless you have discussed that with whoever is reviewing the pull request. When you rebase a branch with an open pull request, it is no longer possible to distinguish your latest changes from already reviewed parts, resulting in unnecessary extra work for the reviewer. Comments made directly to a single commit will be lost. Rebased commits are likely not tested and there is a risk that building fails in a later `git bisect` run. If you want to correct minor mistakes with a rebase within a few minutes of pushing commits, that is okay as long as no one has started reviewing those commits yet. A `git commit --amend` is possible at any time as long the commit has the limited scope of one topic. -* If you plan to rebase your branch before merge to eliminate forth and back commits or such, you can commit them with the --fixup option or manually add "fixup!" as the first word in the commit message. This prevents mergeing to upstream before the rebase. -* If you are helping with someone else's pull request that is not yet merged, open a pull request targeted at their fork. Leave a comment on the upstream pull request (which targets mixxxdj/mixxx) with a link to your pull request so other Mixxx contributors are aware of your changes. -* Low risk bug fixes should be targeted at the stable branch (e.g., `2.3`). However, bug fixes for the stable branches must have a direct impact on users. If you spot a minor bug reading the code or only want to clean up the code, target that at the `main` or beta branch. -* Controller mappings should be targeted at the stable branch unless they use features that are new in the beta or `main` branch. -* If you are making changes to the GUI with a pull request, please post before and after screenshots of the changes. -* Please help review other people's pull requests. When others review your pull requests, please return the favor. The continued progress of Mixxx depends on all of us working together. Even if you are not familiar with the area of the code being changed in a pull request, you can be helpful by building the branch, verifying that it works as described, and commenting with feedback about the user experience design. -* If you demonstrate good coding skills, help review pull requests, contribute major features, and show a commitment to Mixxx over time, we may invite you to the core team. - -### Core Team - -Mixxx core team members are contributors who have write access to the [upstream mixxxdj repositories](https://github.com/mixxxdj/) on GitHub, access to the Jenkins web interface for the build servers, and access to the private Zulip stream for the core team. - -* Enable [two-factor authentication (2FA)](https://help.github.com/en/github/authenticating-to-github/securing-your-account-with-two-factor-authentication-2fa) for your GitHub account. -* _Never_ force push to an upstream repository (mixxxdj). If you encounter an error from Git saying you would need to force push, stop what you are doing and discuss the situation on Zulip. -* Only push directly to an upstream repository (mixxxdj) for trivial, uncontroversial changes like fixing a typo. -* All non-trivial contributions should be made with a pull request, just like any other contributor who does not have write access. Do not merge your own pull requests. -* You may merge someone else's pull request as the only reviewer if no other contributors have expressed concerns about the changes or said they want to review the code. Please do not merge pull requests immediately; allow at least a day or two for others to comment. Remember we are all volunteers and cannot respond to everything immediately. -* If there is disagreement about changes in a pull request, do not merge it until a consensus has been reached. -* Check CI to ensure builds work and tests pass before merging. If CI timed out, either manually restart it or build the branch and run the tests locally before merging. -* When you merge a pull request to a stable branch, merge the stable branch to the beta branch afterwards. If you merge a pull request to a beta branch, merge the beta branch to `main` afterwards. When backporting, cherry-pick or rebase rather than merge. -* Merge PRs using a merge, to keep the original commits valid. Keep the default commit message "Merge pull request ..." with the reference to the pull request. In case where the PR contains broken (non-building) commits, back-and-forth commits or commits without a meaningful commit message that are not worth keeping, ask the author to squash the commits before merge. Alternatively you may ask the contributor to check "Allow edits and access to secrets by maintainers". Then you can squash locally or use the `/softfix` comment to squash remotely. See [Softfix](https://github.com/daschuer/softfix/?tab=readme-ov-file#softfix-a-pull-request) -* Default to open; only post in the private Zulip stream for discussions that have a reason to be private. Most of the time, post to a public Zulip stream so anyone can participate in the discussion. -* When Mixxx participates in Google Summer of Code, you may volunteer as a mentor if you like. -* Benchmark runs will occur on release branches only by default. It also possible to request a benchmark run in the PR by using the comment `/benchmark`. +- [Using Git](https://github.com/mixxxdj/mixxx/wiki/Using-Git) — branching, + rebasing, and common workflows +- [Coding Guidelines](https://github.com/mixxxdj/mixxx/wiki/Coding-Guidelines) + — full C++ style guide +- [Contribution Guidelines](https://github.com/mixxxdj/mixxx/wiki/Contribution-Guidelines) + — complete contributor and core-team policies +- [Developer Guide](https://github.com/mixxxdj/mixxx/wiki/Developer-Guide) +- [Mixxx Wiki](https://github.com/mixxxdj/mixxx/wiki) diff --git a/LICENSE b/LICENSE index 0a5e26ebe833..8ea4ac28edd6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Mixxx 2.6-beta, Digital DJ'ing software. +Mixxx 2.7-alpha, Digital DJ'ing software. Copyright (C) 2001-2026 Mixxx Development Team Mixxx is free software; you can redistribute it and/or modify diff --git a/README.md b/README.md index aa8f7a2b53b4..0dd1be6f6863 100644 --- a/README.md +++ b/README.md @@ -26,42 +26,11 @@ Have a bug or feature request? [File a bug on Github][fileabug]. Want to get involved in Mixxx development? Assign yourself a bug from the [easy bug list][easybugs] and get started! -Read [CONTRIBUTING](CONTRIBUTING.md) for more information. ## Building Mixxx -First, open a terminal (on Windows, use "**x64 Native Tools Command Prompt for -[VS 2022][visualstudio2022]**"), download the mixxx -source code and navigate to it: - - $ git clone https://github.com/mixxxdj/mixxx.git - $ cd mixxx - -Download the required dependencies and set up the build environment by running the -corresponding command for your operating system: - -| Platform | Command | Requirements | -| -- | ------- | ------------ | -| Windows | `tools\windows_buildenv.bat` | ~2.5 GB download, ~9 GB disk space | -| macOS | `source tools/macos_buildenv.sh setup` | ~1.5 GB download, ~3 GB disk space | -| Debian/Ubuntu | `tools/debian_buildenv.sh setup` | ~200 MB download, ~1 GB disk space | -| Fedora | `tools/rpm_buildenv.sh setup` | ~200 MB download, ~1 GB disk space | -| Flatpak | `tools/flatpak_buildenv.sh setup` | ~2.6 GB download, ~5 GB disk space | -| Other Linux distros | See the [wiki article](https://github.com/mixxxdj/mixxx/wiki/Compiling%20on%20Linux) | | - -To build Mixxx, run - - $ mkdir build - $ cd build - $ cmake .. - $ cmake --build . - -There should now be a `mixxx` executable in the current directory that you can -run. Alternatively, can generate a package using `cpack`. - -For building and installing Mixxx as a Flatpak, check the documentation in [packaging/flatpak/README.md](packaging/flatpak/README.md). - -Detailed build instructions for each target OS can be found [on the wiki](https://github.com/mixxxdj/mixxx/wiki#compile-mixxx-from-source-code) +Read [CONTRIBUTING.md](CONTRIBUTING.md) for build instructions, code style +guidelines, and how to open a pull request. ## Documentation @@ -107,7 +76,6 @@ license. [blog]: https://mixxx.org/news/ [manual]: https://manual.mixxx.org/ [wiki]: https://github.com/mixxxdj/mixxx/wiki -[visualstudio2022]: https://docs.microsoft.com/visualstudio/install/install-visual-studio?view=vs-2022 [easybugs]: https://github.com/mixxxdj/mixxx/issues?q=is%3Aopen+is%3Aissue+label%3Aeasy [creating skins]: https://mixxx.org/wiki/doku.php/Creating-Skins [help translate content]: https://explore.transifex.com/mixxx-dj-software/ diff --git a/cmake/modules/FindFFMPEG.cmake b/cmake/modules/FindFFMPEG.cmake deleted file mode 100644 index 55255cc30e20..000000000000 --- a/cmake/modules/FindFFMPEG.cmake +++ /dev/null @@ -1,202 +0,0 @@ -#.rst: -# FindFFMPEG -# ---------- -# -# Try to find the required ffmpeg components (default: libavformat, libavutil, libavcodec) -# -# Next variables can be used to hint FFMPEG libs search: -# -# :: -# -# PC__LIBRARY_DIRS -# PC_FFMPEG_LIBRARY_DIRS -# PC__INCLUDE_DIRS -# PC_FFMPEG_INCLUDE_DIRS -# -# Once done this will define -# -# :: -# -# FFMPEG_FOUND - System has the all required components. -# FFMPEG_INCLUDE_DIRS - Include directory necessary for using the required components headers. -# FFMPEG_LIBRARIES - Link these to use the required ffmpeg components. -# FFMPEG_DEFINITIONS - Compiler switches required for using the required ffmpeg components. -# -# For each of the components it will additionally set. -# -# :: -# -# libavcodec -# libavdevice -# libavformat -# libavfilter -# libavutil -# libswscale -# libswresample -# -# the following variables will be defined -# -# :: -# -# _FOUND - System has -# _INCLUDE_DIRS - Include directory necessary for using the headers -# _LIBRARIES - Link these to use -# _DEFINITIONS - Compiler switches required for using -# _VERSION - The components version -# -# the following import targets is created -# -# :: -# -# FFMPEG::FFMPEG - for all components -# FFMPEG:: - where in lower case (FFMPEG::avcodec) for each components -# -# Copyright (c) 2006, Matthias Kretz, -# Copyright (c) 2008, Alexander Neundorf, -# Copyright (c) 2011, Michael Jansen, -# Copyright (c) 2017, Alexander Drozdov, -# Copyright (c) 2019, Jan Holthuis, -# -# Redistribution and use is allowed according to the terms of the BSD license. -# For details see the accompanying COPYING-CMAKE-SCRIPTS file. - -include(FindPackageHandleStandardArgs) - -# The default components were taken from a survey over other FindFFMPEG.cmake files -if(NOT FFMPEG_FIND_COMPONENTS) - set(FFMPEG_FIND_COMPONENTS libavcodec libavformat libavutil) -endif() - -# -### Macro: find_component -# -# Checks for the given component by invoking pkgconfig and then looking up the libraries and -# include directories. -# -macro(find_component component pkgconfig library header) - # use pkg-config to get the directories and then use these values - # in the FIND_PATH() and FIND_LIBRARY() calls - find_package(PkgConfig QUIET) - if(PkgConfig_FOUND) - pkg_check_modules(PC_FFMPEG_${component} QUIET ${pkgconfig}) - endif() - - find_path( - FFMPEG_${component}_INCLUDE_DIRS - ${header} - HINTS - ${PC_FFMPEG_${component}_INCLUDEDIR} - ${PC_FFMPEG_${component}_INCLUDE_DIRS} - ${PC_FFMPEG_INCLUDE_DIRS} - PATH_SUFFIXES ffmpeg - ) - - find_library( - FFMPEG_${component}_LIBRARIES - NAMES ${PC_FFMPEG_${component}_LIBRARIES} ${library} - HINTS - ${PC_FFMPEG_${component}_LIBDIR} - ${PC_FFMPEG_${component}_LIBRARY_DIRS} - ${PC_FFMPEG_LIBRARY_DIRS} - ) - - #message(STATUS ${FFMPEG_${component}_LIBRARIES}) - #message(STATUS ${PC_FFMPEG_${component}_LIBRARIES}) - - set( - FFMPEG_${component}_DEFINITIONS - ${PC_FFMPEG_${component}_CFLAGS_OTHER} - CACHE STRING - "The ${component} CFLAGS." - ) - set( - FFMPEG_${component}_VERSION - ${PC_FFMPEG_${component}_VERSION} - CACHE STRING - "The ${component} version number." - ) - - if(FFMPEG_${component}_LIBRARIES AND FFMPEG_${component}_INCLUDE_DIRS) - message(STATUS " - ${component} ${FFMPEG_${component}_VERSION} found.") - set(FFMPEG_${component}_FOUND TRUE) - else() - message(STATUS " - ${component} not found.") - endif() - - mark_as_advanced( - FFMPEG_${component}_INCLUDE_DIRS - FFMPEG_${component}_LIBRARIES - FFMPEG_${component}_DEFINITIONS - FFMPEG_${component}_VERSION - ) -endmacro() - -message(STATUS "Searching for FFMPEG components") -# Check for all possible component. -find_component(libavcodec libavcodec avcodec libavcodec/avcodec.h) -find_component(libavformat libavformat avformat libavformat/avformat.h) -find_component(libavdevice libavdevice avdevice libavdevice/avdevice.h) -find_component(libavutil libavutil avutil libavutil/avutil.h) -find_component(libavfilter libavfilter avfilter libavfilter/avfilter.h) -find_component(libswscale libswscale swscale libswscale/swscale.h) -find_component(libswresample libswresample swresample libswresample/swresample.h) - -set(FFMPEG_LIBRARIES "") -set(FFMPEG_DEFINITIONS "") -# Check if the required components were found and add their stuff to the FFMPEG_* vars. -foreach(component ${FFMPEG_FIND_COMPONENTS}) - if(FFMPEG_${component}_FOUND) - #message(STATUS "Required component ${component} present.") - set(FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES} ${FFMPEG_${component}_LIBRARIES}) - set( - FFMPEG_DEFINITIONS - ${FFMPEG_DEFINITIONS} - ${FFMPEG_${component}_DEFINITIONS} - ) - list(APPEND FFMPEG_INCLUDE_DIRS ${FFMPEG_${component}_INCLUDE_DIRS}) - endif() -endforeach() - -# Build the include path with duplicates removed. -if(FFMPEG_INCLUDE_DIRS) - list(REMOVE_DUPLICATES FFMPEG_INCLUDE_DIRS) -endif() - -# cache the vars. -set( - FFMPEG_INCLUDE_DIRS - ${FFMPEG_INCLUDE_DIRS} - CACHE STRING - "The FFMPEG include directories." - FORCE -) -set( - FFMPEG_LIBRARIES - ${FFMPEG_LIBRARIES} - CACHE STRING - "The FFMPEG libraries." - FORCE -) -set( - FFMPEG_DEFINITIONS - ${FFMPEG_DEFINITIONS} - CACHE STRING - "The FFMPEG cflags." - FORCE -) - -mark_as_advanced(FFMPEG_INCLUDE_DIRS FFMPEG_LIBRARIES FFMPEG_DEFINITIONS) - -# Compile the list of required vars -set(FFMPEG_REQUIRED_VARS FFMPEG_LIBRARIES FFMPEG_INCLUDE_DIRS) -foreach(component ${FFMPEG_FIND_COMPONENTS}) - list( - APPEND - FFMPEG_REQUIRED_VARS - FFMPEG_${component}_LIBRARIES - FFMPEG_${component}_INCLUDE_DIRS - ) -endforeach() - -# Give a nice error message if some of the required vars are missing. -find_package_handle_standard_args(FFMPEG DEFAULT_MSG ${FFMPEG_REQUIRED_VARS}) diff --git a/cmake/modules/FindFFmpeg.cmake b/cmake/modules/FindFFmpeg.cmake new file mode 100644 index 000000000000..725d19b8d634 --- /dev/null +++ b/cmake/modules/FindFFmpeg.cmake @@ -0,0 +1,347 @@ +#.rst: +# FindFFmpeg +# ---------- +# +# Try to find the required FFmpeg components (default: AVCODEC, AVFORMAT, AVUTIL) +# +# Next variables can be used to hint FFmpeg libs search: +# +# :: +# +# PC_FFmpeg_LIBRARY_DIRS +# PC_FFmpeg_INCLUDE_DIRS +# +# Once done this will define +# +# :: +# +# FFmpeg_FOUND - System has all required components. +# FFmpeg_INCLUDE_DIRS - Include directories for all required components. +# FFmpeg_LIBRARIES - Libraries to link for all required components. +# FFmpeg_DEFINITIONS - Compiler switches required for using FFmpeg. +# +# For each of the components it will additionally set. +# +# :: +# +# FFmpeg__FOUND - System has +# FFmpeg__INCLUDE_DIRS - Include directories for +# FFmpeg__LIBRARIES - Libraries to link for +# FFmpeg__DEFINITIONS - Compiler switches for +# FFmpeg__VERSION - Version of +# +# The following imported targets are created: +# +# :: +# +# FFmpeg::FFmpeg - interface target aggregating all found components +# FFmpeg::avcodec - libavcodec +# FFmpeg::avformat - libavformat +# FFmpeg::avdevice - libavdevice +# FFmpeg::avutil - libavutil +# FFmpeg::avfilter - libavfilter +# FFmpeg::swscale - libswscale +# FFmpeg::swresample - libswresample +# +# Component names are Qt-compliant uppercase (AVCODEC, AVFORMAT, …) as +# required by Qt6's internal find_dependency() calls in +# Qt6FFmpegMediaPluginImplPrivateDependencies.cmake: +# +# find_dependency(FFmpeg COMPONENTS AVCODEC AVFORMAT AVUTIL SWRESAMPLE SWSCALE) +# +# Imported target names are lowercase (FFmpeg::avcodec, …) as listed in +# Qt6FFmpegMediaPluginImplPrivateDependencies.cmake: +# +# provided_targets "FFmpeg::avcodec;FFmpeg::avformat;FFmpeg::avutil; +# FFmpeg::swresample;FFmpeg::swscale" +# +# Copyright (c) 2006, Matthias Kretz, +# Copyright (c) 2008, Alexander Neundorf, +# Copyright (c) 2011, Michael Jansen, +# Copyright (c) 2017, Alexander Drozdov, +# Copyright (c) 2019, Jan Holthuis, +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +include(FindPackageHandleStandardArgs) +include(IsStaticLibrary) + +# Early-return guard: Qt6's find_dependency() chain calls find_package(FFmpeg) +# once per multimedia plugin. Imported targets are global and persistent, so +# if FFmpeg::avcodec already exists the search is done — return silently. +if(TARGET FFmpeg::avcodec) + set(FFmpeg_FOUND TRUE) + return() +endif() + +if(NOT FFmpeg_FIND_COMPONENTS) + set(FFmpeg_FIND_COMPONENTS AVCODEC AVFORMAT AVUTIL) +endif() + +# Component table: UPPERCASE lowercase-stem pkg-config-name primary-header +set( + _FFmpeg_components + "AVCODEC avcodec libavcodec libavcodec/avcodec.h" + "AVFORMAT avformat libavformat libavformat/avformat.h" + "AVDEVICE avdevice libavdevice libavdevice/avdevice.h" + "AVUTIL avutil libavutil libavutil/avutil.h" + "AVFILTER avfilter libavfilter libavfilter/avfilter.h" + "SWSCALE swscale libswscale libswscale/swscale.h" + "SWRESAMPLE swresample libswresample libswresample/swresample.h" +) +foreach(_entry IN LISTS _FFmpeg_components) + separate_arguments(_fields UNIX_COMMAND "${_entry}") + list(GET _fields 0 _uc) + list(GET _fields 1 _lc) + list(GET _fields 2 _pc) + list(GET _fields 3 _hdr) + set(_FFmpeg_${_uc}_lower "${_lc}") + set(_FFmpeg_${_uc}_pkgconfig "${_pc}") + set(_FFmpeg_${_uc}_header "${_hdr}") +endforeach() +unset(_entry) +unset(_fields) +unset(_uc) +unset(_lc) +unset(_pc) +unset(_hdr) + +find_package(PkgConfig QUIET) + +macro(find_component component) + set(_lower "${_FFmpeg_${component}_lower}") + set(_pkgcfg "${_FFmpeg_${component}_pkgconfig}") + set(_header "${_FFmpeg_${component}_header}") + + if(PkgConfig_FOUND) + pkg_check_modules(PC_FFmpeg_${component} QUIET ${_pkgcfg}) + endif() + + find_path( + FFmpeg_${component}_INCLUDE_DIRS + ${_header} + HINTS + ${PC_FFmpeg_${component}_INCLUDEDIR} + ${PC_FFmpeg_${component}_INCLUDE_DIRS} + ${PC_FFmpeg_INCLUDE_DIRS} + PATH_SUFFIXES ffmpeg + ) + find_library( + FFmpeg_${component}_LIBRARIES + NAMES ${PC_FFmpeg_${component}_LIBRARIES} ${_lower} + HINTS + ${PC_FFmpeg_${component}_LIBDIR} + ${PC_FFmpeg_${component}_LIBRARY_DIRS} + ${PC_FFmpeg_LIBRARY_DIRS} + ) + set( + FFmpeg_${component}_DEFINITIONS + ${PC_FFmpeg_${component}_CFLAGS_OTHER} + CACHE STRING + "The ${component} CFLAGS." + ) + set( + FFmpeg_${component}_VERSION + ${PC_FFmpeg_${component}_VERSION} + CACHE STRING + "The ${component} version number." + ) + mark_as_advanced( + FFmpeg_${component}_INCLUDE_DIRS + FFmpeg_${component}_LIBRARIES + FFmpeg_${component}_DEFINITIONS + FFmpeg_${component}_VERSION + ) + + if(FFmpeg_${component}_LIBRARIES AND FFmpeg_${component}_INCLUDE_DIRS) + message(STATUS " - ${component} ${FFmpeg_${component}_VERSION} found.") + set(FFmpeg_${component}_FOUND TRUE) + else() + message(STATUS " - ${component} not found.") + endif() + unset(_lower) + unset(_pkgcfg) + unset(_header) +endmacro() + +message(STATUS "Searching for FFmpeg components") +foreach( + _comp + AVCODEC + AVFORMAT + AVDEVICE + AVUTIL + AVFILTER + SWSCALE + SWRESAMPLE +) + find_component(${_comp}) +endforeach() +unset(_comp) + +# Aggregate results for requested components +set(FFmpeg_LIBRARIES "") +set(FFmpeg_DEFINITIONS "") +set(FFmpeg_INCLUDE_DIRS "") +foreach(component ${FFmpeg_FIND_COMPONENTS}) + if(FFmpeg_${component}_FOUND) + list(APPEND FFmpeg_LIBRARIES ${FFmpeg_${component}_LIBRARIES}) + list(APPEND FFmpeg_DEFINITIONS ${FFmpeg_${component}_DEFINITIONS}) + list(APPEND FFmpeg_INCLUDE_DIRS ${FFmpeg_${component}_INCLUDE_DIRS}) + endif() +endforeach() +list(REMOVE_DUPLICATES FFmpeg_INCLUDE_DIRS) + +# Cache aggregates (consumed by CMakeLists.txt via "${FFmpeg_LIBRARIES}") +set( + FFmpeg_LIBRARIES + ${FFmpeg_LIBRARIES} + CACHE STRING + "The FFmpeg libraries." + FORCE +) +set( + FFmpeg_INCLUDE_DIRS + ${FFmpeg_INCLUDE_DIRS} + CACHE STRING + "The FFmpeg include directories." + FORCE +) +set( + FFmpeg_DEFINITIONS + ${FFmpeg_DEFINITIONS} + CACHE STRING + "The FFmpeg cflags." + FORCE +) +mark_as_advanced(FFmpeg_LIBRARIES FFmpeg_INCLUDE_DIRS FFmpeg_DEFINITIONS) + +# Build required-vars list from requested components +set(_FFmpeg_required FFmpeg_LIBRARIES FFmpeg_INCLUDE_DIRS) +foreach(component ${FFmpeg_FIND_COMPONENTS}) + list( + APPEND + _FFmpeg_required + FFmpeg_${component}_LIBRARIES + FFmpeg_${component}_INCLUDE_DIRS + ) +endforeach() +find_package_handle_standard_args(FFmpeg DEFAULT_MSG ${_FFmpeg_required}) +unset(_FFmpeg_required) + +if(FFmpeg_FOUND) + # Create per-component IMPORTED targets + foreach( + component + AVCODEC + AVFORMAT + AVDEVICE + AVUTIL + AVFILTER + SWSCALE + SWRESAMPLE + ) + if(FFmpeg_${component}_FOUND) + set(_target "FFmpeg::${_FFmpeg_${component}_lower}") + if(NOT TARGET ${_target}) + add_library(${_target} UNKNOWN IMPORTED) + set_target_properties( + ${_target} + PROPERTIES + IMPORTED_LOCATION "${FFmpeg_${component}_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${FFmpeg_${component}_INCLUDE_DIRS}" + INTERFACE_COMPILE_OPTIONS "${FFmpeg_${component}_DEFINITIONS}" + ) + endif() + unset(_target) + endif() + endforeach() + + # When libavcodec is static it may have been built with --enable-libfdk-aac. + # Add FdkAac::FdkAac as a transitive dep so the final linker resolves the + # aacEnc*/aacDec* symbols (mirrors FindChromaprint.cmake's FFTW3 pattern). + if(TARGET FFmpeg::avcodec) + is_static_library(_avcodec_static FFmpeg::avcodec) + if(_avcodec_static) + find_package(FdkAac QUIET) + if(FdkAac_FOUND) + set_property( + TARGET FFmpeg::avcodec + APPEND + PROPERTY INTERFACE_LINK_LIBRARIES FdkAac::FdkAac + ) + # Also append to the cached FFmpeg_LIBRARIES variable so any remaining + # consumers that use "${FFmpeg_LIBRARIES}" directly (e.g. legacy call + # sites) also get the dependency resolved. + list(APPEND FFmpeg_LIBRARIES ${FdkAac_LIBRARY}) + set( + FFmpeg_LIBRARIES + ${FFmpeg_LIBRARIES} + CACHE STRING + "The FFmpeg libraries." + FORCE + ) + endif() + endif() + unset(_avcodec_static) + endif() + + # On Apple platforms the static FFmpeg libraries reference VideoToolbox, + # CoreMedia and CoreVideo symbols (e.g. av_map_videotoolbox_format_to_pixfmt + # in libavutil, hardware-accelerated codecs in libavcodec). Add the + # required frameworks as transitive interface deps on the affected targets. + if(APPLE) + foreach(_component_target IN ITEMS FFmpeg::avcodec FFmpeg::avutil) + if(TARGET ${_component_target}) + is_static_library(_is_static ${_component_target}) + if(_is_static) + set_property( + TARGET ${_component_target} + APPEND + PROPERTY + INTERFACE_LINK_LIBRARIES + "-framework VideoToolbox" + "-framework CoreMedia" + "-framework CoreVideo" + ) + endif() + unset(_is_static) + endif() + endforeach() + unset(_component_target) + + # Also append to FFmpeg_LIBRARIES for legacy consumers + is_static_library(_avutil_static FFmpeg::avutil) + if(_avutil_static) + list( + APPEND + FFmpeg_LIBRARIES + "-framework VideoToolbox" + "-framework CoreMedia" + "-framework CoreVideo" + ) + set( + FFmpeg_LIBRARIES + ${FFmpeg_LIBRARIES} + CACHE STRING + "The FFmpeg libraries." + FORCE + ) + endif() + unset(_avutil_static) + endif() + + # Aggregate convenience target + if(NOT TARGET FFmpeg::FFmpeg) + add_library(FFmpeg::FFmpeg INTERFACE IMPORTED) + foreach(component ${FFmpeg_FIND_COMPONENTS}) + if(FFmpeg_${component}_FOUND) + target_link_libraries( + FFmpeg::FFmpeg + INTERFACE "FFmpeg::${_FFmpeg_${component}_lower}" + ) + endif() + endforeach() + endif() +endif() diff --git a/cmake/modules/FindFdkAac.cmake b/cmake/modules/FindFdkAac.cmake new file mode 100644 index 000000000000..4f4c1867338b --- /dev/null +++ b/cmake/modules/FindFdkAac.cmake @@ -0,0 +1,134 @@ +#[=======================================================================[.rst: +FindFdkAac +---------- + +Finds the Fraunhofer FDK AAC library (libfdk-aac). + +FDK-AAC is used in two distinct ways in Mixxx: +``Encoding in EncoderFdkAac`` + which loads libfdk-aac dynamically the DLL/dylib + which is bundled in the Windows and macOS packages +``FFmpeg AAC decoding in SoundSourceFFmpeg`` + prefer this decoder over FFmpeg's built-in aac decoder + and it's used for AAC encoded STEM files. + +Imported Targets +^^^^^^^^^^^^^^^^ + +``FdkAac::FdkAac`` + The FDK-AAC library. + +Result Variables +^^^^^^^^^^^^^^^^ + +``FdkAac_FOUND`` + True if the system has the FDK-AAC library. +``FdkAac_INCLUDE_DIRS`` + Include directories needed to use FDK-AAC (may be empty when headers + are absent — only required when compiling against FDK-AAC directly). +``FdkAac_LIBRARIES`` + Libraries needed to link to FDK-AAC. +``FdkAac_DEFINITIONS`` + Compile definitions needed to use FDK-AAC. +``FdkAac_DLL`` + (Windows only) Full path to ``fdk-aac.dll``, suitable for install(). + +Cache Variables +^^^^^^^^^^^^^^^ + +``FdkAac_INCLUDE_DIR`` + The directory containing ``fdk-aac/aacenc_lib.h`` (optional). +``FdkAac_LIBRARY`` + The FDK-AAC link library (``.dylib`` / ``.so`` / ``.a`` / ``.lib``). +``FdkAac_DLL`` + (Windows only) Path to ``fdk-aac.dll``. + +#]=======================================================================] + +find_package(PkgConfig QUIET) +if(PkgConfig_FOUND) + pkg_check_modules(PC_FdkAac QUIET fdk-aac) +endif() + +find_path( + FdkAac_INCLUDE_DIR + NAMES fdk-aac/aacenc_lib.h + HINTS ${PC_FdkAac_INCLUDE_DIRS} + DOC "FDK-AAC include directory" +) +mark_as_advanced(FdkAac_INCLUDE_DIR) + +find_library( + FdkAac_LIBRARY + NAMES fdk-aac libfdk-aac + HINTS ${PC_FdkAac_LIBRARY_DIRS} + DOC "FDK-AAC library" +) +mark_as_advanced(FdkAac_LIBRARY) + +# On Windows find_library() returns the .lib import library. +# Locate the companion .dll separately so callers can install it. +if(WIN32) + find_file( + FdkAac_DLL + NAMES fdk-aac.dll + HINTS ${PC_FdkAac_LIBRARY_DIRS} + PATH_SUFFIXES ${CMAKE_INSTALL_BINDIR} bin + DOC "FDK-AAC DLL (Windows runtime)" + ) + mark_as_advanced(FdkAac_DLL) +endif() + +if(DEFINED PC_FdkAac_VERSION AND NOT PC_FdkAac_VERSION STREQUAL "") + set(FdkAac_VERSION "${PC_FdkAac_VERSION}") +endif() + +include(FindPackageHandleStandardArgs) +# Only the library is required. Headers are optional as no Mixxx code includes +# FDK-AAC headers directly, so the include dir may legitimately be absent. +find_package_handle_standard_args( + FdkAac + REQUIRED_VARS FdkAac_LIBRARY + VERSION_VAR FdkAac_VERSION +) + +if(FdkAac_FOUND) + set(FdkAac_LIBRARIES "${FdkAac_LIBRARY}") + set(FdkAac_INCLUDE_DIRS "${FdkAac_INCLUDE_DIR}") + set(FdkAac_DEFINITIONS ${PC_FdkAac_CFLAGS_OTHER}) + + if(NOT TARGET FdkAac::FdkAac) + if(WIN32 AND FdkAac_DLL) + # Create a SHARED IMPORTED target so $ can discover + # fdk-aac.dll automatically. The contract is: + # IMPORTED_LOCATION = the .dll (runtime artifact) + # IMPORTED_IMPLIB = the .lib (link artifact) + add_library(FdkAac::FdkAac SHARED IMPORTED) + set_target_properties( + FdkAac::FdkAac + PROPERTIES + IMPORTED_LOCATION "${FdkAac_DLL}" + IMPORTED_IMPLIB "${FdkAac_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${FdkAac_DEFINITIONS}" + ) + else() + # On non-Windows UNKNOWN IMPORTED means the normal shared/static lib. + # On Windows with no DLL found it links fine but $ + # will not pick it up (DLL must be deployed manually). + add_library(FdkAac::FdkAac UNKNOWN IMPORTED) + set_target_properties( + FdkAac::FdkAac + PROPERTIES + IMPORTED_LOCATION "${FdkAac_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${FdkAac_DEFINITIONS}" + ) + endif() + + if(FdkAac_INCLUDE_DIR) + set_target_properties( + FdkAac::FdkAac + PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${FdkAac_INCLUDE_DIR}" + ) + endif() + endif() +endif() diff --git a/cmake/modules/FindOboe.cmake b/cmake/modules/FindOboe.cmake new file mode 100644 index 000000000000..d12ef492639f --- /dev/null +++ b/cmake/modules/FindOboe.cmake @@ -0,0 +1,71 @@ +#[=======================================================================[.rst: +FindOboe +-------- + +Finds the Oboe library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``Oboe::Oboe`` + The Oboe library + +#]=======================================================================] + +# Prefer finding the libraries from pkgconfig rather than find_library. This is +# required to build with PipeWire's reimplementation of the Oboe library. +# +# This also enables using PortAudio with the Oboe port in vcpkg. That only +# builds OboeWeakAPI (not the Oboe server) which dynamically loads the real +# Oboe library and forwards API calls to it. OboeWeakAPI requires linking `dl` +# in addition to Oboe, as specified in the pkgconfig file in vcpkg. +find_package(PkgConfig QUIET) +if(PkgConfig_FOUND) + pkg_check_modules(Oboe Oboe) +endif() + +find_path( + Oboe_INCLUDE_DIR + NAMES oboe/Oboe.h + HINTS ${PC_Oboe_INCLUDE_DIRS} + DOC "Oboe include directory" +) +mark_as_advanced(Oboe_INCLUDE_DIR) + +find_library( + Oboe_LIBRARY + NAMES oboe + HINTS ${PC_Oboe_LIBRARY_DIRS} + DOC "Oboe library" +) +mark_as_advanced(Oboe_LIBRARY) + +if(DEFINED PC_Oboe_VERSION AND NOT PC_Oboe_VERSION STREQUAL "") + set(Oboe_VERSION "${PC_Oboe_VERSION}") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + Oboe + REQUIRED_VARS Oboe_LIBRARY Oboe_INCLUDE_DIR + VERSION_VAR Oboe_VERSION +) + +if(Oboe_FOUND) + set(Oboe_LIBRARIES "${Oboe_LIBRARY}") + set(Oboe_INCLUDE_DIRS "${Oboe_INCLUDE_DIR}") + set(Oboe_DEFINITIONS ${PC_Oboe_CFLAGS_OTHER}) + + if(NOT TARGET Oboe::Oboe) + add_library(Oboe::Oboe UNKNOWN IMPORTED) + set_target_properties( + Oboe::Oboe + PROPERTIES + IMPORTED_LOCATION "${Oboe_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${PC_Oboe_CFLAGS_OTHER}" + INTERFACE_INCLUDE_DIRECTORIES "${Oboe_INCLUDE_DIR}" + ) + endif() +endif() diff --git a/cmake/modules/FindPipeWire.cmake b/cmake/modules/FindPipeWire.cmake new file mode 100644 index 000000000000..6fd709c8ca5c --- /dev/null +++ b/cmake/modules/FindPipeWire.cmake @@ -0,0 +1,42 @@ +#[=======================================================================[.rst: +FindPipeWire +-------- + +Finds the PipeWire library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``PipeWire::PipeWire`` + The PipeWire library + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``PipeWire_FOUND`` + True if the system has the PipeWire library. + +#]=======================================================================] + +find_package(PkgConfig REQUIRED) +pkg_check_modules(PIPEWIRE QUIET IMPORTED_TARGET libpipewire-0.3) + +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args( + PipeWire + REQUIRED_VARS PIPEWIRE_FOUND + VERSION_VAR PIPEWIRE_VERSION +) + +if(PipeWire_FOUND AND NOT TARGET PipeWire::PipeWire) + add_library(PipeWire::PipeWire INTERFACE IMPORTED) + set_target_properties( + PipeWire::PipeWire + PROPERTIES INTERFACE_LINK_LIBRARIES PkgConfig::PIPEWIRE + ) +endif() diff --git a/cmake/modules/FindPortAudio.cmake b/cmake/modules/FindPortAudio.cmake index cbd5377f51b1..c92beafe090a 100644 --- a/cmake/modules/FindPortAudio.cmake +++ b/cmake/modules/FindPortAudio.cmake @@ -110,6 +110,17 @@ if(PortAudio_FOUND) PROPERTY INTERFACE_LINK_LIBRARIES JACK::jack ) endif() + if(CMAKE_SYSTEM_NAME STREQUAL Android) + find_package(Oboe) + if(NOT (OBOE_FOUND)) + message(FATAL_ERROR "Oboe: not found") + endif() + set_property( + TARGET PortAudio::PortAudio + APPEND + PROPERTY INTERFACE_LINK_LIBRARIES Oboe::Oboe + ) + endif() endif() if(PortAudio_ALSA_H) target_compile_definitions(PortAudio::PortAudio INTERFACE PA_USE_ALSA) diff --git a/cmake/modules/FindSQLite3.cmake b/cmake/modules/FindSQLite3.cmake new file mode 100644 index 000000000000..4bf1d91dfbea --- /dev/null +++ b/cmake/modules/FindSQLite3.cmake @@ -0,0 +1,103 @@ +#[=======================================================================[.rst: +FindSQLite3 +----------- + +Finds the SQLite3 library. + +vcpkg uses an ``unofficial-`` prefix for ports where upstream does not ship +CMake config files. This module tries vcpkg's ``unofficial-sqlite3`` first, +then falls back to CMake's built-in ``FindSQLite3`` module. + +Imported Targets +^^^^^^^^^^^^^^^^ + +``SQLite::SQLite3`` + The SQLite3 library + +Result Variables +^^^^^^^^^^^^^^^^ + +``SQLite3_FOUND`` + True if the system has the SQLite3 library. +``SQLite3_INCLUDE_DIRS`` + Include directories needed to use SQLite3. +``SQLite3_VERSION`` + The version of SQLite3 found. + +#]=======================================================================] + +# Avoid repeated processing +if(TARGET SQLite::SQLite3) + set(SQLite3_FOUND TRUE) + return() +endif() + +# Try vcpkg's unofficial-sqlite3 package first (CONFIG mode only) +# vcpkg prefixes packages with "unofficial-" when upstream doesn't provide +# CMake config files. These packages expose only a target for linking; +# so we extract the version from sqlite3.h below. +find_package(unofficial-sqlite3 CONFIG QUIET) + +if(TARGET unofficial::sqlite3::sqlite3) + # Extract the include directory from the imported target + get_target_property( + SQLite3_INCLUDE_DIR + unofficial::sqlite3::sqlite3 + INTERFACE_INCLUDE_DIRECTORIES + ) + if(SQLite3_INCLUDE_DIR) + # INTERFACE_INCLUDE_DIRECTORIES may be a list; use the first entry + list(GET SQLite3_INCLUDE_DIR 0 SQLite3_INCLUDE_DIR) + endif() + + # Detect version from sqlite3.h + if(SQLite3_INCLUDE_DIR AND EXISTS "${SQLite3_INCLUDE_DIR}/sqlite3.h") + file( + STRINGS + "${SQLite3_INCLUDE_DIR}/sqlite3.h" + _sqlite3_version_str + REGEX "SQLITE_VERSION[\t ]+\"" + LIMIT_COUNT 1 + ) + string(REGEX MATCH "\"([^\"]+)\"" _unused "${_sqlite3_version_str}") + set(SQLite3_VERSION "${CMAKE_MATCH_1}") + unset(_sqlite3_version_str) + endif() + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args( + SQLite3 + REQUIRED_VARS SQLite3_INCLUDE_DIR + VERSION_VAR SQLite3_VERSION + ) + + if(SQLite3_FOUND AND NOT TARGET SQLite::SQLite3) + # Use an IMPORTED INTERFACE target instead of ALIAS so that vcpkg's + # vcpkg-cmake-wrapper.cmake can call set_target_properties() on it. + # ALIAS targets do not support set_target_properties(), which causes + # the Android (and potentially other) vcpkg builds to fail. + add_library(SQLite::SQLite3 INTERFACE IMPORTED GLOBAL) + target_link_libraries( + SQLite::SQLite3 + INTERFACE unofficial::sqlite3::sqlite3 + ) + set(SQLite3_INCLUDE_DIRS "${SQLite3_INCLUDE_DIR}") + endif() + + if(SQLite3_FOUND AND TARGET unofficial::sqlite3::sqlite3) + # Ensure that the actual library is linked, not just INTERFACE + get_target_property( + _sqlite3_libs + unofficial::sqlite3::sqlite3 + INTERFACE_LINK_LIBRARIES + ) + # _sqlite3_libs should contain the actual library, but on Android (static linked build) it may not. + # Force linking the real library if needed. + if(_sqlite3_libs) + target_link_libraries(SQLite::SQLite3 INTERFACE ${_sqlite3_libs}) + endif() + endif() +else() + # If VCPKG's unofficial-sqlite3 package is not found, fall back to CMake's built-in FindSQLite3 module + include("${CMAKE_ROOT}/Modules/FindSQLite3.cmake") +endif() diff --git a/lib/qm-dsp/base/Pitch.cpp b/lib/qm-dsp/base/Pitch.cpp index 2b8fd8b9bdb8..9c45d61abe74 100644 --- a/lib/qm-dsp/base/Pitch.cpp +++ b/lib/qm-dsp/base/Pitch.cpp @@ -32,13 +32,8 @@ Pitch::getPitchForFrequency(float frequency, { float p = 12.0 * (log(frequency / (concertA / 2.0)) / log(2.0)) + 57.0; - int midiPitch = int(p + 0.00001); + int midiPitch = static_cast(p + 0.5); float centsOffset = (p - midiPitch) * 100.0; - - if (centsOffset >= 50.0) { - midiPitch = midiPitch + 1; - centsOffset = -(100.0 - centsOffset); - } if (centsOffsetReturn) *centsOffsetReturn = centsOffset; return midiPitch; diff --git a/lib/xwax/complex.h b/lib/xwax/complex.h new file mode 100644 index 000000000000..841c0e61b8bf --- /dev/null +++ b/lib/xwax/complex.h @@ -0,0 +1,78 @@ +#ifndef COMPLEX_H + +#define COMPLEX_H + +#include + +/* + * 32-bit signed fixed point datatype. The fixed point position depends on the usage context + * and can be the ARM 1.31 format (Q0.31) and others. + */ + +typedef int32_t q31_t; + +/* + * 64-bit signed fixed point datatype. The fixed point position depends on the usage context, + * and can be the ARM 2.62 format (Q1.62) and others. + */ + +typedef int64_t q63_t; + +/* + * A complex type in ARM Q format + */ + +struct complex_q63 { + q63_t re; + q63_t im; +}; + +/* + * A complex type in ARM Q format + */ + +struct complex_q31 { + q31_t re; + q31_t im; +}; + +/* + * Complex multiplication in polar form. + * + * Takes ARM 1.31 format as z0 and z1 and returns ARM 2.62 format. + * + * Imaginary number: i = sqrt(-1) --> i * i = -1 + * + * e^(i*x) = cos(x) + i * sin(x) + * e^(i*y) = cos(y) + i * sin(y) + * + * e^(i*x) * e^(i*y) = ( cos(x) + i * sin(x) ) * ( cos(y) + i * sin(y) ) + * e^(i*x) * e^(i*y) = cos(x) * cos(y) + cos(x) * i * sin(y) + cos(y) * i * sin(x) + i * sin(x) * i * sin(x) + * e^(i*x) * e^(i*y) = cos(x) * cos(y) + cos(x) * i * sin(y) + cos(y) * i * sin(x) - sin(x) * sin(y) + * + * Everything that has an i, belongs to the imaginary part, so we reorder: + * + * e^(i*x) * e^(i*y) = cos(x) * cos(y) - sin(x) * sin(y) + cos(x) * i * sin(y) + cos(y) * i * sin(x) + * { real } { imag } + */ + +static inline struct complex_q63 complex_q31_mul(struct complex_q31 z0, + struct complex_q31 z1) +{ + q63_t re = (q63_t)z0.re * z1.re - (q63_t)z0.im * z1.im; + q63_t im = (q63_t)z0.re * z1.im + (q63_t)z1.re * z0.im; + + return (struct complex_q63) { re, im }; +} + +/* + * Converts a complex_q31 into its complex conjugate + */ + +static inline struct complex_q31 complex_q31_conj(struct complex_q31 z) +{ + z.im = -z.im; + return z; +} + +#endif /* end of include guard COMPLEX_H */ diff --git a/lib/xwax/filters.c b/lib/xwax/filters.c new file mode 100644 index 000000000000..c76b5138d533 --- /dev/null +++ b/lib/xwax/filters.c @@ -0,0 +1,394 @@ +#include "filters.h" +#include "fmatrix.h" + +#include +#include +#define _USE_MATH_DEFINES +#include +#include +#include + +static inline double clamp_to_int32(double x) +{ + if (x > INT_MAX) { + x = INT_MAX; + } else if (x < INT_MIN) { + x = INT_MIN; + } + + return x; +} + +static inline void normalize_coeffs(double *coeffs, size_t N) +{ + double sum = 0.0; + + for (size_t i = 0; i < N; i++) + sum += coeffs[i]; + + for (size_t i = 0; i < N; i++) + coeffs[i] /= sum; +} + +/* + * Initializes the exponential weighted moving average filter. + */ + +void ewma_init(struct ewma_filter *f, const double alpha) +{ + if (!f) { + errno = EINVAL; + perror(__func__); + return; + } + + f->alpha = alpha; + f->y_old = 0; +} + +/* + * Initializes the exponential weighted moving average filter. + * Takes the carrier frequency and sampling rate into account. + */ + +void ewma_init_adaptive(struct ewma_filter *f, double k, double f_carrier, + double fs) +{ + if (!f) { + errno = EINVAL; + perror(__func__); + return; + } + + double tau = k / f_carrier; /* fraction of carrier frequency */ + f->alpha = 1.0 - exp(-1.0 / (fs * tau)); + f->y_old = 0.0; +} + +/* + * Computes an exponential weighted moving average with the possibility to weight newly added + * values with a factor alpha. + */ + +int ewma(struct ewma_filter *f, const int x) +{ + if (!f) { + errno = EINVAL; + perror(__func__); + return -EINVAL; + } + + int y = f->alpha * x + (1 - f->alpha) * f->y_old; + f->y_old = y; + + return y; +} + +/* + * Initializes the derivative filter. + */ + +void derivative_init(struct differentiator *f) +{ + if (!f) { + errno = EINVAL; + perror(__func__); + return; + } + + f->x_old = 0; +} + +/* + * Computes a simple derivative, i.e. the slope of the input signal without gain compensation. + */ + +int derivative(struct differentiator *f, const int x) +{ + int y = x - f->x_old; + f->x_old = x; + + return y; +} + +/* + * Initializes the RMS filter + */ + +void rms_init(struct root_mean_square *f, const float alpha) +{ + if (!f) { + errno = EINVAL; + perror(__func__); + return; + } + + f->squared_old = 0; + f->alpha = alpha; +} + +/* + * Computes the RMS value over a running sum. + * The 1.0 > alpha > 0 determines the smoothness of the result: + */ + +int rms(struct root_mean_square *f, const int x) +{ + if (!f) { + errno = EINVAL; + perror(__func__); + return -EINVAL; + } + + /* Compute squared value */ + double squared = (double)x * (double)x; + + /* Apply EMA filter to squared values */ + f->squared_old = (1.0 - f->alpha) * f->squared_old + f->alpha * squared; + + /* Take square root at the end */ + return (int)lround(sqrt(f->squared_old)); +} + +/* + * Computes the coefficients for a Savitzky-Golay filter at runtime + * by using a Vandermonde matrix: + * + * | (-M)^0 (-M)^1 (-M)^N | + * | : : : | + * A = | 0^0 0^1 0^N | + * | : : : | + * | M^0 M^1 M^N | + * + * see https://c.mql5.com/forextsd/forum/147/sgfilter.pdf + * + * where M is the half-width of the sample window + * and N is the order of the polynomial + * + * N + * p(n) = Σ a_k * n^k + * k=0 + */ + +static double *savgol_gen_coeffs(size_t window_size, size_t polyorder) +{ + if (window_size % 2 == 0 || polyorder >= window_size) { + errno = EINVAL; + perror(__func__); + return NULL; + } + + struct fmatrix *A = NULL; + struct fmatrix *AT = NULL; + struct fmatrix *B = NULL; + struct fmatrix *B_inv = NULL; + struct fmatrix *H = NULL; + + size_t M = (window_size - 1) / 2; + size_t N = window_size; + size_t I = polyorder + 1; + double *coeff = NULL; + + /* A: (N x I) */ + + A = fmat_alloc(N, I); + if (!A) + goto out; + + /* + * Fill the n x i Vandermonde matrix + * + * a_n,i = n^i, -M <= n <= M + * i = 0, 1, ..., N + */ + + for (size_t n = 0; n < N; n++) { + int base = (int)n - (int)M; // May be negative + + for (size_t i = 0; i < I; i++) { + fmat_set(A, n, i, pow((double)base, (double)i)); + } + } + + /* AT = A^T */ + + AT = fmat_trans(NULL, A); + if (!AT) + goto out; + + /* B = A^T x A */ + + B = fmat_mul(NULL, AT, A); + if (!B) + goto out; + + /* B_inv */ + + B_inv = fmat_inv(NULL, B); + if (!B_inv) + goto out; + + /* H = B_inv * AT */ + + H = fmat_mul(NULL, B_inv, AT); + if (!H) + goto out; + + /* Allocate coefficient array */ + + coeff = calloc(window_size, sizeof(double)); + if (!coeff) + goto out; + + /* The coefficients can be obtained by extracting row 0 of H. */ + + for (size_t i = 0; i < N; i++) + coeff[i] = fmat_get(H, 0, i); + + normalize_coeffs(coeff, N); + + /* Cleanup */ + +out: + fmat_free(A); + fmat_free(AT); + fmat_free(B); + fmat_free(B_inv); + fmat_free(H); + + return coeff; +} + +/* + * Creates a Savitzky-Golay filter at runtime given a windows size N + * and filter order. The sample window size determines the delay of the filter. + * The delay of the filter equals the half width M of the sample window. + */ + +struct savitzky_golay *savgol_create(size_t window_size, size_t polyorder) +{ + if (window_size < 3 || window_size % 2 == 0 || polyorder >= window_size) { + errno = EINVAL; + if (polyorder >= window_size) + fprintf(stderr, + "%s: The filter order needs to be less than the window size\n", + __func__); + if (window_size % 2 == 0) + fprintf(stderr, "%s: The window size N must be odd\n", __func__); + goto error; + } + + struct savitzky_golay *f = malloc(sizeof(*f)); + if (!f) + goto error; + + f->window_size = window_size; + f->polyorder = polyorder; + f->M = (window_size - 1) / 2; /* Determine the half width */ + + f->delayline = rb_alloc(window_size, sizeof(int)); + if (!f->delayline) + goto error_rb_alloc; + + f->coeff = savgol_gen_coeffs(window_size, polyorder); + if (!f->coeff) + goto error_coeff; + + return f; + +error_coeff: + rb_free(f->delayline); +error_rb_alloc: + free(f); +error: + perror(__func__); + + return NULL; +} + +/* + * Destroys a Savitzky-Golay filter object + */ + +void savgol_destroy(struct savitzky_golay *f) +{ + if (!f) + return; + + rb_free(f->delayline); + free(f->coeff); + free(f); +} + +/* + * Applies a Savitzky-Golay filter to an input sample. The filter achieves + * optimal smoothing of signal using least-squares polynomials. + */ + +int savgol(struct savitzky_golay *f, int x) +{ + if (!f) { + errno = EINVAL; + perror(__func__); + return 0; + } + + rb_push(f->delayline, &x); + + double y = 0; + int *sample = NULL; /* Sample pointer */ + + for (size_t i = 0; i < f->window_size; i++) { + sample = (int *)rb_at(f->delayline, i); + y += f->coeff[i] * *sample; + } + + /* Clamp as last security measure */ + + y = clamp_to_int32(y); + + return (int)lround(y); +} + +/* + * Implements a simple highpass as rumble filter + */ + +void rhpf_init(struct rumble_filter *f, unsigned int fs, double fc) +{ + if (!f) { + errno = EINVAL; + perror(__func__); + return; + } + + f->fs = (double)fs; + f->fc = fc; + + double RC = 1.0 / (2.0 * M_PI * fc); + double dt = 1.0 / fs; + f->alpha = RC / (RC + dt); /* from standard 1st order HP formula */ + + f->x_old = 0.0; + f->y_old = 0.0; +} + +/* + * Processes a sample in the rumble filter + */ + +int rhpf_process(struct rumble_filter *f, int x) +{ + if (!f) { + errno = EINVAL; + perror(__func__); + return 0; + } + + double y = f->alpha * (f->y_old + x - f->x_old); + + f->x_old = x; + f->y_old = y; + + return (int)lround(y); +} diff --git a/lib/xwax/filters.h b/lib/xwax/filters.h new file mode 100644 index 000000000000..0b4627ad7308 --- /dev/null +++ b/lib/xwax/filters.h @@ -0,0 +1,63 @@ +#ifndef FILTERS_H + +#define FILTERS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ringbuffer.h" + +struct ewma_filter { + double alpha; + int y_old; +}; + +void ewma_init(struct ewma_filter *f, const double alpha); +void ewma_init_adaptive(struct ewma_filter *f, double k, double f_carrier, + double fs); +int ewma(struct ewma_filter *f, const int x); + +struct differentiator { + int x_old; +}; + +void derivative_init(struct differentiator *f); +int derivative(struct differentiator *f, const int x); + +struct root_mean_square { + double alpha; + double squared_old; +}; + +void rms_init(struct root_mean_square *f, const float alpha); +int rms(struct root_mean_square *f, const int x); + +struct savitzky_golay { + size_t window_size; /* Window size */ + size_t M; /* Half width */ + size_t polyorder; /* Polynomial order */ + double *coeff; /* Filter coefficients */ + struct ringbuffer *delayline; /* Sample ringbuffer */ +}; + +struct savitzky_golay *savgol_create(size_t window_size, size_t polyorder); +void savgol_destroy(struct savitzky_golay *f); +int savgol(struct savitzky_golay *f, int x); + +struct rumble_filter { + double fc; /* Cutoff frequency */ + double fs; /* Sample rate */ + double alpha; /* Filter coefficient */ + double x_old; /* x[n-1] */ + double y_old; /* y[n-1] */ +}; + +void rhpf_init(struct rumble_filter *f, unsigned int fs, double fc); +int rhpf_process(struct rumble_filter *f, int x); + +#ifdef __cplusplus +} +#endif + +#endif /* end of include guard FILTERS_H */ diff --git a/lib/xwax/fmatrix.c b/lib/xwax/fmatrix.c new file mode 100644 index 000000000000..94ad4dfd706d --- /dev/null +++ b/lib/xwax/fmatrix.c @@ -0,0 +1,409 @@ +#include "fmatrix.h" + +#include +#include +#include +#include +#include + +struct fmatrix { + size_t cols, rows; + fval_t *data; +}; + +struct fmatrix *fmat_alloc(const size_t rows, const size_t cols) +{ + if (!rows || !cols) { + errno = EINVAL; + perror(__func__); + return NULL; + } + + /* Allocate the struct */ + struct fmatrix *m = malloc(sizeof(struct fmatrix)); + if (!m) { + perror(__func__); + goto error; + } + + m->cols = cols; + m->rows = rows; + m->data = NULL; + + /* Allocate the rows */ + m->data = calloc(rows * cols, sizeof(fval_t)); + if (!m->data) { + perror(__func__); + goto error_data; + } + + return m; + +error_data: + free(m); +error: + return NULL; +} + +void fmat_free(struct fmatrix *m) +{ + if (!m) + return; + + if (m->data) + free(m->data); + + free(m); +} + +void fmat_set(struct fmatrix *m, size_t row, size_t col, fval_t val) +{ + if (!m || row >= m->rows || col >= m->cols) { + errno = EINVAL; + perror(__func__); + return; + } + + m->data[m->cols * row + col] = val; +} + +inline void fmat_set_unsafe(struct fmatrix *m, size_t row, size_t col, fval_t val) +{ + m->data[m->cols * row + col] = val; +} + +fval_t fmat_get(const struct fmatrix *m, size_t row, size_t col) +{ + if (!m || row >= m->rows || col >= m->cols) { + errno = EINVAL; + perror(__func__); + return 0; + } + + return m->data[m->cols * row + col]; +} + +inline fval_t fmat_get_unsafe(const struct fmatrix *m, size_t row, size_t col) +{ + return m->data[m->cols * row + col]; +} + +struct fmatrix *fmat_mul(struct fmatrix *dest, const struct fmatrix *a, const struct fmatrix *b) +{ + if (!a || !b || a->cols != b->rows) { + errno = EINVAL; + perror(__func__); + return NULL; + } + + if (dest) { + if (dest->rows != a->rows || dest->cols != b->cols) { + errno = EINVAL; + perror(__func__); + return NULL; + } + } else { + dest = fmat_alloc(a->rows, b->cols); + if (!dest) + return NULL; + } + + for (size_t i = 0; i < a->rows; i++) { + for (size_t j = 0; j < b->cols; j++) { + fval_t sum = 0; + + for (size_t k = 0; k < a->cols; k++) + sum += FMAT_GET(a, i, k) * FMAT_GET(b, k, j); + + FMAT_SET(dest, i, j, sum); + } + } + + return dest; +} + +struct fmatrix *fmat_trans(struct fmatrix *dest, const struct fmatrix *src) +{ + if (!src) { + errno = EINVAL; + perror(__func__); + return NULL; + } + + if (dest) { + if (dest->rows != src->cols || dest->cols != src->rows) { + errno = EINVAL; + perror(__func__); + return NULL; + } + } else { + dest = fmat_alloc(src->cols, src->rows); + if (!dest) + return NULL; + } + + for (size_t r = 0; r < src->rows; r++) + for (size_t c = 0; c < src->cols; c++) + FMAT_SET(dest, c, r, FMAT_GET(src, r, c)); + + return dest; +} + +static int fmat_lup_decompose(struct fmatrix *a, size_t *perm, double *sign) +{ + const size_t n = a->rows; + const double eps = 1e-12; + + for (size_t i = 0; i < n; ++i) + perm[i] = i; + + *sign = 1.0; + + for (size_t k = 0; k < n; ++k) { + size_t pivot_row = k; + double max_abs = fabs(a->data[k * n + k]); + + for (size_t i = k + 1; i < n; ++i) { + double v = fabs(a->data[i * n + k]); + if (v > max_abs) { + max_abs = v; + pivot_row = i; + } + } + + if (max_abs < eps) + return -1; + + if (pivot_row != k) { + for (size_t j = 0; j < n; ++j) { + double t = a->data[k * n + j]; + a->data[k * n + j] = a->data[pivot_row * n + j]; + a->data[pivot_row * n + j] = t; + } + size_t tp = perm[k]; + perm[k] = perm[pivot_row]; + perm[pivot_row] = tp; + *sign = -*sign; + } + + double pivot = a->data[k * n + k]; + + for (size_t i = k + 1; i < n; ++i) { + double *row_i = &a->data[i * n]; + double *row_k = &a->data[k * n]; + row_i[k] /= pivot; + double lik = row_i[k]; + for (size_t j = k + 1; j < n; ++j) + row_i[j] -= lik * row_k[j]; + } + } + + return 0; +} + +static void fmat_lu_solve(const struct fmatrix *lu, const size_t *perm, const double *b, double *x) +{ + const size_t n = lu->rows; + + for (size_t i = 0; i < n; ++i) { + double sum = b[perm[i]]; + for (size_t j = 0; j < i; ++j) + sum -= lu->data[i * n + j] * x[j]; + x[i] = sum; + } + + for (size_t i = n; i-- > 0;) { + double sum = x[i]; + for (size_t j = i + 1; j < n; ++j) + sum -= lu->data[i * n + j] * x[j]; + x[i] = sum / lu->data[i * n + i]; + } +} + +struct fmatrix *fmat_inv_lup(struct fmatrix *dest, const struct fmatrix *src) +{ + struct fmatrix *lu = NULL; + int dest_allocated = 0; + size_t *perm = NULL; + double *rhs = NULL; + double *sol = NULL; + int error = -1; + double sign; + + if (!src || src->rows != src->cols) { + errno = EINVAL; + goto out; + } + + size_t n = src->rows; + + if (dest) { + if (dest->rows != n || dest->cols != n) { + errno = EINVAL; + goto out; + } + } else { + dest = fmat_alloc(n, n); + if (!dest) + goto out; + dest_allocated = 1; + } + + lu = fmat_alloc(n, n); + perm = malloc(n * sizeof(*perm)); + rhs = malloc(n * sizeof(*rhs)); + sol = malloc(n * sizeof(*sol)); + if (!lu || !perm || !rhs || !sol) + goto out; + + memcpy(lu->data, src->data, n * n * sizeof(double)); + + if (fmat_lup_decompose(lu, perm, &sign) != 0) { + errno = EINVAL; + goto out; + } + + for (size_t col = 0; col < n; ++col) { + memset(rhs, 0, n * sizeof(*rhs)); + rhs[col] = 1.0; + fmat_lu_solve(lu, perm, rhs, sol); + + for (size_t row = 0; row < n; ++row) + dest->data[row * n + col] = sol[row]; + } + + error = 0; + +out: + free(sol); + free(rhs); + free(perm); + fmat_free(lu); + + if (error) { + if (dest_allocated) + fmat_free(dest); + perror(__func__); + return NULL; + } + + return dest; +} + +static int fmat_inv_2x2(const struct fmatrix *src, struct fmatrix *dest) +{ + const double a = src->data[0]; + const double b = src->data[1]; + const double c = src->data[2]; + const double d = src->data[3]; + + const double det = a * d - b * c; + if (fabs(det) < 1e-12) + return -1; + + const double inv_det = 1.0 / det; + + dest->data[0] = d * inv_det; + dest->data[1] = -b * inv_det; + dest->data[2] = -c * inv_det; + dest->data[3] = a * inv_det; + + return 0; +} + +static int fmat_inv_3x3(const struct fmatrix *src, struct fmatrix *dest) +{ + const double *m = src->data; + + const double a = m[0], b = m[1], c = m[2]; + const double d = m[3], e = m[4], f = m[5]; + const double g = m[6], h = m[7], i = m[8]; + + const double A = e * i - f * h; + const double B = -(d * i - f * g); + const double C = d * h - e * g; + const double D = -(b * i - c * h); + const double E = a * i - c * g; + const double F = -(a * h - b * g); + const double G = b * f - c * e; + const double H = -(a * f - c * d); + const double I = a * e - b * d; + + const double det = a * A + b * B + c * C; + if (fabs(det) < 1e-12) + return -1; + + const double inv_det = 1.0 / det; + + dest->data[0] = A * inv_det; + dest->data[1] = D * inv_det; + dest->data[2] = G * inv_det; + dest->data[3] = B * inv_det; + dest->data[4] = E * inv_det; + dest->data[5] = H * inv_det; + dest->data[6] = C * inv_det; + dest->data[7] = F * inv_det; + dest->data[8] = I * inv_det; + + return 0; +} + +struct fmatrix *fmat_inv(struct fmatrix *dest, const struct fmatrix *src) +{ + if (!src || src->rows != src->cols) { + errno = EINVAL; + perror(__func__); + return NULL; + } + + const size_t n = src->rows; + + if (dest) { + if (dest->rows != n || dest->cols != n) { + errno = EINVAL; + perror(__func__); + + return NULL; + } + } else { + dest = fmat_alloc(n, n); + + if (!dest) + return NULL; + } + + if (n == 1) { + if (fabs(src->data[0]) < 1e-12) { + fprintf(stderr, "%s: matrix is singular\n", __func__); + goto error; + } + + dest->data[0] = 1.0 / src->data[0]; + + return dest; + } + + if (n == 2) { + if (fmat_inv_2x2(src, dest) != 0) { + fprintf(stderr, "%s: matrix is singular\n", __func__); + goto error; + } + + return dest; + } + + if (n == 3) { + if (fmat_inv_3x3(src, dest) != 0) { + fprintf(stderr, "%s: matrix is singular\n", __func__); + goto error; + } + + return dest; + } + + return fmat_inv_lup(dest, src); + +error: + fmat_free(dest); + return NULL; +} diff --git a/lib/xwax/fmatrix.h b/lib/xwax/fmatrix.h new file mode 100644 index 000000000000..3eb8d881740c --- /dev/null +++ b/lib/xwax/fmatrix.h @@ -0,0 +1,46 @@ +#ifndef FMATRIX_H +#define FMATRIX_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Scalar type for matrix elements */ +typedef double fval_t; + +#ifdef DEBUG +#define FMAT_SET(m, row, col, val) fmat_set(m, row, col, val) +#define FMAT_GET(m, row, col) fmat_get(m, row, col) +#else +#define FMAT_SET(m, row, col, val) fmat_set_unsafe(m, row, col, val) +#define FMAT_GET(m, row, col) fmat_get_unsafe(m, row, col) +#endif + +/* Allocate an empty floating-point matrix */ +struct fmatrix *fmat_alloc(const size_t rows, const size_t cols); +/* Delete a floating-point matrix */ +void fmat_free(struct fmatrix *m); + +/* Set a single field of a floating-point matrix (slower safe version for testing) */ +void fmat_set(struct fmatrix *m, size_t row, size_t col, fval_t val); +/* Set a single field of a floating-point matrix (unsafe inlined version) */ +void fmat_set_unsafe(struct fmatrix *m, size_t row, size_t col, fval_t val); +/* Get a single field of a floating-point matrix (slower safe version for testing) */ +fval_t fmat_get(const struct fmatrix *m, size_t row, size_t col); +/* Get a single field of a floating-point matrix (unsafe inlined version) */ +fval_t fmat_get_unsafe(const struct fmatrix *m, size_t row, size_t col); + +/* Multiply two floating-point matrices */ +struct fmatrix *fmat_mul(struct fmatrix *dest, const struct fmatrix *a, const struct fmatrix *b); +/* Transpose a floating-point matrix */ +struct fmatrix *fmat_trans(struct fmatrix *dest, const struct fmatrix *src); +/* Compute the inverse of a floating-point matrix */ +struct fmatrix *fmat_inv(struct fmatrix *dest, const struct fmatrix *src); + +#ifdef __cplusplus +} +#endif + +#endif /* FMATRIX_H */ diff --git a/lib/xwax/lut.c b/lib/xwax/lut.c index d9d16585d240..f137a39f21ed 100644 --- a/lib/xwax/lut.c +++ b/lib/xwax/lut.c @@ -28,7 +28,7 @@ #define HASH_BITS 16 #define HASH(timecode) ((timecode) & ((1 << HASH_BITS) - 1)) -#define NO_SLOT ((unsigned)-1) +#define NO_SLOT ((slot_no_t)-1) /* Initialise an empty hash lookup table to store the given number @@ -74,7 +74,7 @@ void lut_clear(struct lut *lut) } -void lut_push(struct lut *lut, unsigned int timecode) +void lut_push(struct lut *lut, bits_t timecode) { unsigned int hash; slot_no_t slot_no; @@ -91,7 +91,7 @@ void lut_push(struct lut *lut, unsigned int timecode) } -unsigned int lut_lookup(struct lut *lut, unsigned int timecode) +slot_no_t lut_lookup(struct lut *lut, bits_t timecode) { unsigned int hash; slot_no_t slot_no; @@ -107,5 +107,5 @@ unsigned int lut_lookup(struct lut *lut, unsigned int timecode) slot_no = slot->next; } - return (unsigned)-1; + return (slot_no_t)-1; } diff --git a/lib/xwax/lut.h b/lib/xwax/lut.h index 9667705446ab..da98f56b2754 100644 --- a/lib/xwax/lut.h +++ b/lib/xwax/lut.h @@ -20,10 +20,13 @@ #ifndef LUT_H #define LUT_H +#include "types.h" + typedef unsigned int slot_no_t; +typedef unsigned int bits_t; struct slot { - unsigned int timecode; + bits_t timecode; slot_no_t next; /* next slot with the same hash */ }; @@ -36,7 +39,7 @@ struct lut { int lut_init(struct lut *lut, int nslots); void lut_clear(struct lut *lut); -void lut_push(struct lut *lut, unsigned int timecode); -unsigned int lut_lookup(struct lut *lut, unsigned int timecode); +void lut_push(struct lut *lut, bits_t timecode); +unsigned int lut_lookup(struct lut *lut, bits_t timecode); #endif diff --git a/lib/xwax/lut_mk2.c b/lib/xwax/lut_mk2.c new file mode 100644 index 000000000000..560c471aae02 --- /dev/null +++ b/lib/xwax/lut_mk2.c @@ -0,0 +1,136 @@ +#include +#include + +#include "lut_mk2.h" + +/* + * The number of bits to form the hash, which governs the overall size + * of the hash lookup table, and hence the amount of chaining + */ + +#define HASH_BITS 16 + +#define HASH(timecode) ((timecode) & ((1 << HASH_BITS) - 1)) +#define NO_SLOT ((slot_no_t)-1) + +/* + * Hash function that takes all 110-bits of the MK2s into account + */ + +unsigned short HASH110(mk2bits_t *value) { + + /* Simple hash mixing using bit shifts and XORs */ + unsigned short hash = (unsigned short)(value->low ^ (value->low >> 16) ^ (value->low >> 32) ^ + (value->low >> 48)); + hash ^= (unsigned short)(value->high ^ (value->high << 5) ^ (value->high >> 3)); + + /* Final scrambling to improve distribution */ + hash ^= (hash >> 7) ^ (hash << 9); + + return hash; +} + +/* + * Initialise an empty hash lookup table to store the given number * of timecode -> position + * lookups (Traktor MK2 version) + */ + +int lut_init_mk2(struct lut_mk2 *lut, int nslots) +{ + size_t bytes; + int n, hashes; + + hashes = 1 << HASH_BITS; + bytes = sizeof(struct slot_mk2) * nslots + sizeof(slot_no_t) * hashes; + + fprintf(stderr, "Lookup table has %d hashes to %d slots" + " (%d slots per hash, %zuKb)\n", + hashes, nslots, nslots / hashes, bytes / 1024); + + lut->hdr = malloc(sizeof(struct lut_mk2_header)); + if (lut->hdr == NULL) { + perror("malloc"); + return -1; + } + + lut->hdr->magic = MIXXX_LUT_MAGIC; + lut->hdr->major = MIXXX_LUT_MAJOR; + lut->hdr->minor = MIXXX_LUT_MINOR; + + lut->slot = malloc(sizeof(struct slot_mk2) * nslots); + if (lut->slot == NULL) { + perror("malloc"); + return -1; + } + + lut->table = malloc(sizeof(slot_no_t) * hashes); + if (lut->table == NULL) { + perror("malloc"); + return -1; + } + + for (n = 0; n < hashes; n++) + lut->table[n] = NO_SLOT; + + lut->avail = 0; + + return 0; +} + +void lut_clear_mk2(struct lut_mk2 *lut) +{ + if (!lut) + return; + + if (lut->hdr) + free(lut->hdr); + + if (lut->table) + free(lut->table); + + if (lut->slot) + free(lut->slot); +} + +/* + * Traktor MK2 version holding 110-bit integers as timecode + */ + +void lut_push_mk2(struct lut_mk2 *lut, mk2bits_t *timecode) +{ + unsigned int hash; + slot_no_t slot_no; + struct slot_mk2 *slot; + + slot_no = lut->avail++; /* take the next available slot */ + + slot = &lut->slot[slot_no]; + slot->timecode = *timecode; + + hash = HASH110(timecode); + slot->next = lut->table[hash]; + lut->table[hash] = slot_no; +} + +/* + * Traktor MK2 version holding 110-bit integers as timecode + */ + +slot_no_t lut_lookup_mk2(struct lut_mk2 *lut, mk2bits_t *timecode) +{ + unsigned int hash; + slot_no_t slot_no; + struct slot_mk2 *slot; + + hash = HASH110(timecode); + slot_no = lut->table[hash]; + + while (slot_no != NO_SLOT) { + slot = &lut->slot[slot_no]; + if (u128_eq(slot->timecode, *timecode)) + return slot_no; + slot_no = slot->next; + } + + return (slot_no_t)-1; +} diff --git a/lib/xwax/lut_mk2.h b/lib/xwax/lut_mk2.h new file mode 100644 index 000000000000..3785eb4a8889 --- /dev/null +++ b/lib/xwax/lut_mk2.h @@ -0,0 +1,43 @@ +#ifndef LUT_MK2_H + +#define LUT_MK2_H + +#include "lut.h" + +#include + +#define MIXXX_LUT_MAGIC 0x54554c585858494duLL /* MIXXXLUT */ +#define MIXXX_LUT_MAJOR 1 +#define MIXXX_LUT_MINOR 0 + +typedef u128 mk2bits_t; + +/* + * NOTE: Should the member order of any of these structs ever be changed, + * it is imperative that the MIXXX_LUT_MINOR gets incremented !!! + */ + +struct lut_mk2_header { + uint64_t magic; + uint16_t major; + uint16_t minor; +}; + +struct slot_mk2 { + mk2bits_t timecode; + slot_no_t next; /* next slot with the same hash */ +}; + +struct lut_mk2 { + struct lut_mk2_header *hdr; + struct slot_mk2 *slot; + slot_no_t *table, /* hash -> slot lookup */ + avail; /* next available slot */ +}; + +int lut_init_mk2(struct lut_mk2 *lut, int nslots); +void lut_clear_mk2(struct lut_mk2 *lut); +void lut_push_mk2(struct lut_mk2 *lut, mk2bits_t *timecode); +slot_no_t lut_lookup_mk2(struct lut_mk2 *lut, mk2bits_t *timecode); + +#endif /* end of include guard LUT_MK2_H */ diff --git a/lib/xwax/pitch.h b/lib/xwax/pitch.h index c53313b474a0..cb84f0fcd49a 100644 --- a/lib/xwax/pitch.h +++ b/lib/xwax/pitch.h @@ -27,13 +27,13 @@ /* State of the pitch calculation filter */ -struct pitch { +struct pitch_filter { double dt, x, v; }; /* Prepare the filter for observations every dt seconds */ -static inline void pitch_init(struct pitch *p, double dt) +static inline void pitch_init(struct pitch_filter *p, double dt) { p->dt = dt; p->x = 0.0; @@ -46,7 +46,7 @@ static inline void pitch_init(struct pitch *p, double dt) * Because the vinyl uses timestamps, the values for dx are discrete * rather than smooth. */ -static inline void pitch_dt_observation(struct pitch *p, double dx) +static inline void pitch_dt_observation(struct pitch_filter *p, double dx) { double predicted_x, predicted_v, residual_x; @@ -61,11 +61,4 @@ static inline void pitch_dt_observation(struct pitch *p, double dx) p->x -= dx; /* relative to previous */ } -/* Get the pitch after filtering */ - -static inline double pitch_current(struct pitch *p) -{ - return p->v; -} - #endif diff --git a/lib/xwax/pitch_kalman.c b/lib/xwax/pitch_kalman.c index 9f0ce6bceefc..7d15be0396ae 100644 --- a/lib/xwax/pitch_kalman.c +++ b/lib/xwax/pitch_kalman.c @@ -32,18 +32,9 @@ static inline double pow3(double val) * r: variance of dx measurement (tune up if observations are noisier) */ -void pitch_kalman_init(struct pitch_kalman *p, double dt, struct kalman_coeffs stable, - struct kalman_coeffs adjust, struct kalman_coeffs reactive, - struct kalman_coeffs scratch, double adjust_threshold, - double reactive_threshold, double scratch_threshold, bool debug) +void pitch_kalman_init(struct pitch_kalman_filter *p, double dt, struct kalman_coeffs stable, + struct kalman_coeffs scratch, double scratch_threshold, bool debug) { - const bool thresholds_well_ordered = scratch_threshold > reactive_threshold && reactive_threshold > adjust_threshold; - if (!p || !thresholds_well_ordered) { - errno = EINVAL; - perror(__func__); - return; - } - kalman_debug_state = debug; /* Sampling interval */ @@ -68,26 +59,22 @@ void pitch_kalman_init(struct pitch_kalman *p, double dt, struct kalman_coeffs s /* Fixed thresholds for the mode switches */ p->scratch_threshold = scratch_threshold; - p->reactive_threshold = reactive_threshold; - p->adjust_threshold = adjust_threshold; /* Q and R for the different modes */ p->stable = stable; - p->adjust = adjust; - p->reactive = reactive; p->scratch = scratch; /* Initialize as reactive */ - kalman_tune_sensitivity(p, &p->reactive); + kalman_tune_sensitivity(p, &p->scratch); } /* * Feed one observation: in the last dt seconds, position moved by dx */ -void pitch_kalman_update(struct pitch_kalman* p, double dx) +void pitch_kalman_update(struct pitch_kalman_filter* p, double dx) { if (!p) { errno = EINVAL; @@ -185,25 +172,13 @@ void pitch_kalman_update(struct pitch_kalman* p, double dx) kalman_debug("innovation: %+f, ", y); if (y_abs > p->scratch_threshold) { - kalman_debug(" SCRATCH MODE\n"); + kalman_debug(" SCRATCH MODE\n"); kalman_tune_sensitivity(p, &p->scratch); - } else if (y_abs > p->reactive_threshold) { - kalman_debug(" REACTIVE MODE\n"); - kalman_tune_sensitivity(p, &p->reactive); - } else if (y_abs > p->adjust_threshold) { - kalman_debug(" ADJUST MODE\n"); - kalman_tune_sensitivity(p, &p->adjust); } else { kalman_debug("STABLE MODE\n"); kalman_tune_sensitivity(p, &p->stable); } - /* Ensure reactivity and quick decay after standstill */ - - if (fabs(p->Xk[v]) < 5e-2) { - kalman_tune_sensitivity(p, &p->scratch); - } - /* * Innovation covariance: S = H * P_pred * H^T + R * With H = [1 0], this reduces to: S = P_pred[x][x] + R @@ -211,6 +186,10 @@ void pitch_kalman_update(struct pitch_kalman* p, double dx) const double S = P_pred[x][x] + p->coeffs->R; + /* Check for division by zero */ + if (S == 0.0) + return; + /* * Kalman gain: K = P_pred * H^T * S^-1 * diff --git a/lib/xwax/pitch_kalman.h b/lib/xwax/pitch_kalman.h index 50e2da8e28e6..267f6a06d51a 100644 --- a/lib/xwax/pitch_kalman.h +++ b/lib/xwax/pitch_kalman.h @@ -22,8 +22,7 @@ * * Modes: * stable : Low Q and high R for stable playback - * adjust : Medium values for slight pitch changes - * reactive: High Q and low R for high reactivity (scratching) + * scratch : High Q and low R for high reactivity (scratching) */ #ifndef PITCH_KALMAN_H @@ -43,7 +42,7 @@ struct kalman_coeffs { #define KALMAN_COEFFS(q_arg, r_arg) \ (struct kalman_coeffs) {.Q = (q_arg), .R = (r_arg)} -struct pitch_kalman { +struct pitch_kalman_filter { /* * NOTE: In discrete time dt is usually denoted as Ts = 1/Fs, * but xwax uses the continuous notation. @@ -57,32 +56,29 @@ struct pitch_kalman { double P[2][2]; - /* Thresholds of the innovation quantity for the mode switches */ + /* Thresholds of the innovation quantity for the mode switch */ - double scratch_threshold, reactive_threshold, adjust_threshold; + double scratch_threshold; /* Currently used coefficients*/ struct kalman_coeffs* coeffs; - /* Stable, adjust reactive coefficients for the mode switch */ + /* Stable and scratch coefficients for the mode switch */ struct kalman_coeffs stable; - struct kalman_coeffs adjust; - struct kalman_coeffs reactive; struct kalman_coeffs scratch; }; -void pitch_kalman_init(struct pitch_kalman *p, double dt, struct kalman_coeffs stable, - struct kalman_coeffs adjust, struct kalman_coeffs reactive, struct kalman_coeffs scratch, - double adjust_threshold, double reactive_threshold, double scratch_threshold, bool debug); -void pitch_kalman_update(struct pitch_kalman *p, double dx); +void pitch_kalman_init(struct pitch_kalman_filter *p, double dt, struct kalman_coeffs stable, + struct kalman_coeffs scratch, double scratch_threshold, bool debug); +void pitch_kalman_update(struct pitch_kalman_filter *p, double dx); /* * Retune noise sensitivity without resetting state */ -static inline void kalman_tune_sensitivity(struct pitch_kalman* p, struct kalman_coeffs* coeffs) +static inline void kalman_tune_sensitivity(struct pitch_kalman_filter* p, struct kalman_coeffs* coeffs) { if (!p || !coeffs) { errno = EINVAL; @@ -93,22 +89,4 @@ static inline void kalman_tune_sensitivity(struct pitch_kalman* p, struct kalman p->coeffs = coeffs; } - -/* - * Get the current pitch (velocity estimate) - */ - -static inline double pitch_kalman_current(const struct pitch_kalman* p) -{ - if (!p) { - errno = EINVAL; - perror(__func__); - return 0.0; - } - - /* Return the velocity Xk[v] relative to carrier frequency of the timecode (normalized) */ - - return p->Xk[1]; -} - #endif /* PITCH_KALMAN_H */ diff --git a/lib/xwax/ringbuffer.c b/lib/xwax/ringbuffer.c new file mode 100644 index 000000000000..eb39ef29ef10 --- /dev/null +++ b/lib/xwax/ringbuffer.c @@ -0,0 +1,110 @@ +#include +#include +#include +#include + +#include "ringbuffer.h" + +/* + * Allocates the ringbuffer + */ + +struct ringbuffer *rb_alloc(size_t size, size_t elem_size) +{ + if (!size || !elem_size) { + errno = EINVAL; + perror(__func__); + return NULL; + } + + struct ringbuffer *rb = malloc(sizeof(struct ringbuffer)); + if (!rb) + goto error; + + rb->size = size; + rb->elem_size = elem_size; + rb->current = size - 1; + + rb->data = calloc(size, elem_size); + if (!rb->data) + goto error_data; + + return rb; + +error_data: + free(rb); +error: + perror(__func__); + + return NULL; +} + +/* + * Frees the ringbuffer + */ + +void rb_free(struct ringbuffer *rb) +{ + if (!rb) + return; + + free(rb->data); + free(rb); +} + +/* + * Gets the sample at index i in the ringbuffer + */ + +const void *rb_at(const struct ringbuffer *rb, ptrdiff_t i) +{ + if (!rb || !rb->data || rb->size == 0) { + errno = EINVAL; + perror(__func__); + return NULL; + } + + ptrdiff_t index = (rb->current + i) % (ptrdiff_t)rb->size; + while (index < 0) + index += rb->size; + + /* Offset from the start in bytes: index * sizeof(element) */ + + return (char *)rb->data + index * rb->elem_size; +} + +/* + * Decrements the ringbuffer pointer + */ + +static inline void rb_dec(struct ringbuffer *rb) +{ + if (!rb || rb->size == 0) { + errno = EINVAL; + perror(__func__); + return; + } + + rb->current--; + while (rb->current < 0) + rb->current += rb->size; +} + +/* + * Pushes a new sample to the ringbuffer + */ + +void rb_push(struct ringbuffer *rb, const void *elem) +{ + if (!rb || !rb->data) { + errno = EINVAL; + perror(__func__); + return; + } + + rb_dec(rb); + + /* Compute the destination pointer as an offset in bytes from the start of the buffer */ + + memcpy((char *)rb->data + rb->current * rb->elem_size, elem, rb->elem_size); +} diff --git a/lib/xwax/ringbuffer.h b/lib/xwax/ringbuffer.h new file mode 100644 index 000000000000..0499b43bb034 --- /dev/null +++ b/lib/xwax/ringbuffer.h @@ -0,0 +1,20 @@ +#ifndef RINGBUFFER_H + +#define RINGBUFFER_H + +#include + +struct ringbuffer { + size_t size; + size_t elem_size; + void *data; + ptrdiff_t current; +}; + +struct ringbuffer *rb_alloc(size_t size, size_t elem_size); +void rb_free(struct ringbuffer *rb); + +const void *rb_at(const struct ringbuffer *rb, ptrdiff_t i); +void rb_push(struct ringbuffer *rb, const void *elem); + +#endif /* end of include guard RINGBUFFER_H */ diff --git a/lib/xwax/timecoder.c b/lib/xwax/timecoder.c index fa8af7f08f45..0524ae53e48e 100755 --- a/lib/xwax/timecoder.c +++ b/lib/xwax/timecoder.c @@ -27,9 +27,14 @@ #ifndef _MSC_VER #include #endif +#define _USE_MATH_DEFINES +#include +#include "complex.h" #include "debug.h" +#include "filters.h" #include "timecoder.h" +#include "timecoder_mk2.h" #define ZERO_RC 0.001 /* time constant for zero/rumble filter */ @@ -51,6 +56,7 @@ #define SWITCH_PHASE 0x1 /* tone phase difference of 270 (not 90) degrees */ #define SWITCH_PRIMARY 0x2 /* use left channel (not right) as primary */ #define SWITCH_POLARITY 0x4 /* read bit values in negative (not positive) */ +#define TRAKTOR_MK2 0x8 /* use for Traktor MK2 timecode*/ static struct timecode_def timecodes[] = { { @@ -110,6 +116,60 @@ static struct timecode_def timecodes[] = { .safe = 907000, .threshold = (128 << 16), }, + { + .name = "traktor_mk2_a", + .desc = "Traktor Scratch MK2, side A", + .resolution = 2500, + .flags = TRAKTOR_MK2, + .bits = 110, + .seed_mk2 = { + .high = 0xc6007c63e, + .low = 0x3fc00c60f8c1f00 + }, + .taps_mk2 = { + .high = 0x400000000040, + .low = 0x0000010800000001 + }, + .length = 1845000, + .safe = 1795000, + .threshold = (128 << 16), + }, + { + .name = "traktor_mk2_b", + .desc = "Traktor Scratch MK2, side B", + .resolution = 2500, + .flags = TRAKTOR_MK2, + .bits = 110, + .seed_mk2 = { + .high = 0x1ff9f00003, + .low = 0xe73ff00f9fe0c7c1 + }, + .taps_mk2 = { + .high = 0x400000000040, + .low = 0x0000010800000001 + }, + .length = 2590000, + .safe = 2540000, + .threshold = (128 << 16), + }, + { + .name = "traktor_mk2_cd", + .desc = "Traktor Scratch MK2, CD", + .resolution = 3000, + .flags = TRAKTOR_MK2, + .bits = 110, + .seed_mk2 = { + .high = 0x7ce73, + .low = 0xe0e0fff1fc1cf8c1 + }, + .taps_mk2 = { + .high = 0x400000000000, + .low = 0x1000010800000001 + }, + .length = 4500000, + .safe = 4450000, + .threshold = (128 << 16), + }, { .name = "mixvibes_v2", .desc = "MixVibes V2", @@ -158,6 +218,26 @@ static struct timecode_def timecodes[] = { .safe = 913000, .threshold = (128 << 16), }, + { + .name = "algoriddim_a", + .desc = "Algoriddim djay PRO AI 12\", side A", + .resolution = 1000, + .bits = 20, + .seed = 0x1ba77, + .taps = 0x1a0f1b, + .length = 600000, + .safe = 590000, + }, + { + .name = "algoriddim_b", + .desc = "Algoriddim djay PRO AI 12\", side B", + .resolution = 1000, + .bits = 20, + .seed = 0xe4606, + .taps = 0x1f1e7b, + .length = 900000, + .safe = 890000, + } }; /* @@ -256,7 +336,7 @@ static int build_lookup(struct timecode_def *def) * Return: pointer to timecode definition, or NULL if not available */ -struct timecode_def* timecoder_find_definition(const char *name) +struct timecode_def* timecoder_find_definition(const char *name, const char *lut_dir_path) { unsigned int n; @@ -266,9 +346,24 @@ struct timecode_def* timecoder_find_definition(const char *name) if (strcmp(def->name, name) != 0) continue; - if (build_lookup(def) == -1) - return NULL; /* error */ - + if (!def->lookup) { + if (def->flags & TRAKTOR_MK2) { + if (!lut_load_mk2(def, lut_dir_path)) + return def; + + if (build_lookup_mk2(def) == -1) + return NULL; /* error */ + + if (lut_store_mk2(def, lut_dir_path)) { + timecoder_free_lookup(); + fprintf(stderr, "Couldn't store LUT on disk\n"); + return NULL; + } + } else { + if (build_lookup(def) == -1) + return NULL; /* error */ + } + } return def; } @@ -285,19 +380,74 @@ void timecoder_free_lookup(void) { for (n = 0; n < ARRAY_SIZE(timecodes); n++) { struct timecode_def *def = &timecodes[n]; - if (def->lookup) - lut_clear(&def->lut); + if (def->flags & TRAKTOR_MK2) { + if (def->lookup) + lut_clear_mk2(&def->lut_mk2); + } else { + if (def->lookup) + lut_clear(&def->lut); + } } } +/* + * Initialise a subcode decoder for the Traktor MK2 + */ + +void mk2_subcode_init(struct mk2_subcode *sc) +{ + sc->valid_counter = 0; + sc->avg_reading = INT_MAX/2; + sc->avg_slope = INT_MAX/2; + sc->bit = U128_ZERO; + + sc->readings = rb_alloc(3, sizeof(int)); + assert(sc->readings); + + /* Initialise smoothing filters */ + ewma_init(&sc->ewma_reading, 0.01); + ewma_init(&sc->ewma_slope, 0.01); +} + /* * Initialise filter values for one channel */ -static void init_channel(struct timecoder_channel *ch) +static void init_channel(struct timecode_def *def, struct timecoder_channel *ch, + unsigned int sample_rate) { ch->positive = false; ch->zero = 0; + + ch->deriv = INT_MAX/2; + ch->rms = INT_MAX/2; + ch->rms_deriv = 0; + + ch->delayline = rb_alloc(5, sizeof(int)); + assert(ch->delayline); + + ch->delayline_deriv = rb_alloc(5, sizeof(int)); + assert(ch->delayline_deriv); + + ewma_init_adaptive(&ch->ewma_filter, 5e-2, def->resolution, sample_rate); + derivative_init(&ch->differentiator); + + rms_init(&ch->rms_filter, 1e-3); + rms_init(&ch->rms_deriv_filter, 1e-3); + + /* + * The ratio of the sampling rate and carrier frequency divided by four + * make for a good window size. This was concluded empirically. + */ + + size_t window = (size_t)ceil(sample_rate / def->resolution)/4; + if (window % 2 == 0) + window++; + + ch->savgol_filter = savgol_create(window, 3); + assert(ch->savgol_filter); + + rhpf_init(&ch->rumble_filter, sample_rate, 50.0 * ((double)def->resolution / 1000.0)); } /* @@ -319,6 +469,7 @@ void timecoder_init(struct timecoder *tc, struct timecode_def *def, tc->speed = speed; tc->dt = 1.0 / sample_rate; + tc->sample_rate = sample_rate; tc->zero_alpha = tc->dt / (ZERO_RC + tc->dt); tc->threshold = tc->def->threshold; @@ -327,30 +478,38 @@ void timecoder_init(struct timecoder *tc, struct timecode_def *def, tc->threshold >>= 5; /* approx -36dB */ tc->forwards = 1; - init_channel(&tc->primary); - init_channel(&tc->secondary); + init_channel(tc->def, &tc->primary, sample_rate); + init_channel(tc->def, &tc->secondary, sample_rate); tc->use_legacy_pitch_filter = pitch_estimator; /* Switch for pitch filter type */ - tc->quadrant = 0; - tc->last_quadrant = 0; - tc->direction_changed = false; + tc->dphi = 0.0; + tc->freq = 0.0; + tc->pitch = 0.0; if (tc->use_legacy_pitch_filter) { - pitch_init(&tc->pitch, tc->dt); + pitch_init(&tc->pitch_filter, tc->dt); } else { - pitch_kalman_init(&tc->pitch_kalman, - tc->dt, - KALMAN_COEFFS(1e-8, 10.0), /* stable mode */ - KALMAN_COEFFS(1e-4, 1e-1), /* adjust mode */ - KALMAN_COEFFS(1e-3, 1e-2), /* reactive mode */ - KALMAN_COEFFS(1e-1, 1e-4), /* scratch mode */ - 6e-4, /* adjust threshold */ - 25e-4, /* reactive threshold */ - 40e-4, /* scratch threshold */ - false); + if (tc->def->flags & TRAKTOR_MK2) { + pitch_kalman_init(&tc->pitch_kalman_filter, tc->dt, + KALMAN_COEFFS(1e-16, 1e-2), /* stable mode */ + KALMAN_COEFFS(0.0135, 8e-6), /* scratch mode */ + 15, /* threshold */ + false); + } else { + pitch_kalman_init(&tc->pitch_kalman_filter, tc->dt, + KALMAN_COEFFS(1e-16, 1e-2), /* stable mode */ + KALMAN_COEFFS(0.0235, 1e-5), /* scratch mode */ + 5, /* threshold */ + false); + } } + tc->ref_level = INT_MAX; + tc->bitstream = 0; + tc->timecode = 0; + tc->valid_counter = 0; + tc->ref_level = INT_MAX; tc->bitstream = 0; tc->timecode = 0; @@ -358,6 +517,11 @@ void timecoder_init(struct timecoder *tc, struct timecode_def *def, tc->timecode_ticker = 0; tc->mon = NULL; + + if (tc->def->flags & TRAKTOR_MK2) { + mk2_subcode_init(&tc->upper_bitstream); + mk2_subcode_init(&tc->lower_bitstream); + } } /* @@ -367,6 +531,19 @@ void timecoder_init(struct timecoder *tc, struct timecode_def *def, void timecoder_clear(struct timecoder *tc) { assert(tc->mon == NULL); + + rb_free(tc->primary.delayline); + rb_free(tc->secondary.delayline); + rb_free(tc->primary.delayline_deriv); + rb_free(tc->secondary.delayline_deriv); + + if (tc->def->flags & TRAKTOR_MK2) { + rb_free(tc->upper_bitstream.readings); + rb_free(tc->lower_bitstream.readings); + } + + savgol_destroy(tc->primary.savgol_filter); + savgol_destroy(tc->secondary.savgol_filter); } /* @@ -438,6 +615,13 @@ static inline void update_monitor(struct timecoder *tc, signed int x, signed int if (!tc->mon) return; + /* Only draw the monitor if the signal level is greater -40 dB */ + + if (tc->dB < -40.0) { + x = 0; + y = 0; + } + size = tc->mon_size; ref = tc->ref_level; @@ -517,56 +701,77 @@ static void process_bitstream(struct timecoder *tc, signed int m) } /* - * Compare the last quadrant we were in to the new one and return the - * correct displacement for the pitch filter model. - * - * A full revolution of the carrier has the length of 1.0 / tc->def->resolution, - * which is also the sample rate of the timecode (not the audio). One quadrant - * corresponds to 1/4 * revolution, which is the time between two zero - * crossings. Hence we multiply this quantity by displacement in quadrants. + * Computes the phase difference of a given sine-cosine pair using + * complex number theory in ARM Q1.31 fixed-point format for max efficiency. */ -static double quantize_phase(struct timecoder *tc) +static inline double phase_difference(const int cos0, const int sin0, + const int cos1, const int sin1) { - unsigned diff = ((tc->quadrant - tc->last_quadrant) % 4); - static unsigned long long direction_change_counter = 0; - const unsigned int forwards_diff = (tc->def->flags & SWITCH_PHASE) ? 1 : 3; - const unsigned int backwards_diff = (tc->def->flags & SWITCH_PHASE) ? 3 : 1; - - /* Check for a displacement of four quadrants */ - if (diff == 0 && !tc->direction_changed) { - return 1.0 / tc->def->resolution; - } - - /* Check for a displacement of three quadrants */ - if ((tc->forwards && diff == forwards_diff) || (!tc->forwards && diff == backwards_diff)) { - return (3.0 / tc->def->resolution) / 4.0; - } - - /* Check for a displacement of two quadrants */ - if (diff == 2) { - return (1.0 / tc->def->resolution) / 2.0; - } - - return (1.0 / tc->def->resolution) / 4.0; + struct complex_q31 z0 = { .re = cos0, .im = sin0 }; + struct complex_q31 z1 = { .re = cos1, .im = sin1 }; + struct complex_q31 z1_conj = complex_q31_conj(z1); + struct complex_q63 product = complex_q31_mul(z0, z1_conj); + + return atan2((double)product.im, (double)product.re); } /* - * Track the quadrature phase of the pitch counter on the unit circle + * Various processing of the carrier wave needed for pitch detection. * - * There are four zero crossings in a whole cycle. In between are the four - * quadrants of the sine and cosine, which are in quadrature. + * Pushes samples into a delayline, computes the derivative, filters it and + * computes RMS values. + * Afterards the upscaled derivative can by processed by the pitch detection + * algorithm. */ -static void track_quadrature_phase(struct timecoder *tc, bool direction_changed) +static void process_carrier(struct timecoder *tc, signed int primary, + signed int secondary) { - tc->last_quadrant = tc->quadrant; - tc->direction_changed = direction_changed; + if (!tc) { + errno = EINVAL; + perror(__func__); + return; + } + + /* Push the samples into the ringbuffer */ + + rb_push(tc->primary.delayline, &primary); + rb_push(tc->secondary.delayline, &secondary); + + primary = rhpf_process(&tc->primary.rumble_filter, primary); + secondary = rhpf_process(&tc->secondary.rumble_filter, secondary); + + /* Compute the discrete derivative */ + tc->primary.deriv = derivative(&tc->primary.differentiator, + primary); + tc->secondary.deriv = derivative(&tc->secondary.differentiator, + secondary); + + tc->primary.deriv = ewma(&tc->primary.ewma_filter, tc->primary.deriv); + tc->secondary.deriv = ewma(&tc->secondary.ewma_filter, tc->secondary.deriv); + + tc->primary.deriv_decoder = tc->primary.deriv; + tc->secondary.deriv_decoder = tc->secondary.deriv; + + tc->primary.deriv = savgol(tc->primary.savgol_filter, tc->primary.deriv); + tc->secondary.deriv = savgol(tc->secondary.savgol_filter, tc->secondary.deriv); - bool pos = tc->primary.swapped ? tc->primary.positive : tc->secondary.positive; - bool add = tc->secondary.swapped ? 0b1 : 0b0; + /* Compute the smoothed RMS value */ + tc->primary.rms = rms(&tc->primary.rms_filter, primary); + tc->secondary.rms = rms(&tc->secondary.rms_filter, secondary); - tc->quadrant = (!pos << 1) | add; + /* Compute the smoothed RMS value for the derivative */ + tc->primary.rms_deriv = + rms(&tc->primary.rms_deriv_filter, tc->primary.deriv); + tc->secondary.rms_deriv = + rms(&tc->secondary.rms_deriv_filter, tc->secondary.deriv); + + tc->dB = 20 * log10((double)tc->secondary.rms / INT_MAX); + + /* Push the derivative samples into the ringbuffer */ + rb_push(tc->primary.delayline_deriv, &tc->primary.deriv); + rb_push(tc->secondary.delayline_deriv, &tc->secondary.deriv); } /* @@ -579,72 +784,74 @@ static void track_quadrature_phase(struct timecoder *tc, bool direction_changed) static void process_sample(struct timecoder *tc, signed int primary, signed int secondary) { - detect_zero_crossing(&tc->primary, primary, tc->zero_alpha, tc->threshold); - detect_zero_crossing(&tc->secondary, secondary, tc->zero_alpha, tc->threshold); + if (tc->def->flags & TRAKTOR_MK2) { + detect_zero_crossing(&tc->primary, tc->primary.deriv_decoder, tc->zero_alpha, + tc->threshold); + detect_zero_crossing(&tc->secondary, tc->secondary.deriv_decoder, tc->zero_alpha, + tc->threshold); + } else { + detect_zero_crossing(&tc->primary, primary, tc->zero_alpha, tc->threshold); + detect_zero_crossing(&tc->secondary, secondary, tc->zero_alpha, tc->threshold); + } - /* If an axis has been crossed, use the direction of the crossing - * to work out the direction of the vinyl */ + if (tc->dB > -45.0) { // Ignore noise + tc->dphi = + phase_difference(*(int*)rb_at(tc->primary.delayline_deriv, 0), + *(int*)rb_at(tc->secondary.delayline_deriv, 0), + *(int*)rb_at(tc->primary.delayline_deriv, 1), + *(int*)rb_at(tc->secondary.delayline_deriv, 1)); - if (tc->primary.swapped || tc->secondary.swapped) { - bool forwards; + double ddphi = 0.0; /* Derivative of the phase difference */ - if (tc->primary.swapped) { - forwards = (tc->primary.positive != tc->secondary.positive); + if (tc->use_legacy_pitch_filter) { + pitch_dt_observation(&tc->pitch_filter, tc->dphi); + ddphi = tc->pitch_filter.v; } else { - forwards = (tc->primary.positive == tc->secondary.positive); + pitch_kalman_update(&tc->pitch_kalman_filter, tc->dphi); + ddphi = tc->pitch_kalman_filter.Xk[1]; } - if (tc->def->flags & SWITCH_PHASE) - forwards = !forwards; - - track_quadrature_phase(tc, forwards != tc->forwards); - - if (forwards != tc->forwards) { /* direction has changed */ - tc->forwards = forwards; - tc->valid_counter = 0; - } + tc->freq = ddphi / (2.0 * M_PI); + tc->pitch = (tc->freq / tc->def->resolution); + } else { + tc->freq = 0.0; + tc->pitch = 0.0; } - /* - * If any axis has been crossed, register movement using the pitch - * counters. This occurs four time per cycle of the sinusoid. - */ + bool forwards = tc->forwards; + if (tc->freq > 0.0) + forwards = true; + else if (tc->freq < 0.0) + forwards = false; - if (!tc->primary.swapped && !tc->secondary.swapped) { - if (tc->use_legacy_pitch_filter) - pitch_dt_observation(&tc->pitch, 0.0); - else - pitch_kalman_update(&tc->pitch_kalman, 0.0); - } else { - double dx; - - /* - * Assumption: We usually advance by a quarter rotation, - * unless we skip zero crossings. In this case the new quadrature - * tracker calculates the correct displacement for the pitch filter. - */ - - dx = quantize_phase(tc); - if (!tc->forwards) - dx = -dx; - - if (tc->use_legacy_pitch_filter) - pitch_dt_observation(&tc->pitch, dx); - else - pitch_kalman_update(&tc->pitch_kalman, dx); + if (tc->def->flags & SWITCH_PHASE) { + tc->pitch = -tc->pitch; + tc->freq = -tc->freq; + forwards = !forwards; } + if (forwards != tc->forwards) /* direction has changed */ + tc->forwards = forwards; + /* If we have crossed the primary channel in the right polarity, * it's time to read off a timecode 0 or 1 value */ - if (tc->secondary.swapped && - tc->primary.positive == ((tc->def->flags & SWITCH_POLARITY) == 0)) - { - signed int m; - - /* scale to avoid clipping */ - m = abs(primary / 2 - tc->primary.zero / 2); - process_bitstream(tc, m); + if (tc->def->flags & TRAKTOR_MK2) { + if (tc->secondary.swapped) + { + int reading = *(int *)rb_at(tc->secondary.delayline, 2); + mk2_process_timecode(tc, reading); + } + } else { + if (tc->secondary.swapped && + tc->primary.positive == ((tc->def->flags & SWITCH_POLARITY) == 0)) + { + signed int m; + + /* scale to avoid clipping */ + m = abs(primary / 2 - tc->primary.zero / 2); + process_bitstream(tc, m); + } } tc->timecode_ticker++; @@ -704,8 +911,24 @@ void timecoder_submit(struct timecoder *tc, signed short *pcm, size_t npcm) secondary = left; } - process_sample(tc, primary, secondary); - update_monitor(tc, left, right); + process_carrier(tc, primary, secondary); + + if (tc->def->flags & TRAKTOR_MK2) { + process_sample(tc, primary, secondary); + + /* + * Display the derivative in the monitor. Since the signal is not + * a perfect ring on the x-y-plane, but jumps up and down a bit, + * it looks to small in the scope. Therefore a multiplication by + * two is necessary. + */ + + update_monitor(tc, tc->primary.deriv * 2, + tc->secondary.deriv * 2); + } else { + process_sample(tc, primary, secondary); + update_monitor(tc, left, right); + } pcm += TIMECODER_CHANNELS; } @@ -730,9 +953,15 @@ signed int timecoder_get_position(struct timecoder *tc, double *when) if (tc->valid_counter <= VALID_BITS) return -1; - r = lut_lookup(&tc->def->lut, tc->bitstream); - if (r == -1) - return -1; + if (tc->def->flags & TRAKTOR_MK2) { + r = lut_lookup_mk2(&tc->def->lut_mk2, &tc->mk2_bitstream); + if (r == -1) + return -1; + } else { + r = lut_lookup(&tc->def->lut, tc->bitstream); + if (r == -1) + return -1; + } if (r >= 0) { // normalize position to milliseconds, not timecode steps -- Owen diff --git a/lib/xwax/timecoder.h b/lib/xwax/timecoder.h index 0e9a197ebeaf..c28c664f43eb 100644 --- a/lib/xwax/timecoder.h +++ b/lib/xwax/timecoder.h @@ -22,15 +22,17 @@ #include +#include "filters.h" #include "lut.h" +#include "lut_mk2.h" #include "pitch.h" #include "pitch_kalman.h" +#include "ringbuffer.h" #define TIMECODER_CHANNELS 2 #ifdef __cplusplus extern "C" { - #endif // __cplusplus typedef unsigned int bits_t; @@ -42,11 +44,14 @@ struct timecode_def { flags; bits_t seed, /* LFSR value at timecode zero */ taps; /* central LFSR taps, excluding end taps */ + mk2bits_t seed_mk2, /* MK2 version */ + taps_mk2; /* MK2 version */ unsigned int length, /* in cycles */ safe; /* last 'safe' timecode number (for auto disconnect) */ signed int threshold; /* threshold for detection of zero-crossings */ bool lookup; /* true if lut has been generated */ struct lut lut; + struct lut_mk2 lut_mk2; /* MK2 version */ }; struct timecoder_channel { @@ -54,43 +59,79 @@ struct timecoder_channel { swapped; /* wave recently swapped polarity */ signed int zero; unsigned int crossing_ticker; /* samples since we last crossed zero */ + + int rms, rms_deriv; /* RMS values for the signal and its derivative */ + signed int deriv, deriv_decoder; /* Derivative */ + + struct ringbuffer *delayline; /* needed for the pitch detection*/ + struct ringbuffer *delayline_deriv; /* needed for the pitch detection*/ + struct ewma_filter ewma_filter; + struct differentiator differentiator; + struct root_mean_square rms_filter, rms_deriv_filter; + struct savitzky_golay *savgol_filter; + struct rumble_filter rumble_filter; +}; + +struct mk2_subcode { + mk2bits_t bitstream; + mk2bits_t timecode; + mk2bits_t bit; + + unsigned int valid_counter; + signed int avg_reading; + signed int avg_slope; + bool recent_bit_flip; + + struct ringbuffer *readings; + struct ewma_filter ewma_reading; + struct ewma_filter ewma_slope; }; struct timecoder { + struct timecoder_channel primary, secondary; struct timecode_def *def; - double speed; + + double speed; /* 33 or 45 rpm */ /* Precomputed values */ double dt, zero_alpha; + int sample_rate; signed int threshold; /* Pitch information */ - bool forwards; - struct timecoder_channel primary, secondary; + double dphi; /* Phase difference */ + double freq; /* Current carrier frequency */ + double pitch; /* Current pitch */ + bool forwards; bool use_legacy_pitch_filter; - struct pitch pitch; - struct pitch_kalman pitch_kalman; - unsigned quadrant, last_quadrant; bool direction_changed; + struct pitch_filter pitch_filter; + struct pitch_kalman_filter pitch_kalman_filter; + /* Numerical timecode */ signed int ref_level; bits_t bitstream, /* actual bits from the record */ timecode; /* corrected timecode */ + mk2bits_t mk2_bitstream, /* Traktor MK2 version */ + mk2_timecode; /* Traktor MK2 version */ unsigned int valid_counter, /* number of successful error checks */ timecode_ticker; /* samples since valid timecode was read */ + double dB; /* Decibels to detect phono level */ /* Feedback */ unsigned char *mon; /* x-y array */ int mon_size, mon_counter; + + struct mk2_subcode upper_bitstream, lower_bitstream; }; -struct timecode_def* timecoder_find_definition(const char *name); +struct timecode_def* timecoder_find_definition(const char *name, const char *lut_dir_path); void timecoder_free_lookup(void); void timecoder_init(struct timecoder *tc, struct timecode_def *def, @@ -119,10 +160,7 @@ static inline struct timecode_def* timecoder_get_definition(struct timecoder *tc static inline double timecoder_get_pitch(struct timecoder *tc) { - if (tc->use_legacy_pitch_filter) - return pitch_current(&tc->pitch) / tc->speed; - else - return pitch_kalman_current(&tc->pitch_kalman) / tc->speed; + return tc->pitch / tc->speed; } /* diff --git a/lib/xwax/timecoder_mk2.c b/lib/xwax/timecoder_mk2.c new file mode 100644 index 000000000000..4e1250572713 --- /dev/null +++ b/lib/xwax/timecoder_mk2.c @@ -0,0 +1,482 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "timecoder_mk2.h" + +#define REF_PEAKS_AVG 48 /* in wave cycles */ + +/* + * Compute the LFSR bit (Traktor MK2 version) + */ + +static inline mk2bits_t lfsr_mk2(mk2bits_t code, mk2bits_t taps) +{ + mk2bits_t taken; + mk2bits_t xrs; + + taken = u128_and(code, taps); + xrs = U128_ZERO; + + while (u128_neq(taken, U128_ZERO)) { + xrs = u128_add(xrs, u128_and(taken, U128_ONE)); + taken = u128_rshift(taken, 1); + } + + return u128_and(xrs, U128_ONE); +} + +/* + * Linear Feedback Shift Register in the forward direction. New values + * are generated at the least-significant bit. (Traktor MK2 version) + */ + +inline mk2bits_t fwd_mk2(mk2bits_t current, struct timecode_def *def) +{ + if (!def) { + errno = -EINVAL; + perror(__func__); + return U128_ZERO; + } + + mk2bits_t l; + + /* New bits are added at the MSB; shift right by one */ + l = lfsr_mk2(current, u128_or(def->taps_mk2, U128_ONE)); + return u128_or(u128_rshift(current, 1), u128_lshift(l, (def->bits - 1))); +} + +/* + * Linear Feedback Shift Register in the reverse direction + * (Traktor MK2 version) + */ + +inline mk2bits_t rev_mk2(mk2bits_t current, struct timecode_def *def) +{ + if (!def) { + errno = -EINVAL; + perror(__func__); + return U128_ZERO; + } + + mk2bits_t l, mask; + + /* New bits are added at the LSB; shift left one and mask */ + mask = u128_sub(u128_lshift(U128_ONE, def->bits), U128_ONE); + l = lfsr_mk2(current, + u128_or(u128_rshift(def->taps_mk2, 1), + u128_lshift(U128_ONE, (def->bits - 1)))); + + return u128_or(u128_and(u128_lshift(current, 1), mask), l); +} + +/* + * Where necessary, build the lookup table required for this timecode + * (Traktor MK2 version) + * + * Return: -1 if not enough memory could be allocated, otherwise 0 + */ + +int build_lookup_mk2(struct timecode_def *def) +{ + if (!def) { + errno = -EINVAL; + perror(__func__); + return -1; + } + + unsigned int n; + mk2bits_t current, next; + + if (def->lookup) + return 0; + + fprintf(stderr, "Building LUT for %d bit %dHz timecode (%s)\n", + def->bits, def->resolution, def->desc); + + if (lut_init_mk2(&def->lut_mk2, def->length) == -1) + return -1; + + current = def->seed_mk2; + + for (n = 0; n < def->length; n++) { + + /* timecode must not wrap */ + assert(lut_lookup_mk2(&def->lut_mk2, ¤t) == (unsigned)-1); + lut_push_mk2(&def->lut_mk2, ¤t); + + /* check symmetry of the lfsr functions */ + next = fwd_mk2(current, def); + assert(u128_eq(rev_mk2(next, def), current)); + + current = next; + } + + def->lookup = true; + + return 0; +} + +/* + * Caches the generated LUT on the disk. + * + * This is only necessary for the Traktor MK2, since the size of its hash + * table is quite large. + */ + +int lut_store_mk2(struct timecode_def *def, const char *lut_dir_path) +{ + if (!def || !lut_dir_path) { + errno = EINVAL; + perror(__func__); + return -1; + } + + struct slot_mk2 *slot; + slot_no_t *hash; + + const size_t hashes = 1 << 16; + size_t hashes_written = 0; + size_t slots_written = 0; + char path[1024]; + FILE *fp = NULL; + int ret = 0; + int size; + + snprintf(path, sizeof(path), "%s/%s%s", lut_dir_path, def->name, ".mk2lut"); + + fprintf(stdout, "Storing LUT at %s\n", path); + + fp = fopen(path, "wb"); + if (!fp) { + perror("fopen"); + goto error_fopen; + } + + size = fwrite(def->lut_mk2.hdr, sizeof(struct lut_mk2_header), 1, fp); + if (!size) { + perror("fwrite"); + goto error; + } + + for (slots_written = 0; slots_written < def->length; slots_written++) { + slot = &def->lut_mk2.slot[slots_written]; + + size = fwrite(slot, sizeof(struct slot_mk2), 1, fp); + if (!size) { + perror("fwrite slot_mk2"); + goto error; + } + } + + for (hashes_written = 0; hashes_written < hashes; hashes_written++) { + hash = &def->lut_mk2.table[hashes_written]; + + size = fwrite(hash, sizeof(slot_no_t), 1, fp); + if (!size) { + perror("fwrite hashes"); + goto error; + } + } + + size = fwrite(&def->lut_mk2.avail, sizeof(slot_no_t), 1, fp); + if (!size) { + perror("fwrite avail"); + goto error; + } + + if (ret || hashes != hashes_written || def->length != slots_written) + fprintf(stderr, "Hashes and slots written don't match what's expected\n"); + + fclose(fp); + + return 0; + +error: + fclose(fp); +error_fopen: + lut_clear_mk2(&def->lut_mk2); + + return -1; +} + +/* + * Loads the stored LUT from the disk. + * + * This is only necessary for the Traktor MK2, since the size of its hash + * table is quite large. + */ + +int lut_load_mk2(struct timecode_def *def, const char *lut_dir_path) +{ + if (!def || !lut_dir_path) { + errno = EINVAL; + perror(__func__); + return -1; + } + + struct lut_mk2_header *hdr; + struct slot_mk2 *slot; + + const size_t hashes = 1 << 16; + size_t hashes_read = 0; + size_t slots_read = 0; + char oldpath[1024]; + size_t lut_size; + char path[1024]; + size_t size; + long fsize; + int ret = 0; + FILE *fp; + + snprintf(oldpath, sizeof(oldpath), "%s/%s%s", lut_dir_path, def->name, ".lut"); + snprintf(path, sizeof(path), "%s/%s%s", lut_dir_path, def->name, ".mk2lut"); + + /* Remove old LUT file. Safe to call when it doesn't exist. */ + + remove(oldpath); + + /* Compute the expected file size */ + + lut_size = sizeof(struct lut_mk2_header) + + def->length * sizeof(struct slot_mk2) + + (hashes + 1) * sizeof(slot_no_t); + + fprintf(stdout, "Loading LUT from %s\n", path); + fp = fopen(path, "rb"); + if (!fp) { + fprintf(stderr, "LUT for %s not found on disk\n", def->desc); + goto error_fopen; + } + + /* Get the actual file size */ + + ret = fseek(fp, 0L, SEEK_END); + if (ret) { + perror("fseek"); + goto error; + } + + fsize = ftell(fp); + if (fsize < 0) { + perror("ftell"); + goto error; + } + + rewind(fp); + + if (fsize < sizeof(struct lut_mk2_header)) { + fprintf(stderr, "Cached LUT file is corrupted. Regenerating....\n"); + goto error; + } + + /* Create the LUT struct and load the data */ + + ret = lut_init_mk2(&def->lut_mk2, def->length); + if (ret) { + fprintf(stderr, "Couldn't initialise LUT\n"); + goto error; + } + + hdr = def->lut_mk2.hdr; + hdr->major = 0; + hdr->minor = 0; + + size = fread(hdr, sizeof(struct lut_mk2_header), 1, fp); + if (!size) { + perror("fread lut_mk2_header"); + goto error; + } + + if (hdr->magic != MIXXX_LUT_MAGIC || hdr->major != MIXXX_LUT_MAJOR || hdr->minor != MIXXX_LUT_MINOR) { + fprintf(stderr, + "LUT version mismatch: (file: v%u.%u, expected: v%u.%u). Regenerating...\n", + hdr->major, hdr->minor, MIXXX_LUT_MAJOR, MIXXX_LUT_MINOR); + goto error; + } + + /* Check if the sizes match */ + + if (lut_size != (size_t)fsize) { + fprintf(stderr, + "LUT size mismatch: (file: %ldKb, expected: %zuKb). Regenerating...\n", + fsize / 1024, lut_size / 1024); + goto error; + } + + for (slots_read = 0; slots_read < def->length; slots_read++) { + slot = &def->lut_mk2.slot[slots_read]; + + size = fread(slot, sizeof(struct slot_mk2), 1, fp); + if (!size) { + perror("fread slot_mk2"); + goto error; + } + } + + for (hashes_read = 0; hashes_read < hashes; hashes_read++) { + slot_no_t *hash = &def->lut_mk2.table[hashes_read]; + + size = fread(hash, sizeof(slot_no_t), 1, fp); + if (!size) { + perror("fread hashes"); + goto error; + } + } + + size = fread(&def->lut_mk2.avail, sizeof(slot_no_t), 1, fp); + if (!size) { + perror("fread avail"); + goto error; + } + + if (hashes == hashes_read && def->length == slots_read) { + def->lookup = true; + } else { + fprintf(stderr, "Hashes and slots read don't match what's expected\n"); + goto error; + } + + return 0; + +error: + fclose(fp); +error_fopen: + lut_clear_mk2(&def->lut_mk2); + + return -1; +} + +/* + * Detect if the upward or downward slope hits a threshold. + * Upwards signifies a one and downwards a zero. + */ + +static inline void detect_bit_flip(const int slope[2], int rms, int reading, int avg_reading, + mk2bits_t *bit, bool *bit_flipped, bool forwards, mk2bits_t one) +{ + static const double reverse_factor = 1.75; + static const double forward_factor = 1.5; + double threshold; + + if (*bit_flipped == false) { + if (forwards) { + threshold = rms / forward_factor; + } else { + threshold = rms / reverse_factor; + one = u128_not(one); + } + + if (u128_eq(*bit, u128_not(one)) && slope[0] > threshold && slope[1] > threshold) { + *bit = one; + *bit_flipped = true; + } else if (u128_eq(*bit, one) && slope[0] < -threshold && slope[1] < -threshold) { + *bit = u128_not(one); + *bit_flipped = true; + } + } else { + *bit_flipped = false; + } +} + +/* + * Verify the new LFSR state in the forward or reverse direction. + */ + +static inline bool lfsr_verify(struct timecode_def *def, mk2bits_t *timecode, mk2bits_t *bitstream, + mk2bits_t bit, bool forwards) +{ + if (forwards) { + *timecode = fwd_mk2(*timecode, def); + *bitstream = u128_add(u128_rshift(*bitstream, 1), u128_lshift(bit, (def->bits - 1))); + } else { + mk2bits_t mask = u128_sub(u128_lshift(U128_ONE, def->bits), U128_ONE); + *timecode = rev_mk2(*timecode, def); + *bitstream = u128_add(u128_and(u128_lshift(*bitstream, 1), mask), bit); + } + + if (u128_eq(*timecode, *bitstream)) + return true; + else + return false; +} + +/* + * Process the upper or lower bitstream contained in the Traktor MK2 signal + */ + +inline static void mk2_process_bitstream(struct timecoder *tc, struct mk2_subcode *sc, + signed int reading) +{ + int current_slope[2]; + + rb_push(sc->readings, &reading); + + sc->avg_reading = ewma(&sc->ewma_reading, reading); + + /* Calculate absolute of average slope */ + sc->avg_slope = ewma(&sc->ewma_slope, abs(reading - *(int*)rb_at(sc->readings, 1))); + + /* Calculate current and last slope */ + current_slope[0] = (reading - *(int*)rb_at(sc->readings, 1)); + current_slope[1] = (reading - *(int*)rb_at(sc->readings, 2)); + + /* The bits only change when an offset jump occurs. Else the previous bit is taken */ + detect_bit_flip(current_slope, tc->secondary.rms, reading, sc->avg_reading, &sc->bit, + &sc->recent_bit_flip, tc->forwards, U128(0x0, !tc->secondary.positive)); + + if (lfsr_verify(tc->def, &sc->timecode, &sc->bitstream, sc->bit, tc->forwards)) { + (sc->valid_counter)++; + } else { + sc->timecode = sc->bitstream; + sc->valid_counter = 0; + } +} + +/* + * Process the upper or lower bitstream contained in the Traktor MK2 signal + */ + +void mk2_process_timecode(struct timecoder *tc, signed int reading) +{ + /* + * Detect if the offset jumps on upper and lower bitstream + */ + + if (tc->secondary.positive) + mk2_process_bitstream(tc, &tc->upper_bitstream, reading); + else if (!tc->secondary.positive) + mk2_process_bitstream(tc, &tc->lower_bitstream, reading); + + if (tc->lower_bitstream.valid_counter > tc->upper_bitstream.valid_counter) { + tc->mk2_bitstream = tc->lower_bitstream.bitstream; + tc->mk2_timecode = tc->lower_bitstream.timecode; + } else { + tc->mk2_bitstream = tc->upper_bitstream.bitstream; + tc->mk2_timecode = tc->upper_bitstream.timecode; + } + + if (u128_eq(tc->mk2_timecode, tc->mk2_bitstream)) { + tc->valid_counter++; + } else { + tc->timecode = tc->bitstream; + tc->valid_counter = 0; + } + /* Take note of the last time we read a valid timecode */ + + tc->timecode_ticker = 0; + + tc->ref_level -= tc->ref_level / REF_PEAKS_AVG; + tc->ref_level += abs((int)(tc->secondary.rms_deriv)) + / REF_PEAKS_AVG; + + debug("upper.valid_counter: %d, lower.valid_counter %d, forwards: %b\n", */ + tc->upper.valid_counter, + tc->lower.valid_counter, + tc->forwards); +} diff --git a/lib/xwax/timecoder_mk2.h b/lib/xwax/timecoder_mk2.h new file mode 100644 index 000000000000..5a2f99293dac --- /dev/null +++ b/lib/xwax/timecoder_mk2.h @@ -0,0 +1,17 @@ +#ifndef TIMECODER_MK2_H + +#define TIMECODER_MK2_H + +#include "lut_mk2.h" +#include "timecoder.h" + +mk2bits_t fwd_mk2(mk2bits_t current, struct timecode_def *def); +mk2bits_t rev_mk2(mk2bits_t current, struct timecode_def *def); + +int build_lookup_mk2(struct timecode_def *def); +int lut_load_mk2(struct timecode_def *def, const char *lut_dir_path); +int lut_store_mk2(struct timecode_def *def, const char *lut_dir_path); + +void mk2_process_timecode(struct timecoder *tc, signed int reading); + +#endif /* end of include guard TIMECODER_MK2_H */ diff --git a/lib/xwax/types.h b/lib/xwax/types.h new file mode 100644 index 000000000000..3fe87e1cd81b --- /dev/null +++ b/lib/xwax/types.h @@ -0,0 +1,148 @@ +#ifndef TYPES_H +#define TYPES_H + +#include +#include + +/* + * Define the u128 struct using two 64-bit unsigned integers, with high part first. + */ + +typedef struct { + uint64_t high; /* Most significant part */ + uint64_t low; /* Least significant part */ +} u128; + +/* + * Inline constructor for u128. + * Works in all compilers, including MSVC. + */ + +static inline u128 make_u128(uint64_t high, uint64_t low) +{ + u128 v; + + v.high = high; + v.low = low; + + return v; +} + +/* + * Macro to preserve U128() syntax, but call the portable constructor. + * + * Used to be only the macro before, but MSVC doesn't like C99 compound + * literals. + */ + +#define U128(high, low) make_u128((high), (low)) +#define U128_ZERO make_u128(0ULL, 0ULL) +#define U128_ONE make_u128(0ULL, 1ULL) + +static inline int u128_eq(u128 a, u128 b) +{ + return (a.high == b.high) && (a.low == b.low); +} + +/* + * Not-equal comparison. + */ + +static inline int u128_neq(u128 a, u128 b) +{ + return (a.high != b.high) || (a.low != b.low); +} + +/* + * Addition of two u128 values. + */ + +static inline u128 u128_add(u128 a, u128 b) +{ + uint64_t sum = a.low + b.low; + uint64_t carry = (sum < a.low) ? 1 : 0; + + return U128(a.high + b.high + carry, sum); +} + +/* + * Subtraction of two u128 values. + */ + +static inline u128 u128_sub(u128 a, u128 b) +{ + uint64_t diff = a.low - b.low; + uint64_t borrow = (a.low < b.low) ? 1 : 0; + + return U128(a.high - b.high - borrow, diff); +} + +/* + * Left shift by n bits. + */ + +static inline u128 u128_lshift(u128 a, uint32_t n) +{ + if (n >= 128) + return U128_ZERO; + else if (n >= 64) + return U128(a.low << (n - 64), 0ULL); + else + return U128((a.high << n) | (a.low >> (64 - n)), a.low << n); +} + +/* + * Right shift by n bits. + */ + +static inline u128 u128_rshift(u128 a, uint32_t n) +{ + if (n >= 128) + return U128_ZERO; + else if (n >= 64) + return U128(0ULL, a.high >> (n - 64)); + else + return U128(a.high >> n, (a.low >> n) | (a.high << (64 - n))); +} + +/* + * Bitwise AND of two u128 values. + */ + +static inline u128 u128_and(u128 a, u128 b) +{ + return U128(a.high & b.high, a.low & b.low); +} + +/* + * Bitwise OR of two u128 values. + */ + +static inline u128 u128_or(u128 a, u128 b) +{ + return U128(a.high | b.high, a.low | b.low); +} + +/* + * Logical NOT (negation) of a u128 value. + */ + +static inline u128 u128_not(u128 a) +{ + if (!a.low && !a.high) + return U128(0ULL, 1ULL); + else + return U128(0ULL, 0ULL); +} + +/* + * Print a u128 value in hexadecimal format (lowercase). + */ + +static inline void u128_print(u128 a) +{ + printf("%016llx%016llx\n", (unsigned long long)a.high, (unsigned long long)a.low); +} + +#endif /* end of include guard TYPES_H */ + diff --git a/packaging/android/AndroidManifest.xml b/packaging/android/AndroidManifest.xml new file mode 100644 index 000000000000..c13b9966c63d --- /dev/null +++ b/packaging/android/AndroidManifest.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/android/res/drawable/ic_launcher_background.xml b/packaging/android/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000000..9c3a415e54a1 --- /dev/null +++ b/packaging/android/res/drawable/ic_launcher_background.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/packaging/android/res/drawable/ic_launcher_foreground.xml b/packaging/android/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000000..248038dd13df --- /dev/null +++ b/packaging/android/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,382 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/android/res/mipmap-anydpi-v26/ic_launcher.xml b/packaging/android/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000000..d378acd7ac99 --- /dev/null +++ b/packaging/android/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/packaging/android/src/org/mixxx/MainActivity.java b/packaging/android/src/org/mixxx/MainActivity.java new file mode 100644 index 000000000000..2e06c7728ca4 --- /dev/null +++ b/packaging/android/src/org/mixxx/MainActivity.java @@ -0,0 +1,27 @@ +package org.mixxx; + +import android.os.Bundle; +import android.view.WindowManager; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; +import org.qtproject.qt.android.QtActivityBase; + +public class MainActivity extends QtActivityBase { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Disable drawing over cutout - isn't working + WindowManager.LayoutParams lp = this.getWindow().getAttributes(); + lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER; + + // Disable system and navigation bar to prevent accidental back or app switch + WindowInsetsControllerCompat windowInsetsController = + WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); + windowInsetsController.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars()); + } +} diff --git a/packaging/android/src/org/mixxx/UsbPermission.java b/packaging/android/src/org/mixxx/UsbPermission.java new file mode 100644 index 000000000000..731f6082083b --- /dev/null +++ b/packaging/android/src/org/mixxx/UsbPermission.java @@ -0,0 +1,51 @@ +package org.mixxx; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; +import android.util.Log; + +public class UsbPermission { + private static final String ACTION_USB_PERMISSION = + "org.mixxx.permissions.USB_PERMISSION"; + private static final String TAG = "MixxxUsbPermission"; + private static native void usbDeviceAccessResult(Object device, boolean granted); + public boolean registerServiceBroadcastReceiver(Context context) { + try { + IntentFilter intentFilter = new IntentFilter(ACTION_USB_PERMISSION); + context.registerReceiver(usbPermissionReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED); + Log.i(TAG, "Registered broadcast receiver"); + return true; + } catch (Exception e) { + Log.w(TAG, "Unable to register the broadcast receiver: " + e.toString()); + return false; + } + } + + private final BroadcastReceiver usbPermissionReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + Log.v(TAG, "Received " + action); + if (ACTION_USB_PERMISSION.equals(action)) { + synchronized (this) { + UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + if (usbDevice == null) { + Log.e(TAG, "USB device is null"); + return; + } + boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); + usbDeviceAccessResult(usbDevice, granted); + if (!granted) { + Log.w(TAG, "Permission was denied"); + } else { + Log.i(TAG, "Permission was granted"); + } + } + } + } + }; +} diff --git a/packaging/debian/control.in b/packaging/debian/control.in index 60ea639f6d58..f5faa0254c42 100644 --- a/packaging/debian/control.in +++ b/packaging/debian/control.in @@ -11,15 +11,20 @@ Build-Depends: debhelper (>= 11), qtkeychain-qt6-dev, qt6-declarative-private-dev, qt6-base-private-dev, - qt6-qpa-plugins, + qt6-multimedia-dev, qml6-module-qt5compat-graphicaleffects, + qml6-module-qtmultimedia, qml6-module-qtqml-workerscript, qml6-module-qtquick-controls, + qml6-module-qtquick-dialogs, qml6-module-qtquick-layouts, qml6-module-qtquick-shapes, qml6-module-qtquick-templates, qml6-module-qtquick-window, + qml6-module-qtquick-dialogs, qml6-module-qt-labs-qmlmodels, + qml6-module-qt-labs-folderlistmodel, + libqt6core5compat6-dev, libqt6opengl6-dev, libqt6sql6-sqlite, libqt6svg6-dev, diff --git a/res/controllers/Dummy Device Screen.hid.xml b/res/controllers/Dummy Device Screen.hid.xml index 1d6526459a82..4a0b7fd23a5c 100644 --- a/res/controllers/Dummy Device Screen.hid.xml +++ b/res/controllers/Dummy Device Screen.hid.xml @@ -62,6 +62,14 @@ white + +